diff --git a/.travis.yml b/.travis.yml index af468095aed2e9db4d963043678ac60eb0229e35..711713eb690688e32b2492e9f4e46eac737274dd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,16 +1,5 @@ +sudo: false language: rust -rust: - - stable - - beta - - nightly script: - cargo build --verbose - cargo test --verbose - - cargo doc -after_success: | - [ $TRAVIS_BRANCH = master ] && - [ $TRAVIS_PULL_REQUEST = false ] && - bash deploy-docs.sh -env: - global: - secure: "XxaPXHiVplTwMaAytYC0VQR/nNnm7SJVzXiUuaVEjssHip0Uje/4f3vGqtJjnD70FfxwNWQKiSYOcbYjWPlsJeANRt4ZoCsRt5eLGUZ+wH79n1fOkp5EIpFT/isjCB51A4n8PRUvuWfQ2OtNNeGLL6akMxt19sHdXoiQkLOe338=" diff --git a/Cargo.toml b/Cargo.toml index c35c55d57c7854dde462b3740b7391db2e2b10e9..784c6490880997a304279cbbf15f52f79022d693 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustyline" -version = "1.0.0" +version = "2.0.0-alpha" authors = ["Katsu Kawakami <kkawa1570@gmail.com>"] description = "Rustyline, a readline implementation based on Antirez's Linenoise" documentation = "http://docs.rs/rustyline" @@ -13,25 +13,29 @@ categories = ["command-line-interface"] [badges] travis-ci = { repository = "kkawakam/rustyline" } appveyor = { repository = "kkawakam/rustyline" } - +maintenance = { status = "actively-developed" } [dependencies] -libc = "0.2.7" -log = "0.3" -unicode-width = "0.1.3" +dirs = "1.0" +libc = "0.2" +log = "0.4" +unicode-width = "0.1" unicode-segmentation = "1.0" -encode_unicode = "0.1.3" +memchr = "2.0" [target.'cfg(all(unix, not(any(target_os = "fuchsia"))))'.dependencies] -nix = "0.8" +nix = "0.11" + +[target.'cfg(unix)'.dependencies] +utf8parse = "0.1" [target.'cfg(target_os = "fuchsia")'.dependencies] fuchsia-zircon = "0.3.2" fuchsia-device = "0.1.0" [target.'cfg(windows)'.dependencies] -winapi = "0.2" -kernel32-sys = "0.2" +winapi = { version = "0.3", features = ["consoleapi", "handleapi", "minwindef", "processenv", "winbase", "wincon", "winuser"] } [dev-dependencies] -tempdir = "0.3.4" +tempdir = "0.3" +assert_matches = "1.2" diff --git a/README.md b/README.md index f031d1b107606d9c14f1308ae504a5ac0201e89b..5f313104ec7ec5cbb125a7e87afaaf73f0769447 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,21 @@ # RustyLine [](https://travis-ci.org/kkawakam/rustyline) -[](https://ci.appveyor.com/project/kkawakam/rustyline/branch/master) -[](https://clippy.bashy.io/github/kkawakam/rustyline/master/log) +[](https://ci.appveyor.com/project/kkawakam/rustyline/branch/master) +[](https://deps.rs/repo/github/kkawakam/rustyline) [](https://crates.io/crates/rustyline) +[](https://docs.rs/rustyline) Readline implementation in Rust that is based on [Antirez' Linenoise](https://github.com/antirez/linenoise) -[Documentation (Releases)](https://docs.rs/rustyline) - -[Documentation (Master)](https://kkawakam.github.io/rustyline/rustyline/) - **Supported Platforms** -* Linux +* Unix (tested on FreeBSD, Linux and macOS) * Windows * cmd.exe * Powershell -**Note**: Powershell ISE is not supported, check [issue #56](https://github.com/kkawakam/rustyline/issues/56) - -## Build -This project uses Cargo and Rust stable -```bash -cargo build --release -``` +**Note**: +* Powershell ISE is not supported, check [issue #56](https://github.com/kkawakam/rustyline/issues/56) +* Mintty (Cygwin/Mingw) is not supported ## Example ```rust @@ -34,14 +27,14 @@ use rustyline::Editor; fn main() { // `()` can be used when no completer is required let mut rl = Editor::<()>::new(); - if let Err(_) = rl.load_history("history.txt") { + if rl.load_history("history.txt").is_err() { println!("No previous history."); } loop { let readline = rl.readline(">> "); match readline { Ok(line) => { - rl.add_history_entry(&line); + rl.add_history_entry(line.as_ref()); println!("Line: {}", line); }, Err(ReadlineError::Interrupted) => { @@ -76,10 +69,11 @@ rustyline = "1.0.0" - Unicode (UTF-8) (linenoise supports only ASCII) - Word completion (linenoise supports only line completion) - Filename completion - - History search ([Searching for Commands in the History](http://cnswww.cns.cwru.edu/php/chet/readline/readline.html#SEC8)) - - Kill ring ([Killing Commands](http://cnswww.cns.cwru.edu/php/chet/readline/readline.html#IDX3)) - - Multi line mode + - History search ([Searching for Commands in the History](http://tiswww.case.edu/php/chet/readline/readline.html#SEC8)) + - Kill ring ([Killing Commands](http://tiswww.case.edu/php/chet/readline/readline.html#IDX3)) + - Multi line mode (line wrapping) - Word commands + - Hints ## Actions @@ -102,6 +96,7 @@ Ctrl-V | Insert any special character without perfoming its associated act Ctrl-W | Delete word leading up to cursor (using white space as a word boundary) Ctrl-Y | Paste from Yank buffer Ctrl-Z | Suspend (unix only) +Ctrl-_ | Undo ### Emacs mode (default mode) @@ -117,6 +112,7 @@ Ctrl-K | Delete from cursor to end of line Ctrl-L | Clear screen Ctrl-N, Down | Next match from history Ctrl-P, Up | Previous match from history +Ctrl-X Ctrl-U | Undo Ctrl-Y | Paste from Yank buffer (Meta-Y to paste next yank instead) Meta-< | Move to first entry in history Meta-> | Move to last entry in history @@ -169,6 +165,7 @@ s | Delete a single character under the cursor and enter input mode S | Change current line (equivalent to 0c$) t<char> | Move right to the next occurance of `char`, then one char backward T<char> | Move left to the previous occurance of `char`, then one char forward +u | Undo w | Move one word or token right W | Move one non-blank word right x | Delete a single character under the cursor @@ -187,12 +184,6 @@ Esc | Switch to command mode [Terminal codes (ANSI/VT100)](http://wiki.bash-hackers.org/scripting/terminalcodes) -## ToDo - - - Undos - - Read input with timeout to properly handle single ESC key - - expose an API callable from C - ## Wine ```sh @@ -214,10 +205,26 @@ $ bind -p ## Similar projects - - [copperline](https://github.com/srijs/rust-copperline) (Rust) - - [linefeed](https://github.com/murarth/linefeed) (Rust) - - [liner](https://github.com/MovingtoMars/liner) (Rust) - - [linenoise-ng](https://github.com/arangodb/linenoise-ng) (C++) - - [liner](https://github.com/peterh/liner) (Go) - - [readline](https://github.com/chzyer/readline) (Go) - - [haskeline](https://github.com/judah/haskeline) (Haskell) +Library | Lang | OS | Term | Unicode | History | Completion | Keymap | Kill Ring | Undo | Colors | Hint/Auto suggest | +-------- | ---- | -- | ---- | ------- | ------- | ---------- | ------- | --------- | ---- | ------ | ----------------- | +[Go-prompt][] | Go | Ux/win | ANSI | Yes | Yes | any | Emacs/prog | No | No | Yes | Yes | +[Haskeline][] | Haskell | Ux/Win | Any | Yes | Yes | any | Emacs/Vi/conf | Yes | Yes | ? | ? | +[Linenoise][] | C | Ux | ANSI | No | Yes | only line | Emacs | No | No | Ux | Yes | +[Linenoise-ng][] | C | Ux/Win | ANSI | Yes | Yes | only line | Emacs | Yes | No | ? | ? | +[Linefeed][] | Rust | Ux/Win | Any | | Yes | any | Emacs/conf | Yes | No | ? | No | +[Liner][] | Rust | Ux | ANSI | | No inc search | only word | Emacs/Vi/prog | No | Yes | Ux | History based | +[Prompt-toolkit][] | Python | Ux/Win | ANSI | Yes | Yes | any | Emacs/Vi/conf | Yes | Yes | Ux/Win | Yes | +[Rb-readline][] | Ruby | Ux/Win | ANSI | Yes | Yes | only word | Emacs/Vi/conf | Yes | Yes | ? | No | +[Replxx][] | C/C++ | Ux/Win | ANSI | Yes | Yes | only line | Emacs | Yes | No | Ux/Win | Yes | +Rustyline | Rust | Ux/Win | ANSI | Yes | Yes | any | Emacs/Vi/bind | Yes | Yes | Ux/Win 10+ | Yes | + +[Go-prompt]: https://github.com/c-bata/go-prompt +[Haskeline]: https://github.com/judah/haskeline +[Linefeed]: https://github.com/murarth/linefeed +[Linenoise]: https://github.com/antirez/linenoise +[Linenoise-ng]: https://github.com/arangodb/linenoise-ng +[Liner]: https://github.com/redox-os/liner +[Prompt-toolkit]: https://github.com/jonathanslenders/python-prompt-toolkit +[Rb-readline]: https://github.com/ConnorAtherton/rb-readline +[Replxx]: https://github.com/AmokHuginnsson/replxx + diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000000000000000000000000000000000000..d4cd2633ff1757f825e9ac3b0ace5c8d7459a412 --- /dev/null +++ b/TODO.md @@ -0,0 +1,83 @@ +API +- [ ] expose an API callable from C + +Async (#126) + +Bell +- [ ] bell-style + +Color +- [X] ANSI Colors & Windows 10+ +- [ ] ANSI Colors & Windows <10 (https://docs.rs/console/0.6.1/console/fn.strip_ansi_codes.html ? https://github.com/mattn/go-colorable/blob/master/colorable_windows.go) +- [ ] Syntax highlighting +- [ ] clicolors spec (https://docs.rs/console/0.6.1/console/fn.colors_enabled.html) + +Completion +- [X] Quoted path +- [ ] Windows escape/unescape space in path +- [ ] file completion & escape/unescape (#106) +- [ ] file completion & tilde (#62) +- [X] display versus replacement +- [ ] composite/alternate completer (if the current completer returns nothing, try the next one) + +Config +- [ ] Maximum buffer size for the line read + +Cursor +- [ ] insert versus overwrite versus command mode +- [ ] In Vi command mode, prevent user from going to end of line. (#94) + +Grapheme +- [ ] grapheme & input auto-wrap are buggy + +Hints Callback +- [X] Not implemented on windows +- [ ] Do an implementation based on previous history + +History +- [ ] Move to the history line n +- [ ] historyFile: Where to read/write the history at the start and end of +each line input session. +- [ ] append_history +- [ ] history_truncate_file + +Input +- [ ] Password input (#58) +- [X] quoted insert (#65) +- [ ] quoted TAB (`\t`) insert and width +- [ ] Overwrite mode (em-toggle-overwrite, vi-replace-mode, rl_insert_mode) +- [ ] Encoding +- [ ] [Ctrl-][Alt-][Shift-]<Key> (#121) + +Mouse +- [ ] Mouse support + +Movement +- [ ] Move to the corresponding opening/closing bracket + +Redo +- [X] redo substitute + +Repeat +- [X] dynamic prompt (arg: ?) +- [ ] transpose chars + +Syntax +- [ ] syntax specific tokenizer/parser +- [ ] highlighting + +Undo +- [ ] Merge consecutive Replace +- [X] Undo group +- [ ] Undo all changes made to this line. +- [X] Kill+Insert (substitute/replace) +- [X] Repeated undo `Undo(RepeatCount)` + +Unix +- [ ] Terminfo (https://github.com/Stebalien/term) +- [ ] [ncurses](https://crates.io/crates/ncurses) alternative backend ? + +Windows +- [ ] is_atty is not working with cygwin/msys (https://github.com/softprops/atty works but then how to make `enable_raw_mode` works ?) +- [X] UTF-16 surrogate pair +- [ ] handle ansi escape code (https://docs.rs/console/0.6.1/console/fn.strip_ansi_codes.html ? https://github.com/mattn/go-colorable/blob/master/colorable_windows.go) diff --git a/appveyor.yml b/appveyor.yml index 8934d3077a2c7256d299e6990d1a88a2ee921890..4506f3af83c2ae62fb45caec18f0bbc018d9ce5d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,14 +1,9 @@ environment: - matrix: - - TARGET: 1.16.0-x86_64-pc-windows-msvc - - TARGET: 1.16.0-x86_64-pc-windows-gnu - - TARGET: beta-x86_64-pc-windows-msvc - - TARGET: beta-x86_64-pc-windows-gnu + TARGET: x86_64-pc-windows-msvc install: - - ps: Start-FileDownload "https://static.rust-lang.org/dist/rust-${env:TARGET}.exe" - - rust-%TARGET%.exe /VERYSILENT /NORESTART /DIR="C:\Program Files (x86)\Rust" - - SET PATH=%PATH%;C:\Program Files (x86)\Rust\bin - - SET PATH=%PATH%;C:\MinGW\bin + - appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe + - rustup-init -yv --default-toolchain stable --default-host %TARGET% + - set PATH=%PATH%;%USERPROFILE%\.cargo\bin - rustc -V - cargo -V diff --git a/deploy-docs.sh b/deploy-docs.sh deleted file mode 100755 index efd5a56cdf371cccf62b61614a2d5af7a043caa9..0000000000000000000000000000000000000000 --- a/deploy-docs.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash - -rev=$(git rev-parse --short HEAD) - -cd target/doc - -echo '<meta http-equiv=refresh content=0;url=rustyline/index.html>' > index.html - -git init -git config user.name "Katsu Kawakami" -git config user.email "kkawa1570@gmail.com" - -git remote add upstream "https://$GH_TOKEN@github.com/kkawakam/rustyline.git" -git fetch upstream -git push upstream --delete gh-pages > /dev/null 2>&1 - -touch . - -git add -A . -git commit -m "rebuild pages at ${rev}" -git push -q upstream HEAD:gh-pages > /dev/null 2>&1 diff --git a/examples/example.rs b/examples/example.rs index 8f0233736d2100a2de17c52936a914597af6d584..ce41cf4e2a34b4346f455a7575e427a809e8a2fa 100644 --- a/examples/example.rs +++ b/examples/example.rs @@ -1,22 +1,51 @@ extern crate log; extern crate rustyline; -use std::io::{self, Write}; -use log::{LogRecord, LogLevel, LogLevelFilter, LogMetadata, SetLoggerError}; +use log::{Level, LevelFilter, Metadata, Record, SetLoggerError}; +use std::borrow::Cow::{self, Borrowed, Owned}; -use rustyline::completion::FilenameCompleter; +use rustyline::completion::{Completer, FilenameCompleter, Pair}; use rustyline::error::ReadlineError; -use rustyline::{Cmd, Config, CompletionType, Editor, EditMode, KeyPress}; +use rustyline::highlight::Highlighter; +use rustyline::hint::Hinter; +use rustyline::{Cmd, CompletionType, Config, EditMode, Editor, Helper, KeyPress}; -// On unix platforms you can use ANSI escape sequences -#[cfg(unix)] -static PROMPT: &'static str = "\x1b[1;32m>>\x1b[0m "; +static COLORED_PROMPT: &'static str = "\x1b[1;32m>>\x1b[0m "; -// Windows consoles typically don't support ANSI escape sequences out -// of the box -#[cfg(windows)] static PROMPT: &'static str = ">> "; +struct MyHelper(FilenameCompleter); + +impl Completer for MyHelper { + type Candidate = Pair; + + fn complete(&self, line: &str, pos: usize) -> Result<(usize, Vec<Pair>), ReadlineError> { + self.0.complete(line, pos) + } +} + +impl Hinter for MyHelper { + fn hint(&self, line: &str, _pos: usize) -> Option<String> { + if line == "hello" { + Some(" World".to_owned()) + } else { + None + } + } +} + +impl Highlighter for MyHelper { + fn highlight_prompt<'p>(&self, _: &str) -> Cow<'static, str> { + Borrowed(COLORED_PROMPT) + } + + fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { + Owned("\x1b[1m".to_owned() + hint + "\x1b[m") + } +} + +impl Helper for MyHelper {} + fn main() { init_logger().is_ok(); let config = Config::builder() @@ -24,9 +53,9 @@ fn main() { .completion_type(CompletionType::List) .edit_mode(EditMode::Emacs) .build(); - let c = FilenameCompleter::new(); + let h = MyHelper(FilenameCompleter::new()); let mut rl = Editor::with_config(config); - rl.set_completer(Some(c)); + rl.set_helper(Some(h)); rl.bind_sequence(KeyPress::Meta('N'), Cmd::HistorySearchForward); rl.bind_sequence(KeyPress::Meta('P'), Cmd::HistorySearchBackward); if rl.load_history("history.txt").is_err() { @@ -56,23 +85,25 @@ fn main() { rl.save_history("history.txt").unwrap(); } +static LOGGER: Logger = Logger; struct Logger; impl log::Log for Logger { - fn enabled(&self, metadata: &LogMetadata) -> bool { - metadata.level() <= LogLevel::Debug + fn enabled(&self, metadata: &Metadata) -> bool { + metadata.level() <= Level::Debug } - fn log(&self, record: &LogRecord) { + fn log(&self, record: &Record) { if self.enabled(record.metadata()) { - writeln!(io::stderr(), "{} - {}", record.level(), record.args()).unwrap(); + eprintln!("{} - {}", record.level(), record.args()); } } + + fn flush(&self) {} } fn init_logger() -> Result<(), SetLoggerError> { - log::set_logger(|max_log_level| { - max_log_level.set(LogLevelFilter::Info); - Box::new(Logger) - }) + try!(log::set_logger(&LOGGER)); + log::set_max_level(LevelFilter::Info); + Ok(()) } diff --git a/index.html b/index.html deleted file mode 100644 index 269d4c6dccc519456451ea51b90403c83f02428c..0000000000000000000000000000000000000000 --- a/index.html +++ /dev/null @@ -1 +0,0 @@ -<meta http-equiv=refresh content=0;url=rustyline/index.html> diff --git a/rustfmt.toml b/rustfmt.toml index 6496add0b747be108c2a26d5848d6604179d3a12..83697e3d738df0d9d3d80b86e04346c33b066804 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,3 +1,4 @@ -reorder_imports = false -normalise_comments = false -write_mode = "Overwrite" \ No newline at end of file +wrap_comments = true +format_strings = true +error_on_unformatted = false +reorder_impl_items = true diff --git a/src/char_iter.rs b/src/char_iter.rs deleted file mode 100644 index 1866dbd8031921628603ebdc664df83f3cb90d6e..0000000000000000000000000000000000000000 --- a/src/char_iter.rs +++ /dev/null @@ -1,111 +0,0 @@ -//! An iterator over the `char`s of a reader. -//! -//! A copy of the unstable code from the stdlib's std::io::Read::chars. -//! TODO: Remove this once [Read::chars](https://github.com/rust-lang/rust/issues/27802) has been stabilized - -use std::error; -use std::fmt; -use std::io; -use std::io::Read; -use std::str; - -pub fn chars<R: Read>(read: R) -> Chars<R> - where R: Sized -{ - Chars { inner: read } -} - -// https://tools.ietf.org/html/rfc3629 -static UTF8_CHAR_WIDTH: [u8; 256] = [ - 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, - 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // 0x1F - 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, - 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // 0x3F - 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, - 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // 0x5F - 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, - 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // 0x7F - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 0x9F - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 0xBF - 0,0,2,2,2,2,2,2,2,2,2,2,2,2,2,2, - 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, // 0xDF - 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, // 0xEF - 4,4,4,4,4,0,0,0,0,0,0,0,0,0,0,0, // 0xFF -]; - -/// Given a first byte, determine how many bytes are in this UTF-8 character -#[inline] -fn utf8_char_width(b: u8) -> usize { - return UTF8_CHAR_WIDTH[b as usize] as usize; -} - -pub struct Chars<R> { - inner: R, -} - -#[derive(Debug)] -pub enum CharsError { - NotUtf8, - Other(io::Error), -} - -impl<R: Read> Iterator for Chars<R> { - type Item = Result<char, CharsError>; - - fn next(&mut self) -> Option<Result<char, CharsError>> { - let mut buf = [0]; - let first_byte = match self.inner.read(&mut buf) { - Ok(0) => return None, - Ok(..) => buf[0], - Err(e) => return Some(Err(CharsError::Other(e))), - }; - let width = utf8_char_width(first_byte); - if width == 1 { - return Some(Ok(first_byte as char)); - } - if width == 0 { - return Some(Err(CharsError::NotUtf8)); - } - let mut buf = [first_byte, 0, 0, 0]; - { - let mut start = 1; - while start < width { - match self.inner.read(&mut buf[start..width]) { - Ok(0) => return Some(Err(CharsError::NotUtf8)), - Ok(n) => start += n, - Err(e) => return Some(Err(CharsError::Other(e))), - } - } - } - Some(match str::from_utf8(&buf[..width]).ok() { - Some(s) => Ok(s.chars().next().unwrap()), - None => Err(CharsError::NotUtf8), - }) - } -} - -impl error::Error for CharsError { - fn description(&self) -> &str { - match *self { - CharsError::NotUtf8 => "invalid utf8 encoding", - CharsError::Other(ref e) => error::Error::description(e), - } - } - fn cause(&self) -> Option<&error::Error> { - match *self { - CharsError::NotUtf8 => None, - CharsError::Other(ref e) => e.cause(), - } - } -} - -impl fmt::Display for CharsError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - CharsError::NotUtf8 => "byte stream did not contain valid utf8".fmt(f), - CharsError::Other(ref e) => e.fmt(f), - } - } -} diff --git a/src/completion.rs b/src/completion.rs index 9c07c4d2f5c7b038a6c30b30f6dfd898c90504ce..ab7a020adfe9a820d84dc833bd7b15a937832858 100644 --- a/src/completion.rs +++ b/src/completion.rs @@ -1,22 +1,60 @@ //! Completion API use std::borrow::Cow::{self, Borrowed, Owned}; -use std::collections::BTreeSet; use std::fs; use std::path::{self, Path}; use super::Result; use line_buffer::LineBuffer; +use memchr::memchr; // TODO: let the implementers choose/find word boudaries ??? -// (line, pos) is like (rl_line_buffer, rl_point) to make contextual completion ("select t.na| from tbl as t") -// TOOD: make &self &mut self ??? +// (line, pos) is like (rl_line_buffer, rl_point) to make contextual completion +// ("select t.na| from tbl as t") +// TODO: make &self &mut self ??? + +/// A completion candidate. +pub trait Candidate { + /// Text to display when listing alternatives. + fn display(&self) -> &str; + /// Text to insert in line. + fn replacement(&self) -> &str; +} + +impl Candidate for String { + fn display(&self) -> &str { + self.as_str() + } + + fn replacement(&self) -> &str { + self.as_str() + } +} + +pub struct Pair { + pub display: String, + pub replacement: String, +} + +impl Candidate for Pair { + fn display(&self) -> &str { + self.display.as_str() + } + + fn replacement(&self) -> &str { + self.replacement.as_str() + } +} /// To be called for tab-completion. pub trait Completer { + type Candidate: Candidate; + /// Takes the currently edited `line` with the cursor `pos`ition and - /// returns the start position and the completion candidates for the partial word to be completed. - /// "ls /usr/loc" => Ok((3, vec!["/usr/local/"])) - fn complete(&self, line: &str, pos: usize) -> Result<(usize, Vec<String>)>; + /// returns the start position and the completion candidates for the + /// partial word to be completed. + /// + /// ("ls /usr/loc", 11) => Ok((3, vec!["/usr/local/"])) + fn complete(&self, line: &str, pos: usize) -> Result<(usize, Vec<Self::Candidate>)>; /// Updates the edited `line` with the `elected` candidate. fn update(&self, line: &mut LineBuffer, start: usize, elected: &str) { let end = line.pos(); @@ -25,18 +63,24 @@ pub trait Completer { } impl Completer for () { + type Candidate = String; + fn complete(&self, _line: &str, _pos: usize) -> Result<(usize, Vec<String>)> { - Ok((0, Vec::new())) + Ok((0, Vec::with_capacity(0))) } + fn update(&self, _line: &mut LineBuffer, _start: usize, _elected: &str) { unreachable!() } } impl<'c, C: ?Sized + Completer> Completer for &'c C { - fn complete(&self, line: &str, pos: usize) -> Result<(usize, Vec<String>)> { + type Candidate = C::Candidate; + + fn complete(&self, line: &str, pos: usize) -> Result<(usize, Vec<Self::Candidate>)> { (**self).complete(line, pos) } + fn update(&self, line: &mut LineBuffer, start: usize, elected: &str) { (**self).update(line, start, elected) } @@ -45,7 +89,9 @@ macro_rules! box_completer { ($($id: ident)*) => { $( impl<C: ?Sized + Completer> Completer for $id<C> { - fn complete(&self, line: &str, pos: usize) -> Result<(usize, Vec<String>)> { + type Candidate = C::Candidate; + + fn complete(&self, line: &str, pos: usize) -> Result<(usize, Vec<Self::Candidate>)> { (**self).complete(line, pos) } fn update(&self, line: &mut LineBuffer, start: usize, elected: &str) { @@ -62,24 +108,49 @@ box_completer! { Box Rc Arc } /// A `Completer` for file and folder names. pub struct FilenameCompleter { - break_chars: BTreeSet<char>, + break_chars: &'static [u8], + double_quotes_special_chars: &'static [u8], } +static DOUBLE_QUOTES_ESCAPE_CHAR: Option<char> = Some('\\'); + +// rl_basic_word_break_characters, rl_completer_word_break_characters #[cfg(unix)] -static DEFAULT_BREAK_CHARS: [char; 18] = [' ', '\t', '\n', '"', '\\', '\'', '`', '@', '$', '>', - '<', '=', ';', '|', '&', '{', '(', '\0']; +static DEFAULT_BREAK_CHARS: [u8; 18] = [ + b' ', b'\t', b'\n', b'"', b'\\', b'\'', b'`', b'@', b'$', b'>', b'<', b'=', b';', b'|', b'&', + b'{', b'(', b'\0', +]; #[cfg(unix)] static ESCAPE_CHAR: Option<char> = Some('\\'); // Remove \ to make file completion works on windows #[cfg(windows)] -static DEFAULT_BREAK_CHARS: [char; 17] = [' ', '\t', '\n', '"', '\'', '`', '@', '$', '>', '<', - '=', ';', '|', '&', '{', '(', '\0']; +static DEFAULT_BREAK_CHARS: [u8; 17] = [ + b' ', b'\t', b'\n', b'"', b'\'', b'`', b'@', b'$', b'>', b'<', b'=', b';', b'|', b'&', b'{', + b'(', b'\0', +]; #[cfg(windows)] static ESCAPE_CHAR: Option<char> = None; +// In double quotes, not all break_chars need to be escaped +// https://www.gnu.org/software/bash/manual/html_node/Double-Quotes.html +#[cfg(unix)] +static DOUBLE_QUOTES_SPECIAL_CHARS: [u8; 4] = [b'"', b'$', b'\\', b'`']; +#[cfg(windows)] +static DOUBLE_QUOTES_SPECIAL_CHARS: [u8; 1] = [b'"']; // TODO Validate: only '"' ? + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Quote { + Double, + Single, + None, +} + impl FilenameCompleter { pub fn new() -> FilenameCompleter { - FilenameCompleter { break_chars: DEFAULT_BREAK_CHARS.iter().cloned().collect() } + FilenameCompleter { + break_chars: &DEFAULT_BREAK_CHARS, + double_quotes_special_chars: &DOUBLE_QUOTES_SPECIAL_CHARS, + } } } @@ -90,10 +161,35 @@ impl Default for FilenameCompleter { } impl Completer for FilenameCompleter { - fn complete(&self, line: &str, pos: usize) -> Result<(usize, Vec<String>)> { - let (start, path) = extract_word(line, pos, ESCAPE_CHAR, &self.break_chars); - let path = unescape(path, ESCAPE_CHAR); - let matches = try!(filename_complete(&path, ESCAPE_CHAR, &self.break_chars)); + type Candidate = Pair; + + fn complete(&self, line: &str, pos: usize) -> Result<(usize, Vec<Pair>)> { + let (start, path, esc_char, break_chars, quote) = + if let Some((idx, quote)) = find_unclosed_quote(&line[..pos]) { + let start = idx + 1; + if quote == Quote::Double { + ( + start, + unescape(&line[start..pos], DOUBLE_QUOTES_ESCAPE_CHAR), + DOUBLE_QUOTES_ESCAPE_CHAR, + &self.double_quotes_special_chars, + quote, + ) + } else { + ( + start, + Borrowed(&line[start..pos]), + None, + &self.break_chars, + quote, + ) + } + } else { + let (start, path) = extract_word(line, pos, ESCAPE_CHAR, &self.break_chars); + let path = unescape(path, ESCAPE_CHAR); + (start, path, ESCAPE_CHAR, &self.break_chars, Quote::None) + }; + let matches = try!(filename_complete(&path, esc_char, break_chars, quote)); Ok((start, matches)) } } @@ -104,15 +200,20 @@ pub fn unescape(input: &str, esc_char: Option<char>) -> Cow<str> { return Borrowed(input); } let esc_char = esc_char.unwrap(); - let n = input.chars().filter(|&c| c == esc_char).count(); - if n == 0 { + if !input.chars().any(|c| c == esc_char) { return Borrowed(input); } - let mut result = String::with_capacity(input.len() - n); + let mut result = String::with_capacity(input.len()); let mut chars = input.chars(); while let Some(ch) = chars.next() { if ch == esc_char { if let Some(ch) = chars.next() { + if cfg!(windows) && ch != '"' { + // TODO Validate: only '"' ? + result.push(esc_char); + } + result.push(ch); + } else if cfg!(windows) { result.push(ch); } } else { @@ -124,23 +225,35 @@ pub fn unescape(input: &str, esc_char: Option<char>) -> Cow<str> { /// Escape any `break_chars` in `input` string with `esc_char`. /// For example, '/User Information' becomes '/User\ Information' -/// when space is a breaking char and '\' the escape char. -pub fn escape(input: String, esc_char: Option<char>, break_chars: &BTreeSet<char>) -> String { - if esc_char.is_none() { - return input; +/// when space is a breaking char and '\\' the escape char. +pub fn escape( + mut input: String, + esc_char: Option<char>, + break_chars: &[u8], + quote: Quote, +) -> String { + if quote == Quote::Single { + return input; // no escape in single quotes } - let esc_char = esc_char.unwrap(); let n = input - .chars() - .filter(|c| break_chars.contains(c)) + .bytes() + .filter(|b| memchr(*b, break_chars).is_some()) .count(); if n == 0 { + return input; // no need to escape + } + if esc_char.is_none() { + if cfg!(windows) && quote == Quote::None { + input.insert(0, '"'); // force double quote + return input; + } return input; } + let esc_char = esc_char.unwrap(); let mut result = String::with_capacity(input.len() + n); for c in input.chars() { - if break_chars.contains(&c) { + if c.is_ascii() && memchr(c as u8, break_chars).is_some() { result.push(esc_char); } result.push(c); @@ -148,11 +261,14 @@ pub fn escape(input: String, esc_char: Option<char>, break_chars: &BTreeSet<char result } -fn filename_complete(path: &str, - esc_char: Option<char>, - break_chars: &BTreeSet<char>) - -> Result<Vec<String>> { - use std::env::{current_dir, home_dir}; +fn filename_complete( + path: &str, + esc_char: Option<char>, + break_chars: &[u8], + quote: Quote, +) -> Result<Vec<Pair>> { + use dirs::home_dir; + use std::env::current_dir; let sep = path::MAIN_SEPARATOR; let (dir_name, file_name) = match path.rfind(sep) { @@ -182,16 +298,21 @@ fn filename_complete(path: &str, dir_path.to_path_buf() }; - let mut entries: Vec<String> = Vec::new(); + let mut entries: Vec<Pair> = Vec::new(); for entry in try!(dir.read_dir()) { let entry = try!(entry); if let Some(s) = entry.file_name().to_str() { if s.starts_with(file_name) { - let mut path = String::from(dir_name) + s; - if try!(fs::metadata(entry.path())).is_dir() { - path.push(sep); - } - entries.push(escape(path, esc_char, break_chars)); + if let Ok(metadata) = fs::metadata(entry.path()) { + let mut path = String::from(dir_name) + s; + if metadata.is_dir() { + path.push(sep); + } + entries.push(Pair { + display: String::from(s), + replacement: escape(path, esc_char, break_chars, quote), + }); + } // else ignore PermissionDenied } } } @@ -202,11 +323,12 @@ fn filename_complete(path: &str, /// try to find backward the start of a word. /// Return (0, `line[..pos]`) if no break char has been found. /// Return the word and its start position (idx, `line[idx..pos]`) otherwise. -pub fn extract_word<'l>(line: &'l str, - pos: usize, - esc_char: Option<char>, - break_chars: &BTreeSet<char>) - -> (usize, &'l str) { +pub fn extract_word<'l>( + line: &'l str, + pos: usize, + esc_char: Option<char>, + break_chars: &[u8], +) -> (usize, &'l str) { let line = &line[..pos]; if line.is_empty() { return (0, line); @@ -222,7 +344,7 @@ pub fn extract_word<'l>(line: &'l str, break; } } - if break_chars.contains(&c) { + if c.is_ascii() && memchr(c as u8, break_chars).is_some() { start = Some(i + c.len_utf8()); if esc_char.is_none() { break; @@ -236,46 +358,109 @@ pub fn extract_word<'l>(line: &'l str, } } -pub fn longest_common_prefix(candidates: &[String]) -> Option<&str> { +pub fn longest_common_prefix<C: Candidate>(candidates: &[C]) -> Option<&str> { if candidates.is_empty() { return None; } else if candidates.len() == 1 { - return Some(&candidates[0]); + return Some(&candidates[0].replacement()); } let mut longest_common_prefix = 0; 'o: loop { for (i, c1) in candidates.iter().enumerate().take(candidates.len() - 1) { - let b1 = c1.as_bytes(); - let b2 = candidates[i + 1].as_bytes(); - if b1.len() <= longest_common_prefix || b2.len() <= longest_common_prefix || - b1[longest_common_prefix] != b2[longest_common_prefix] { + let b1 = c1.replacement().as_bytes(); + let b2 = candidates[i + 1].replacement().as_bytes(); + if b1.len() <= longest_common_prefix + || b2.len() <= longest_common_prefix + || b1[longest_common_prefix] != b2[longest_common_prefix] + { break 'o; } } longest_common_prefix += 1; } - while !candidates[0].is_char_boundary(longest_common_prefix) { + let candidate = candidates[0].replacement(); + while !candidate.is_char_boundary(longest_common_prefix) { longest_common_prefix -= 1; } if longest_common_prefix == 0 { return None; } - Some(&candidates[0][0..longest_common_prefix]) + Some(&candidate[0..longest_common_prefix]) +} + +#[derive(PartialEq)] +enum ScanMode { + DoubleQuote, + Escape, + EscapeInDoubleQuote, + Normal, + SingleQuote, +} + +/// try to find an unclosed single/double quote in `s`. +/// Return `None` if no unclosed quote is found. +/// Return the unclosed quote position and if it is a double quote. +fn find_unclosed_quote(s: &str) -> Option<(usize, Quote)> { + let char_indices = s.char_indices(); + let mut mode = ScanMode::Normal; + let mut quote_index = 0; + for (index, char) in char_indices { + match mode { + ScanMode::DoubleQuote => { + if char == '"' { + mode = ScanMode::Normal; + } else if char == '\\' { + // both windows and unix support escape in double quote + mode = ScanMode::EscapeInDoubleQuote; + } + } + ScanMode::Escape => { + mode = ScanMode::Normal; + } + ScanMode::EscapeInDoubleQuote => { + mode = ScanMode::DoubleQuote; + } + ScanMode::Normal => { + if char == '"' { + mode = ScanMode::DoubleQuote; + quote_index = index; + } else if char == '\\' && cfg!(not(windows)) { + mode = ScanMode::Escape; + } else if char == '\'' && cfg!(not(windows)) { + mode = ScanMode::SingleQuote; + quote_index = index; + } + } + ScanMode::SingleQuote => { + if char == '\'' { + mode = ScanMode::Normal; + } // no escape in single quotes + } + }; + } + if ScanMode::DoubleQuote == mode || ScanMode::EscapeInDoubleQuote == mode { + return Some((quote_index, Quote::Double)); + } else if ScanMode::SingleQuote == mode { + return Some((quote_index, Quote::Single)); + } + None } #[cfg(test)] mod tests { - use std::collections::BTreeSet; - #[test] pub fn extract_word() { - let break_chars: BTreeSet<char> = super::DEFAULT_BREAK_CHARS.iter().cloned().collect(); + let break_chars: &[u8] = &super::DEFAULT_BREAK_CHARS; let line = "ls '/usr/local/b"; - assert_eq!((4, "/usr/local/b"), - super::extract_word(line, line.len(), Some('\\'), &break_chars)); + assert_eq!( + (4, "/usr/local/b"), + super::extract_word(line, line.len(), Some('\\'), &break_chars) + ); let line = "ls /User\\ Information"; - assert_eq!((3, "/User\\ Information"), - super::extract_word(line, line.len(), Some('\\'), &break_chars)); + assert_eq!( + (3, "/User\\ Information"), + super::extract_word(line, line.len(), Some('\\'), &break_chars) + ); } #[test] @@ -283,20 +468,31 @@ mod tests { use std::borrow::Cow::{self, Borrowed, Owned}; let input = "/usr/local/b"; assert_eq!(Borrowed(input), super::unescape(input, Some('\\'))); - let input = "/User\\ Information"; - let result: Cow<str> = Owned(String::from("/User Information")); - assert_eq!(result, super::unescape(input, Some('\\'))); + if cfg!(windows) { + let input = "c:\\users\\All Users\\"; + let result: Cow<str> = Borrowed(input); + assert_eq!(result, super::unescape(input, Some('\\'))); + } else { + let input = "/User\\ Information"; + let result: Cow<str> = Owned(String::from("/User Information")); + assert_eq!(result, super::unescape(input, Some('\\'))); + } } #[test] pub fn escape() { - let break_chars: BTreeSet<char> = super::DEFAULT_BREAK_CHARS.iter().cloned().collect(); + let break_chars: &[u8] = &super::DEFAULT_BREAK_CHARS; let input = String::from("/usr/local/b"); - assert_eq!(input.clone(), - super::escape(input, Some('\\'), &break_chars)); + assert_eq!( + input.clone(), + super::escape(input, Some('\\'), &break_chars, super::Quote::None) + ); let input = String::from("/User Information"); let result = String::from("/User\\ Information"); - assert_eq!(result, super::escape(input, Some('\\'), &break_chars)); + assert_eq!( + result, + super::escape(input, Some('\\'), &break_chars, super::Quote::None) + ); } #[test] @@ -333,4 +529,21 @@ mod tests { let lcp = super::longest_common_prefix(&candidates); assert_eq!(Some("f"), lcp); } + + #[test] + pub fn find_unclosed_quote() { + assert_eq!(None, super::find_unclosed_quote("ls /etc")); + assert_eq!( + Some((3, super::Quote::Double)), + super::find_unclosed_quote("ls \"User Information") + ); + assert_eq!( + None, + super::find_unclosed_quote("ls \"/User Information\" /etc") + ); + assert_eq!( + Some((0, super::Quote::Double)), + super::find_unclosed_quote("\"c:\\users\\All Users\\") + ) + } } diff --git a/src/config.rs b/src/config.rs index 3be614c8bc3c8d2ca3153cd0d8fd83213437d87c..86c04dc2cd01f40bbdaa836aa79a69df3173c5e2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,20 +1,27 @@ //! Customize line editor use std::default::Default; +/// User preferences #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Config { /// Maximum number of entries in History. - max_history_size: usize, + max_history_size: usize, // history_max_entries history_duplicates: HistoryDuplicates, history_ignore_space: bool, completion_type: CompletionType, /// When listing completion alternatives, only display /// one screen of possibilities at a time. completion_prompt_limit: usize, - /// Duration (milliseconds) Rustyline will wait for a character when reading an ambiguous key sequence. + /// Duration (milliseconds) Rustyline will wait for a character when + /// reading an ambiguous key sequence. keyseq_timeout: i32, - // Emacs or Vi mode + /// Emacs or Vi mode edit_mode: EditMode, + /// If true, each nonblank line returned by `readline` will be + /// automatically added to the history. + auto_add_history: bool, + /// if colors should be enabled. + color_mode: ColorMode, } impl Config { @@ -27,13 +34,17 @@ impl Config { self.max_history_size } - /// Tell if lines which match the previous history entry are saved or not in the history list. + /// Tell if lines which match the previous history entry are saved or not + /// in the history list. + /// /// By default, they are ignored. pub fn history_duplicates(&self) -> HistoryDuplicates { self.history_duplicates } - /// Tell if lines which begin with a space character are saved or not in the history list. + /// Tell if lines which begin with a space character are saved or not in + /// the history list. + /// /// By default, they are saved. pub fn history_ignore_space(&self) -> bool { self.history_ignore_space @@ -54,6 +65,20 @@ impl Config { pub fn edit_mode(&self) -> EditMode { self.edit_mode } + + /// Tell if lines are automatically added to the history. + /// + /// By default, they are not. + pub fn auto_add_history(&self) -> bool { + self.auto_add_history + } + + /// Tell if colors should be enabled. + /// + /// By default, they are except if stdout is not a tty. + pub fn color_mode(&self) -> ColorMode { + self.color_mode + } } impl Default for Config { @@ -64,8 +89,10 @@ impl Default for Config { history_ignore_space: false, completion_type: CompletionType::Circular, // TODO Validate completion_prompt_limit: 100, - keyseq_timeout: 500, + keyseq_timeout: -1, edit_mode: EditMode::Emacs, + auto_add_history: false, + color_mode: ColorMode::Enabled, } } } @@ -73,6 +100,7 @@ impl Default for Config { #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum HistoryDuplicates { AlwaysAdd, + /// a line will not be added to the history if it matches the previous entry IgnoreConsecutive, } @@ -86,12 +114,22 @@ pub enum CompletionType { List, } +/// Style of editing / Standard keymaps #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum EditMode { Emacs, Vi, } +/// Colorization mode +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ColorMode { + Enabled, + Forced, + Disabled, +} + +/// Configuration builder #[derive(Debug, Default)] pub struct Builder { p: Config, @@ -99,7 +137,9 @@ pub struct Builder { impl Builder { pub fn new() -> Builder { - Builder { p: Config::default() } + Builder { + p: Config::default(), + } } /// Set the maximum length for the history. @@ -108,7 +148,9 @@ impl Builder { self } - /// Tell if lines which match the previous history entry are saved or not in the history list. + /// Tell if lines which match the previous history entry are saved or not + /// in the history list. + /// /// By default, they are ignored. pub fn history_ignore_dups(mut self, yes: bool) -> Builder { self.p.history_duplicates = if yes { @@ -119,7 +161,9 @@ impl Builder { self } - /// Tell if lines which begin with a space character are saved or not in the history list. + /// Tell if lines which begin with a space character are saved or not in + /// the history list. + /// /// By default, they are saved. pub fn history_ignore_space(mut self, yes: bool) -> Builder { self.p.history_ignore_space = yes; @@ -132,16 +176,18 @@ impl Builder { self } - /// The number of possible completions that determines when the user is asked - /// whether the list of possibilities should be displayed. + /// The number of possible completions that determines when the user is + /// asked whether the list of possibilities should be displayed. pub fn completion_prompt_limit(mut self, completion_prompt_limit: usize) -> Builder { self.p.completion_prompt_limit = completion_prompt_limit; self } /// Timeout for ambiguous key sequences in milliseconds. - /// Currently, it is used only to distinguish a single ESC from an ESC sequence. - /// After seeing an ESC key, wait at most `keyseq_timeout_ms` for another byte. + /// Currently, it is used only to distinguish a single ESC from an ESC + /// sequence. + /// After seeing an ESC key, wait at most `keyseq_timeout_ms` for another + /// byte. pub fn keyseq_timeout(mut self, keyseq_timeout_ms: i32) -> Builder { self.p.keyseq_timeout = keyseq_timeout_ms; self @@ -150,6 +196,26 @@ impl Builder { /// Choose between Emacs or Vi mode. pub fn edit_mode(mut self, edit_mode: EditMode) -> Builder { self.p.edit_mode = edit_mode; + match edit_mode { + EditMode::Emacs => self.p.keyseq_timeout = -1, // no timeout + EditMode::Vi => self.p.keyseq_timeout = 500, + }; + self + } + + /// Tell if lines are automatically added to the history. + /// + /// By default, they are not. + pub fn auto_add_history(mut self, yes: bool) -> Builder { + self.p.auto_add_history = yes; + self + } + + /// Forces colorization on or off. + /// + /// By default, colorization is on except if stdout is not a tty. + pub fn color_mode(mut self, color_mode: ColorMode) -> Builder { + self.p.color_mode = color_mode; self } diff --git a/src/consts.rs b/src/consts.rs index aadac847a23da4e52ad543f3044008963a26e2cf..584101665369f29dc35f53975ddc5ec8a8d05906 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -5,19 +5,29 @@ pub enum KeyPress { UnknownEscSeq, Backspace, Char(char), + ControlDown, + ControlLeft, + ControlRight, + ControlUp, Ctrl(char), Delete, Down, End, Enter, // Ctrl('M') Esc, + F(u8), Home, + Insert, Left, Meta(char), Null, PageDown, PageUp, Right, + ShiftDown, + ShiftLeft, + ShiftRight, + ShiftUp, Tab, // Ctrl('I') Up, } @@ -28,7 +38,7 @@ pub fn char_to_key_press(c: char) -> KeyPress { return KeyPress::Char(c); } match c { - '\x00' => KeyPress::Null, + '\x00' => KeyPress::Ctrl(' '), '\x01' => KeyPress::Ctrl('A'), '\x02' => KeyPress::Ctrl('B'), '\x03' => KeyPress::Ctrl('C'), @@ -37,12 +47,13 @@ pub fn char_to_key_press(c: char) -> KeyPress { '\x06' => KeyPress::Ctrl('F'), '\x07' => KeyPress::Ctrl('G'), '\x08' => KeyPress::Backspace, // '\b' - '\x09' => KeyPress::Tab, + '\x09' => KeyPress::Tab, // '\t' '\x0a' => KeyPress::Ctrl('J'), // '\n' (10) '\x0b' => KeyPress::Ctrl('K'), '\x0c' => KeyPress::Ctrl('L'), '\x0d' => KeyPress::Enter, // '\r' (13) '\x0e' => KeyPress::Ctrl('N'), + '\x0f' => KeyPress::Ctrl('O'), '\x10' => KeyPress::Ctrl('P'), '\x12' => KeyPress::Ctrl('R'), '\x13' => KeyPress::Ctrl('S'), @@ -50,10 +61,15 @@ pub fn char_to_key_press(c: char) -> KeyPress { '\x15' => KeyPress::Ctrl('U'), '\x16' => KeyPress::Ctrl('V'), '\x17' => KeyPress::Ctrl('W'), + '\x18' => KeyPress::Ctrl('X'), '\x19' => KeyPress::Ctrl('Y'), '\x1a' => KeyPress::Ctrl('Z'), - '\x1b' => KeyPress::Esc, - '\x7f' => KeyPress::Backspace, + '\x1b' => KeyPress::Esc, // Ctrl-[ + '\x1c' => KeyPress::Ctrl('\\'), + '\x1d' => KeyPress::Ctrl(']'), + '\x1e' => KeyPress::Ctrl('^'), + '\x1f' => KeyPress::Ctrl('_'), + '\x7f' => KeyPress::Backspace, // Rubout _ => KeyPress::Null, } } diff --git a/src/edit.rs b/src/edit.rs new file mode 100644 index 0000000000000000000000000000000000000000..f9fcca874a1e30b248bac37c8b6631431af9acd5 --- /dev/null +++ b/src/edit.rs @@ -0,0 +1,555 @@ +//! Command processor + +use std::cell::RefCell; +use std::fmt; +use std::rc::Rc; +use unicode_segmentation::UnicodeSegmentation; +use unicode_width::UnicodeWidthChar; + +use super::Result; +use highlight::Highlighter; +use hint::Hinter; +use history::{Direction, History}; +use keymap::{Anchor, At, CharSearch, Cmd, Movement, RepeatCount, Word}; +use keymap::{InputState, Refresher}; +use line_buffer::{LineBuffer, WordAction, MAX_LINE}; +use tty::{Position, RawReader, Renderer}; +use undo::Changeset; + +/// Represent the state during line editing. +/// Implement rendering. +pub struct State<'out, 'prompt> { + pub out: &'out mut Renderer, + prompt: &'prompt str, // Prompt to display (rl_prompt) + prompt_size: Position, // Prompt Unicode/visible width and height + pub line: LineBuffer, // Edited line buffer + pub cursor: Position, /* Cursor position (relative to the start of the prompt + * for `row`) */ + pub old_rows: usize, // Number of rows used so far (from start of prompt to end of input) + history_index: usize, // The history index we are currently editing + saved_line_for_history: LineBuffer, // Current edited line before history browsing + byte_buffer: [u8; 4], + pub changes: Rc<RefCell<Changeset>>, // changes to line, for undo/redo + pub hinter: Option<&'out Hinter>, + pub highlighter: Option<&'out Highlighter>, +} + +impl<'out, 'prompt> State<'out, 'prompt> { + pub fn new( + out: &'out mut Renderer, + prompt: &'prompt str, + history_index: usize, + hinter: Option<&'out Hinter>, + highlighter: Option<&'out Highlighter>, + ) -> State<'out, 'prompt> { + let capacity = MAX_LINE; + let prompt_size = out.calculate_position(prompt, Position::default()); + State { + out, + prompt, + prompt_size, + line: LineBuffer::with_capacity(capacity), + cursor: prompt_size, + old_rows: 0, + history_index, + saved_line_for_history: LineBuffer::with_capacity(capacity), + byte_buffer: [0; 4], + changes: Rc::new(RefCell::new(Changeset::new())), + hinter, + highlighter, + } + } + + pub fn next_cmd<R: RawReader>( + &mut self, + input_state: &mut InputState, + rdr: &mut R, + single_esc_abort: bool, + ) -> Result<Cmd> { + loop { + let rc = input_state.next_cmd(rdr, self, single_esc_abort); + if rc.is_err() && self.out.sigwinch() { + self.out.update_size(); + try!(self.refresh_line()); + continue; + } + if let Ok(Cmd::Replace(_, _)) = rc { + self.changes.borrow_mut().begin(); + } + return rc; + } + } + + pub fn backup(&mut self) { + self.saved_line_for_history + .update(self.line.as_str(), self.line.pos()); + } + + pub fn restore(&mut self) { + self.line.update( + self.saved_line_for_history.as_str(), + self.saved_line_for_history.pos(), + ); + } + + pub fn move_cursor(&mut self) -> Result<()> { + // calculate the desired position of the cursor + let cursor = self + .out + .calculate_position(&self.line[..self.line.pos()], self.prompt_size); + if self.cursor == cursor { + return Ok(()); + } + if self.highlighter.map_or(false, |h| { + self.line + .grapheme_at_cursor() + .map_or(false, |s| h.highlight_char(s)) + }) { + let prompt_size = self.prompt_size; + try!(self.refresh(self.prompt, prompt_size, None)); + } else { + try!(self.out.move_cursor(self.cursor, cursor)); + } + self.cursor = cursor; + Ok(()) + } + + fn refresh(&mut self, prompt: &str, prompt_size: Position, hint: Option<String>) -> Result<()> { + let (cursor, end_pos) = try!(self.out.refresh_line( + prompt, + prompt_size, + &self.line, + hint, + self.cursor.row, + self.old_rows, + self.highlighter, + )); + + self.cursor = cursor; + self.old_rows = end_pos.row; + Ok(()) + } + + fn hint(&self) -> Option<String> { + if let Some(hinter) = self.hinter { + hinter.hint(self.line.as_str(), self.line.pos()) + } else { + None + } + } +} + +impl<'out, 'prompt> Refresher for State<'out, 'prompt> { + fn refresh_line(&mut self) -> Result<()> { + let prompt_size = self.prompt_size; + let hint = self.hint(); + self.refresh(self.prompt, prompt_size, hint) + } + + fn refresh_prompt_and_line(&mut self, prompt: &str) -> Result<()> { + let prompt_size = self.out.calculate_position(prompt, Position::default()); + let hint = self.hint(); + self.refresh(prompt, prompt_size, hint) + } + + fn doing_insert(&mut self) { + self.changes.borrow_mut().begin(); + } + + fn done_inserting(&mut self) { + self.changes.borrow_mut().end(); + } + + fn last_insert(&self) -> Option<String> { + self.changes.borrow().last_insert() + } +} + +impl<'out, 'prompt> fmt::Debug for State<'out, 'prompt> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("State") + .field("prompt", &self.prompt) + .field("prompt_size", &self.prompt_size) + .field("buf", &self.line) + .field("cursor", &self.cursor) + .field("cols", &self.out.get_columns()) + .field("old_rows", &self.old_rows) + .field("history_index", &self.history_index) + .field("saved_line_for_history", &self.saved_line_for_history) + .finish() + } +} + +impl<'out, 'prompt> State<'out, 'prompt> { + /// Insert the character `ch` at cursor current position. + pub fn edit_insert(&mut self, ch: char, n: RepeatCount) -> Result<()> { + if let Some(push) = self.line.insert(ch, n) { + if push { + let prompt_size = self.prompt_size; + let hint = self.hint(); + if n == 1 + && self.cursor.col + ch.width().unwrap_or(0) < self.out.get_columns() + && hint.is_none() // TODO refresh only current line + && !self.highlighter.map_or(true, |h| h.highlight_char(ch.encode_utf8(&mut self.byte_buffer))) + { + // Avoid a full update of the line in the trivial case. + let cursor = self + .out + .calculate_position(&self.line[..self.line.pos()], self.prompt_size); + self.cursor = cursor; + let bits = ch.encode_utf8(&mut self.byte_buffer); + let bits = bits.as_bytes(); + self.out.write_and_flush(bits) + } else { + self.refresh(self.prompt, prompt_size, hint) + } + } else { + self.refresh_line() + } + } else { + Ok(()) + } + } + + /// Replace a single (or n) character(s) under the cursor (Vi mode) + pub fn edit_replace_char(&mut self, ch: char, n: RepeatCount) -> Result<()> { + self.changes.borrow_mut().begin(); + let succeed = if let Some(chars) = self.line.delete(n) { + let count = chars.graphemes(true).count(); + self.line.insert(ch, count); + self.line.move_backward(1); + true + } else { + false + }; + self.changes.borrow_mut().end(); + if succeed { + self.refresh_line() + } else { + Ok(()) + } + } + + /// Overwrite the character under the cursor (Vi mode) + pub fn edit_overwrite_char(&mut self, ch: char) -> Result<()> { + if let Some(end) = self.line.next_pos(1) { + { + let text = ch.encode_utf8(&mut self.byte_buffer); + let start = self.line.pos(); + self.line.replace(start..end, text); + } + self.refresh_line() + } else { + Ok(()) + } + } + + // Yank/paste `text` at current position. + pub fn edit_yank( + &mut self, + input_state: &InputState, + text: &str, + anchor: Anchor, + n: RepeatCount, + ) -> Result<()> { + if let Anchor::After = anchor { + self.line.move_forward(1); + } + if self.line.yank(text, n).is_some() { + if !input_state.is_emacs_mode() { + self.line.move_backward(1); + } + self.refresh_line() + } else { + Ok(()) + } + } + + // Delete previously yanked text and yank/paste `text` at current position. + pub fn edit_yank_pop(&mut self, yank_size: usize, text: &str) -> Result<()> { + self.changes.borrow_mut().begin(); + let result = if self.line.yank_pop(yank_size, text).is_some() { + self.refresh_line() + } else { + Ok(()) + }; + self.changes.borrow_mut().end(); + result + } + + /// Move cursor on the left. + pub fn edit_move_backward(&mut self, n: RepeatCount) -> Result<()> { + if self.line.move_backward(n) { + self.move_cursor() + } else { + Ok(()) + } + } + + /// Move cursor on the right. + pub fn edit_move_forward(&mut self, n: RepeatCount) -> Result<()> { + if self.line.move_forward(n) { + self.move_cursor() + } else { + Ok(()) + } + } + + /// Move cursor to the start of the line. + pub fn edit_move_home(&mut self) -> Result<()> { + if self.line.move_home() { + self.move_cursor() + } else { + Ok(()) + } + } + + /// Move cursor to the end of the line. + pub fn edit_move_end(&mut self) -> Result<()> { + if self.line.move_end() { + self.move_cursor() + } else { + Ok(()) + } + } + + pub fn edit_kill(&mut self, mvt: &Movement) -> Result<()> { + if self.line.kill(mvt) { + self.refresh_line() + } else { + Ok(()) + } + } + + pub fn edit_insert_text(&mut self, text: &str) -> Result<()> { + if !text.is_empty() { + let cursor = self.line.pos(); + self.line.insert_str(cursor, text); + self.refresh_line() + } else { + Ok(()) + } + } + + pub fn edit_delete(&mut self, n: RepeatCount) -> Result<()> { + if self.line.delete(n).is_some() { + self.refresh_line() + } else { + Ok(()) + } + } + + /// Exchange the char before cursor with the character at cursor. + pub fn edit_transpose_chars(&mut self) -> Result<()> { + self.changes.borrow_mut().begin(); + let succeed = self.line.transpose_chars(); + self.changes.borrow_mut().end(); + if succeed { + self.refresh_line() + } else { + Ok(()) + } + } + + pub fn edit_move_to_prev_word(&mut self, word_def: Word, n: RepeatCount) -> Result<()> { + if self.line.move_to_prev_word(word_def, n) { + self.move_cursor() + } else { + Ok(()) + } + } + + pub fn edit_move_to_next_word(&mut self, at: At, word_def: Word, n: RepeatCount) -> Result<()> { + if self.line.move_to_next_word(at, word_def, n) { + self.move_cursor() + } else { + Ok(()) + } + } + + pub fn edit_move_to(&mut self, cs: CharSearch, n: RepeatCount) -> Result<()> { + if self.line.move_to(cs, n) { + self.move_cursor() + } else { + Ok(()) + } + } + + pub fn edit_word(&mut self, a: WordAction) -> Result<()> { + self.changes.borrow_mut().begin(); + let succeed = self.line.edit_word(a); + self.changes.borrow_mut().end(); + if succeed { + self.refresh_line() + } else { + Ok(()) + } + } + + pub fn edit_transpose_words(&mut self, n: RepeatCount) -> Result<()> { + self.changes.borrow_mut().begin(); + let succeed = self.line.transpose_words(n); + self.changes.borrow_mut().end(); + if succeed { + self.refresh_line() + } else { + Ok(()) + } + } + + /// Substitute the currently edited line with the next or previous history + /// entry. + pub fn edit_history_next(&mut self, history: &History, prev: bool) -> Result<()> { + if history.is_empty() { + return Ok(()); + } + if self.history_index == history.len() { + if prev { + // Save the current edited line before overwriting it + self.backup(); + } else { + return Ok(()); + } + } else if self.history_index == 0 && prev { + return Ok(()); + } + if prev { + self.history_index -= 1; + } else { + self.history_index += 1; + } + if self.history_index < history.len() { + let buf = history.get(self.history_index).unwrap(); + self.changes.borrow_mut().begin(); + self.line.update(buf, buf.len()); + self.changes.borrow_mut().end(); + } else { + // Restore current edited line + self.restore(); + } + self.refresh_line() + } + + // Non-incremental, anchored search + pub fn edit_history_search(&mut self, history: &History, dir: Direction) -> Result<()> { + if history.is_empty() { + return self.out.beep(); + } + if self.history_index == history.len() && dir == Direction::Forward + || self.history_index == 0 && dir == Direction::Reverse + { + return self.out.beep(); + } + if dir == Direction::Reverse { + self.history_index -= 1; + } else { + self.history_index += 1; + } + if let Some(history_index) = history.starts_with( + &self.line.as_str()[..self.line.pos()], + self.history_index, + dir, + ) { + self.history_index = history_index; + let buf = history.get(history_index).unwrap(); + self.changes.borrow_mut().begin(); + self.line.update(buf, buf.len()); + self.changes.borrow_mut().end(); + self.refresh_line() + } else { + self.out.beep() + } + } + + /// Substitute the currently edited line with the first/last history entry. + pub fn edit_history(&mut self, history: &History, first: bool) -> Result<()> { + if history.is_empty() { + return Ok(()); + } + if self.history_index == history.len() { + if first { + // Save the current edited line before overwriting it + self.backup(); + } else { + return Ok(()); + } + } else if self.history_index == 0 && first { + return Ok(()); + } + if first { + self.history_index = 0; + let buf = history.get(self.history_index).unwrap(); + self.changes.borrow_mut().begin(); + self.line.update(buf, buf.len()); + self.changes.borrow_mut().end(); + } else { + self.history_index = history.len(); + // Restore current edited line + self.restore(); + } + self.refresh_line() + } +} + +#[cfg(test)] +pub fn init_state<'out>(out: &'out mut Renderer, line: &str, pos: usize) -> State<'out, 'static> { + State { + out, + prompt: "", + prompt_size: Position::default(), + line: LineBuffer::init(line, pos, None), + cursor: Position::default(), + old_rows: 0, + history_index: 0, + saved_line_for_history: LineBuffer::with_capacity(100), + byte_buffer: [0; 4], + changes: Rc::new(RefCell::new(Changeset::new())), + hinter: None, + highlighter: None, + } +} + +#[cfg(test)] +mod test { + use super::init_state; + use history::History; + use tty::Sink; + + #[test] + fn edit_history_next() { + let mut out = Sink::new(); + let line = "current edited line"; + let mut s = init_state(&mut out, line, 6); + let mut history = History::new(); + history.add("line0"); + history.add("line1"); + s.history_index = history.len(); + + for _ in 0..2 { + s.edit_history_next(&history, false).unwrap(); + assert_eq!(line, s.line.as_str()); + } + + s.edit_history_next(&history, true).unwrap(); + assert_eq!(line, s.saved_line_for_history.as_str()); + assert_eq!(1, s.history_index); + assert_eq!("line1", s.line.as_str()); + + for _ in 0..2 { + s.edit_history_next(&history, true).unwrap(); + assert_eq!(line, s.saved_line_for_history.as_str()); + assert_eq!(0, s.history_index); + assert_eq!("line0", s.line.as_str()); + } + + s.edit_history_next(&history, false).unwrap(); + assert_eq!(line, s.saved_line_for_history.as_str()); + assert_eq!(1, s.history_index); + assert_eq!("line1", s.line.as_str()); + + s.edit_history_next(&history, false).unwrap(); + // assert_eq!(line, s.saved_line_for_history); + assert_eq!(2, s.history_index); + assert_eq!(line, s.line.as_str()); + } +} diff --git a/src/error.rs b/src/error.rs index d778960f17306f3625eb79ecdf4fc72bfe953070..0ac688feb58e76097748cbf8c88f3a04654fb65a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,14 +1,12 @@ //! Contains error type for handling I/O and Errno errors +#[cfg(all(unix, not(any(target_os = "fuchsia"))))] +use nix; #[cfg(windows)] use std::char; -use std::io; use std::error; use std::fmt; -#[cfg(all(unix, not(any(target_os = "fuchsia"))))] -use nix; - -#[cfg(unix)] -use char_iter; +use std::io; +use std::str; /// The error type for Rustyline errors that can arise from /// I/O related errors or Errno when using the nix-rust library @@ -22,7 +20,7 @@ pub enum ReadlineError { Interrupted, /// Chars Error #[cfg(unix)] - Char(char_iter::CharsError), + Utf8Error, /// Unix Error from syscall #[cfg(all(unix, not(any(target_os = "fuchsia"))))] Errno(nix::Error), @@ -39,9 +37,9 @@ impl fmt::Display for ReadlineError { ReadlineError::Eof => write!(f, "EOF"), ReadlineError::Interrupted => write!(f, "Interrupted"), #[cfg(unix)] - ReadlineError::Char(ref err) => err.fmt(f), + ReadlineError::Utf8Error => write!(f, "invalid utf-8: corrupt contents"), #[cfg(all(unix, not(any(target_os = "fuchsia"))))] - ReadlineError::Errno(ref err) => write!(f, "Errno: {}", err.errno().desc()), + ReadlineError::Errno(ref err) => err.fmt(f), #[cfg(windows)] ReadlineError::WindowResize => write!(f, "WindowResize"), #[cfg(windows)] @@ -57,9 +55,9 @@ impl error::Error for ReadlineError { ReadlineError::Eof => "EOF", ReadlineError::Interrupted => "Interrupted", #[cfg(unix)] - ReadlineError::Char(ref err) => err.description(), + ReadlineError::Utf8Error => "invalid utf-8: corrupt contents", #[cfg(all(unix, not(any(target_os = "fuchsia"))))] - ReadlineError::Errno(ref err) => err.errno().desc(), + ReadlineError::Errno(ref err) => err.description(), #[cfg(windows)] ReadlineError::WindowResize => "WindowResize", #[cfg(windows)] @@ -81,13 +79,6 @@ impl From<nix::Error> for ReadlineError { } } -#[cfg(unix)] -impl From<char_iter::CharsError> for ReadlineError { - fn from(err: char_iter::CharsError) -> ReadlineError { - ReadlineError::Char(err) - } -} - #[cfg(windows)] impl From<char::DecodeUtf16Error> for ReadlineError { fn from(err: char::DecodeUtf16Error) -> ReadlineError { diff --git a/src/highlight.rs b/src/highlight.rs new file mode 100644 index 0000000000000000000000000000000000000000..04bc042c84c09e0c00035beb743fa166a0ebffa0 --- /dev/null +++ b/src/highlight.rs @@ -0,0 +1,59 @@ +use config::CompletionType; +///! Syntax highlighting +use std::borrow::Cow::{self, Borrowed}; + +/// Syntax highlighter with [ansi color](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters). +/// Rustyline will try to handle escape sequence for ansi color on windows +/// when not supported natively (windows <10). +/// +/// Currently, the highlighted version *must* have the same display width as +/// the original input. +pub trait Highlighter { + /// Takes the currently edited `line` with the cursor `pos`ition and + /// returns the highlighted version (with ANSI color). + /// + /// For example, you can implement + /// [blink-matching-paren](https://www.gnu.org/software/bash/manual/html_node/Readline-Init-File-Syntax.html). + fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> { + let _ = pos; + Borrowed(line) + } + /// Takes the `prompt` and + /// returns the highlighted version (with ANSI color). + fn highlight_prompt<'p>(&self, prompt: &'p str) -> Cow<'p, str> { + Borrowed(prompt) + } + /// Takes the dynamic `prompt` and + /// returns the highlighted version (with ANSI color). + fn highlight_dynamic_prompt<'p>(&self, prompt: &'p str) -> Cow<'p, str> { + Borrowed(prompt) + } + /// Takes the `hint` and + /// returns the highlighted version (with ANSI color). + fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { + Borrowed(hint) + } + /// Takes the completion `canditate` and + /// returns the highlighted version (with ANSI color). + /// + /// Currently, used only with `CompletionType::List`. + fn highlight_candidate<'c>( + &self, + candidate: &'c str, + completion: CompletionType, + ) -> Cow<'c, str> { + let _ = completion; + Borrowed(candidate) + } + /// Tells if the `ch`ar needs to be highlighted when typed or when cursor + /// is moved under. + /// + /// Used to optimize refresh when a character is inserted or the cursor is + /// moved. + fn highlight_char(&self, grapheme: &str) -> bool { + let _ = grapheme; + false + } +} + +impl Highlighter for () {} diff --git a/src/hint.rs b/src/hint.rs new file mode 100644 index 0000000000000000000000000000000000000000..c15db9c82d7bdb3195849852de9c36bdef6e3845 --- /dev/null +++ b/src/hint.rs @@ -0,0 +1,15 @@ +//! Hints (suggestions at the right of the prompt as you type). + +/// Hints provider +pub trait Hinter { + /// Takes the currently edited `line` with the cursor `pos`ition and + /// returns the string that should be displayed or `None` + /// if no hint is available for the text the user currently typed. + fn hint(&self, line: &str, pos: usize) -> Option<String>; +} + +impl Hinter for () { + fn hint(&self, _line: &str, _pos: usize) -> Option<String> { + None + } +} diff --git a/src/history.rs b/src/history.rs index e283cb4ab94629fc1717711b2ab541a03079866e..76a5bcf4498179bdadb4dabd5ebbe65b3f844234 100644 --- a/src/history.rs +++ b/src/history.rs @@ -1,17 +1,18 @@ //! History API -use std::collections::VecDeque; +#[cfg(unix)] +use libc; use std::collections::vec_deque; +use std::collections::VecDeque; use std::fs::File; use std::iter::DoubleEndedIterator; use std::ops::Index; use std::path::Path; -#[cfg(unix)] -use libc; use super::Result; use config::{Config, HistoryDuplicates}; +/// Search direction #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Direction { Forward, @@ -31,12 +32,13 @@ impl History { pub fn new() -> History { Self::with_config(Config::default()) } + pub fn with_config(config: Config) -> History { History { entries: VecDeque::new(), max_len: config.max_history_size(), - ignore_space: config.history_duplicates() == HistoryDuplicates::IgnoreConsecutive, - ignore_dups: config.history_ignore_space(), + ignore_space: config.history_ignore_space(), + ignore_dups: config.history_duplicates() == HistoryDuplicates::IgnoreConsecutive, } } @@ -55,12 +57,13 @@ impl History { if self.max_len == 0 { return false; } - if line.as_ref().is_empty() || - (self.ignore_space && - line.as_ref() + if line.as_ref().is_empty() + || (self.ignore_space && line + .as_ref() .chars() .next() - .map_or(true, |c| c.is_whitespace())) { + .map_or(true, |c| c.is_whitespace())) + { return false; } if self.ignore_dups { @@ -77,19 +80,23 @@ impl History { true } - /// Returns the number of entries in the history. + /// Return the number of entries in the history. pub fn len(&self) -> usize { self.entries.len() } - /// Returns true if the history has no entry. + + /// Return true if the history has no entry. pub fn is_empty(&self) -> bool { self.entries.is_empty() } /// Set the maximum length for the history. This function can be called even /// if there is already some history, the function will make sure to retain - /// just the latest `len` elements if the new history length value is smaller - /// than the amount of items already inside the history. + /// just the latest `len` elements if the new history length value is + /// smaller than the amount of items already inside the history. + /// + /// Like [stifle_history](http://cnswww.cns.cwru. + /// edu/php/chet/readline/history.html#IDX11). pub fn set_max_len(&mut self, len: usize) { self.max_len = len; if len == 0 { @@ -105,6 +112,10 @@ impl History { } /// Save the history in the specified file. + // TODO append_history + // http://cnswww.cns.cwru.edu/php/chet/readline/history.html#IDX30 + // TODO history_truncate_file + // http://cnswww.cns.cwru.edu/php/chet/readline/history.html#IDX31 pub fn save<P: AsRef<Path> + ?Sized>(&self, path: &P) -> Result<()> { use std::io::{BufWriter, Write}; @@ -121,13 +132,15 @@ impl History { try!(wtr.write_all(entry.as_bytes())); try!(wtr.write_all(b"\n")); } + // https://github.com/rust-lang/rust/issues/32677#issuecomment-204833485 + try!(wtr.flush()); Ok(()) } /// Load the history from the specified file. /// - /// # Failure - /// Will return `Err` if path does not already exist. + /// # Errors + /// Will return `Err` if path does not already exist or could not be read. pub fn load<P: AsRef<Path> + ?Sized>(&mut self, path: &P) -> Result<()> { use std::io::{BufRead, BufReader}; @@ -144,29 +157,36 @@ impl History { self.entries.clear() } - /// Search history (start position inclusive [0, len-1]) - /// Return the absolute index of the nearest history entry that matches `term`. - /// Return None if no entry contains `term` between [start, len -1] for forward search + /// Search history (start position inclusive [0, len-1]). + /// + /// Return the absolute index of the nearest history entry that matches + /// `term`. + /// + /// Return None if no entry contains `term` between [start, len -1] for + /// forward search /// or between [0, start] for reverse search. pub fn search(&self, term: &str, start: usize, dir: Direction) -> Option<usize> { let test = |entry: &String| entry.contains(term); self.search_match(term, start, dir, test) } + /// Anchored search pub fn starts_with(&self, term: &str, start: usize, dir: Direction) -> Option<usize> { let test = |entry: &String| entry.starts_with(term); self.search_match(term, start, dir, test) } fn search_match<F>(&self, term: &str, start: usize, dir: Direction, test: F) -> Option<usize> - where F: Fn(&String) -> bool + where + F: Fn(&String) -> bool, { if term.is_empty() || start >= self.len() { return None; } match dir { Direction::Reverse => { - let index = self.entries + let index = self + .entries .iter() .rev() .skip(self.entries.len() - 1 - start) @@ -195,8 +215,8 @@ impl Index<usize> for History { } impl<'a> IntoIterator for &'a History { - type Item = &'a String; type IntoIter = Iter<'a>; + type Item = &'a String; fn into_iter(self) -> Iter<'a> { self.iter() @@ -254,9 +274,9 @@ fn fix_perm(file: &File) { #[cfg(test)] mod tests { extern crate tempdir; - use std::path::Path; use super::{Direction, History}; use config::Config; + use std::path::Path; fn init() -> History { let mut history = History::new(); @@ -289,7 +309,7 @@ mod tests { let mut history = init(); history.set_max_len(1); assert_eq!(1, history.entries.len()); - assert_eq!(Some(&"line3".to_string()), history.last()); + assert_eq!(Some(&"line3".to_owned()), history.last()); } #[test] diff --git a/src/keymap.rs b/src/keymap.rs index 71410660de4d0e4c4488141bba40e36b24fda75a..877a12dd6a85887886d39e5ea7609f1c3652e1fa 100644 --- a/src/keymap.rs +++ b/src/keymap.rs @@ -1,74 +1,117 @@ //! Bindings from keys to command for Emacs and Vi modes -use std::cell::RefCell; use std::collections::HashMap; -use std::rc::Rc; +use std::sync::{Arc, RwLock}; +use super::Result; use config::Config; use config::EditMode; use consts::KeyPress; use tty::RawReader; -use super::Result; +/// 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, - Replace(RepeatCount, char), + /// 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::Noop | Cmd::Suspend | Cmd::Yank(_, _) | - Cmd::YankPop => false, + 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(_, _) => true, - Cmd::Kill(_) => true, - Cmd::Replace(_, _) => true, - Cmd::SelfInsert(_, _) => true, + Cmd::Insert(_, _) + | Cmd::Kill(_) + | Cmd::ReplaceChar(_, _) + | Cmd::Replace(_, _) + | Cmd::SelfInsert(_, _) + | Cmd::ViYankTo(_) + | Cmd::Yank(_, _) => true, Cmd::TransposeChars => false, // TODO Validate - Cmd::ViYankTo(_) => true, - Cmd::Yank(_, _) => true, _ => false, } } + fn is_repeatable(&self) -> bool { match *self { Cmd::Move(_) => true, @@ -76,16 +119,41 @@ impl Cmd { } } - fn redo(&self, new: Option<RepeatCount>) -> Cmd { + // 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::Replace(previous, c) => Cmd::Replace(repeat_count(previous, new), c), - Cmd::SelfInsert(previous, c) => Cmd::SelfInsert(repeat_count(previous, new), c), - //Cmd::TransposeChars => Cmd::TransposeChars, + 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!(), @@ -100,16 +168,18 @@ fn repeat_count(previous: RepeatCount, new: Option<RepeatCount>) -> RepeatCount } } +/// Different word definitions #[derive(Debug, Clone, PartialEq, Copy)] pub enum Word { - // non-blanks characters + /// non-blanks characters Big, - // alphanumeric characters + /// alphanumeric characters Emacs, - // alphanumeric (and '_') characters + /// alphanumeric (and '_') characters Vi, } +/// Where to move with respect to word boundary #[derive(Debug, Clone, PartialEq, Copy)] pub enum At { Start, @@ -117,13 +187,15 @@ pub enum At { AfterEnd, } +/// Where to paste (relative to cursor position) #[derive(Debug, Clone, PartialEq, Copy)] pub enum Anchor { After, Before, } -#[derive(Debug, Clone, PartialEq)] +/// Vi character search +#[derive(Debug, Clone, PartialEq, Copy)] pub enum CharSearch { Forward(char), // until @@ -134,8 +206,8 @@ pub enum CharSearch { } impl CharSearch { - fn opposite(&self) -> CharSearch { - match *self { + fn opposite(self) -> CharSearch { + match self { CharSearch::Forward(c) => CharSearch::Backward(c), CharSearch::ForwardBefore(c) => CharSearch::BackwardAfter(c), CharSearch::Backward(c) => CharSearch::Forward(c), @@ -144,21 +216,30 @@ impl CharSearch { } } - +/// 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, @@ -171,8 +252,8 @@ impl Movement { Movement::ForwardWord(previous, at, word) => { Movement::ForwardWord(repeat_count(previous, new), at, word) } - Movement::ViCharSearch(previous, ref char_search) => { - Movement::ViCharSearch(repeat_count(previous, new), char_search.clone()) + 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)), @@ -180,27 +261,52 @@ impl Movement { } } -pub struct EditState { +#[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: Rc<RefCell<HashMap<KeyPress, Cmd>>>, - // Vi Command/Alternate, Insert/Input mode - insert: bool, // vi only ? + 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 - consecutive_insert: bool, + last_cmd: Cmd, // vi only last_char_search: Option<CharSearch>, // vi only } -impl EditState { - pub fn new(config: &Config, custom_bindings: Rc<RefCell<HashMap<KeyPress, Cmd>>>) -> EditState { - EditState { +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: custom_bindings, - insert: true, + custom_bindings, + input_mode: InputMode::Insert, num_args: 0, last_cmd: Cmd::Noop, - consecutive_insert: false, last_char_search: None, } } @@ -209,16 +315,29 @@ impl EditState { self.mode == EditMode::Emacs } - pub fn next_cmd<R: RawReader>(&mut self, rdr: &mut R) -> Result<Cmd> { + /// 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), - EditMode::Vi if self.insert => self.vi_insert(rdr), - EditMode::Vi => self.vi_command(rdr), + 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, digit: char) -> Result<KeyPress> { + 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; @@ -229,198 +348,217 @@ impl EditState { _ => unreachable!(), } loop { - let key = try!(rdr.next_key()); + 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') => { + 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 { - self.num_args = self.num_args + } 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); } } - _ => return Ok(key), + KeyPress::Char('-') | KeyPress::Meta('-') => {} + _ => { + try!(wrt.refresh_line()); + return Ok(key); + } }; } } - fn emacs<R: RawReader>(&mut self, rdr: &mut R) -> Result<Cmd> { - let mut key = try!(rdr.next_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, digit)); + 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, digit)); + key = try!(self.emacs_digit_argument(rdr, wrt, digit)); } let (n, positive) = self.emacs_num_args(); // consume them in all cases - if let Some(cmd) = self.custom_bindings.borrow().get(&key) { - debug!(target: "rustyline", "Custom command: {:?}", cmd); - return Ok(if cmd.is_repeatable() { - cmd.redo(Some(n)) - } else { - cmd.clone() - }); - } - let cmd = match key { - KeyPress::Char(c) => { - if positive { - Cmd::SelfInsert(n, c) + { + 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::Unknown - } + 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('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 => Cmd::Abort, - KeyPress::Ctrl('H') | - KeyPress::Backspace => { - if positive { - Cmd::Kill(Movement::BackwardChar(n)) - } else { - Cmd::Kill(Movement::ForwardChar(n)) - } - } + 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('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::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::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') => { - if positive { - Cmd::Move(Movement::BackwardWord(n, Word::Emacs)) - } else { - Cmd::Move(Movement::ForwardWord(n, At::AfterEnd, Word::Emacs)) - } - } - KeyPress::Meta('C') => Cmd::CapitalizeWord, - 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') => { - if positive { - Cmd::Move(Movement::ForwardWord(n, At::AfterEnd, Word::Emacs)) - } else { - Cmd::Move(Movement::BackwardWord(n, Word::Emacs)) - } - } - KeyPress::Meta('L') => Cmd::DowncaseWord, - KeyPress::Meta('T') => Cmd::TransposeWords(n), - KeyPress::Meta('U') => Cmd::UpcaseWord, - KeyPress::Meta('Y') => Cmd::YankPop, + 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, digit: char) -> Result<KeyPress> { + 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 { - let key = try!(rdr.next_key()); + 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') => { - self.num_args = self.num_args - .saturating_mul(10) - .saturating_add(digit.to_digit(10).unwrap() as i16); + 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); } - _ => return Ok(key), }; } } - fn vi_command<R: RawReader>(&mut self, rdr: &mut R) -> Result<Cmd> { - let mut key = try!(rdr.next_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, digit)); + 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 - if let Some(cmd) = self.custom_bindings.borrow().get(&key) { - debug!(target: "rustyline", "Custom command: {:?}", cmd); - return Ok(if cmd.is_repeatable() { - if no_num_args { - cmd.redo(None) - } else { - cmd.redo(Some(n)) - } - } else { - cmd.clone() - }); + { + 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 + KeyPress::Char('.') => { // vi-redo (repeat last command) if no_num_args { - self.last_cmd.redo(None) + self.last_cmd.redo(None, wrt) } else { - self.last_cmd.redo(Some(n)) + 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: Vi enter insert mode after the cursor. - self.insert = true; + // vi-append-mode + self.input_mode = InputMode::Insert; + wrt.doing_insert(); Cmd::Move(Movement::ForwardChar(n)) } KeyPress::Char('A') => { - // vi-append-eol: Vi enter insert mode at end of line. - self.insert = true; + // 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.insert = true; - match try!(self.vi_cmd_motion(rdr, key, n)) { - Some(mvt) => Cmd::Kill(mvt), + 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.insert = true; - Cmd::Kill(Movement::EndOfLine) + self.input_mode = InputMode::Insert; + Cmd::Replace(Movement::EndOfLine, None) } KeyPress::Char('d') => { - match try!(self.vi_cmd_motion(rdr, key, n)) { + match try!(self.vi_cmd_motion(rdr, wrt, key, n)) { Some(mvt) => Cmd::Kill(mvt), None => Cmd::Unknown, } @@ -431,12 +569,14 @@ impl EditState { KeyPress::Char('E') => Cmd::Move(Movement::ForwardWord(n, At::BeforeEnd, Word::Big)), KeyPress::Char('i') => { // vi-insertion-mode - self.insert = true; + self.input_mode = InputMode::Insert; + wrt.doing_insert(); Cmd::Noop } KeyPress::Char('I') => { // vi-insert-beg - self.insert = true; + self.input_mode = InputMode::Insert; + wrt.doing_insert(); Cmd::Move(Movement::BeginningOfLine) } KeyPress::Char(c) if c == 'f' || c == 'F' || c == 't' || c == 'T' => { @@ -449,7 +589,7 @@ impl EditState { } KeyPress::Char(';') => { match self.last_char_search { - Some(ref cs) => Cmd::Move(Movement::ViCharSearch(n, cs.clone())), + Some(cs) => Cmd::Move(Movement::ViCharSearch(n, cs)), None => Cmd::Noop, } } @@ -463,32 +603,37 @@ impl EditState { 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: Vi replace character under the cursor with the next character typed. - let ch = try!(rdr.next_key()); + // vi-replace-char: + let ch = try!(rdr.next_key(false)); match ch { - KeyPress::Char(c) => Cmd::Replace(n, c), + KeyPress::Char(c) => Cmd::ReplaceChar(n, c), 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('R') => { + // vi-replace-mode (overwrite-mode) + self.input_mode = InputMode::Replace; + Cmd::Replace(Movement::ForwardChar(0), None) + } KeyPress::Char('s') => { - // vi-substitute-char: Vi replace character under the cursor and enter insert mode. - self.insert = true; - Cmd::Kill(Movement::ForwardChar(n)) + // vi-substitute-char: + self.input_mode = InputMode::Insert; + Cmd::Replace(Movement::ForwardChar(n), None) } KeyPress::Char('S') => { - // vi-substitute-line: Vi substitute entire line. - self.insert = true; - Cmd::Kill(Movement::WholeLine) + // 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, key, n)) { + match try!(self.vi_cmd_motion(rdr, wrt, key, n)) { Some(mvt) => Cmd::ViYankTo(mvt), None => Cmd::Unknown, } @@ -508,11 +653,11 @@ impl EditState { KeyPress::Char('k') | // TODO: move to the start of the line. KeyPress::Ctrl('P') => Cmd::PreviousHistory, KeyPress::Ctrl('R') => { - self.insert = true; // TODO Validate + self.input_mode = InputMode::Insert; // TODO Validate Cmd::ReverseSearchHistory } KeyPress::Ctrl('S') => { - self.insert = true; // TODO Validate + self.input_mode = InputMode::Insert; // TODO Validate Cmd::ForwardSearchHistory } KeyPress::Esc => Cmd::Noop, @@ -520,130 +665,137 @@ impl EditState { }; debug!(target: "rustyline", "Vi command: {:?}", cmd); if cmd.is_repeatable_change() { - self.update_last_cmd(cmd.clone()); + self.last_cmd = cmd.clone(); } Ok(cmd) } - fn vi_insert<R: RawReader>(&mut self, rdr: &mut R) -> Result<Cmd> { - let key = try!(rdr.next_key()); - if let Some(cmd) = self.custom_bindings.borrow().get(&key) { - debug!(target: "rustyline", "Custom command: {:?}", cmd); - return Ok(if cmd.is_repeatable() { - cmd.redo(None) - } else { - cmd.clone() - }); + 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) => Cmd::SelfInsert(1, c), - KeyPress::Ctrl('H') | - KeyPress::Backspace => Cmd::Kill(Movement::BackwardChar(1)), + 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: Vi enter command mode (use alternative key bindings). - self.insert = false; + // 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() { - self.update_last_cmd(cmd.clone()); + 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(); + } } - self.consecutive_insert = match cmd { - Cmd::SelfInsert(_, _) => true, - _ => false, - }; Ok(cmd) } - fn vi_cmd_motion<R: RawReader>(&mut self, - rdr: &mut R, - key: KeyPress, - n: RepeatCount) - -> Result<Option<Movement>> { - let mut mvt = try!(rdr.next_key()); + 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, 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), // vi-change-to-eol: Vi change to end of line. - KeyPress::Char('0') => Some(Movement::BeginningOfLine), // vi-kill-line-prev: Vi cut from beginning of line to cursor. - 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 { + 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(';') => { - match self.last_char_search { - Some(ref cs) => Some(Movement::ViCharSearch(n, cs.clone())), - 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)), // vi-delete-prev-char: Vi move to previous character (backspace). - 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)) + 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, - }) + _ => None, + }) } - fn vi_char_search<R: RawReader>(&mut self, - rdr: &mut R, - cmd: char) - -> Result<Option<CharSearch>> { - let ch = try!(rdr.next_key()); + 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.clone()); - Some(cs) - } - _ => None, - }) + 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 { @@ -682,9 +834,9 @@ impl EditState { 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 @@ -704,6 +856,7 @@ impl EditState { } } KeyPress::Ctrl('Z') => Cmd::Suspend, + KeyPress::Ctrl('_') => Cmd::Undo(n), KeyPress::UnknownEscSeq => Cmd::Noop, _ => Cmd::Unknown, } @@ -739,26 +892,4 @@ impl EditState { num_args.abs() as RepeatCount } } - - fn update_last_cmd(&mut self, new: Cmd) { - // consecutive char inserts are repeatable not only the last one... - if !self.consecutive_insert { - self.last_cmd = new; - } else if let Cmd::SelfInsert(_, c) = new { - match self.last_cmd { - Cmd::SelfInsert(_, pc) => { - let mut text = String::new(); - text.push(pc); - text.push(c); - self.last_cmd = Cmd::Insert(1, text); - } - Cmd::Insert(_, ref mut text) => { - text.push(c); - } - _ => self.last_cmd = new, - } - } else { - self.last_cmd = new; - } - } } diff --git a/src/kill_ring.rs b/src/kill_ring.rs index 635cb4f566668d3852ecab3d62c6a07444119aca..b5d87780a496c8d48e6b6f8a4763014917404896 100644 --- a/src/kill_ring.rs +++ b/src/kill_ring.rs @@ -1,4 +1,5 @@ -//! Kill Ring +//! Kill Ring management +use line_buffer::{DeleteListener, Direction}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum Action { @@ -19,6 +20,7 @@ pub struct KillRing { index: usize, // whether or not the last command was a kill or a yank last_action: Action, + killing: bool, } impl KillRing { @@ -28,6 +30,7 @@ impl KillRing { slots: Vec::with_capacity(size), index: 0, last_action: Action::Other, + killing: false, } } @@ -102,9 +105,30 @@ impl KillRing { } } +impl DeleteListener for KillRing { + fn start_killing(&mut self) { + self.killing = true; + } + + fn delete(&mut self, _: usize, string: &str, dir: Direction) { + if !self.killing { + return; + } + let mode = match dir { + Direction::Forward => Mode::Append, + Direction::Backward => Mode::Prepend, + }; + self.kill(string, mode); + } + + fn stop_killing(&mut self) { + self.killing = false; + } +} + #[cfg(test)] mod tests { - use super::{Action, Mode, KillRing}; + use super::{Action, KillRing, Mode}; #[test] fn disabled() { @@ -187,9 +211,9 @@ mod tests { kill_ring.reset(); kill_ring.kill("word2", Mode::Append); - assert_eq!(Some(&"word2".to_string()), kill_ring.yank()); + assert_eq!(Some(&"word2".to_owned()), kill_ring.yank()); assert_eq!(Action::Yank(5), kill_ring.last_action); - assert_eq!(Some(&"word2".to_string()), kill_ring.yank()); + assert_eq!(Some(&"word2".to_owned()), kill_ring.yank()); assert_eq!(Action::Yank(5), kill_ring.last_action); } @@ -202,8 +226,8 @@ mod tests { assert_eq!(None, kill_ring.yank_pop()); kill_ring.yank(); - assert_eq!(Some((9, &"word1".to_string())), kill_ring.yank_pop()); - assert_eq!(Some((5, &"longword2".to_string())), kill_ring.yank_pop()); - assert_eq!(Some((9, &"word1".to_string())), kill_ring.yank_pop()); + assert_eq!(Some((9, &"word1".to_owned())), kill_ring.yank_pop()); + assert_eq!(Some((5, &"longword2".to_owned())), kill_ring.yank_pop()); + assert_eq!(Some((9, &"word1".to_owned())), kill_ring.yank_pop()); } } diff --git a/src/lib.rs b/src/lib.rs index 133b569e6ac96cec6ffd22a1f3304d18536e415e..af5e791321e481bd243d35ad747c7d0263ae847e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ //! Readline for Rust //! -//! This implementation is based on [Antirez's Linenoise](https://github.com/antirez/linenoise) +//! This implementation is based on [Antirez's +//! Linenoise](https://github.com/antirez/linenoise) //! //! # Example //! @@ -10,712 +11,180 @@ //! let mut rl = rustyline::Editor::<()>::new(); //! let readline = rl.readline(">> "); //! match readline { -//! Ok(line) => println!("Line: {:?}",line), -//! Err(_) => println!("No input"), +//! Ok(line) => println!("Line: {:?}", line), +//! Err(_) => println!("No input"), //! } //! ``` #![allow(unknown_lints)] +extern crate dirs; +#[cfg(target_os = "fuchsia")] +extern crate fuchsia_zircon as zx; +#[cfg(target_os = "fuchsia")] +extern crate fuchsia_device; extern crate libc; -extern crate encode_unicode; #[macro_use] extern crate log; -extern crate unicode_segmentation; -extern crate unicode_width; +extern crate memchr; #[cfg(all(unix, not(any(target_os = "fuchsia"))))] extern crate nix; +extern crate unicode_segmentation; +extern crate unicode_width; +#[cfg(unix)] +extern crate utf8parse; #[cfg(windows)] extern crate winapi; -#[cfg(windows)] -extern crate kernel32; -#[cfg(target_os = "fuchsia")] -extern crate fuchsia_zircon as zx; -#[cfg(target_os = "fuchsia")] -extern crate fuchsia_device; pub mod completion; +pub mod config; mod consts; +mod edit; pub mod error; +pub mod highlight; +pub mod hint; pub mod history; mod keymap; mod kill_ring; pub mod line_buffer; -#[cfg(unix)] -mod char_iter; -pub mod config; +mod undo; mod tty; -use std::cell::RefCell; use std::collections::HashMap; use std::fmt; use std::io::{self, Write}; -use std::mem; use std::path::Path; -use std::rc::Rc; use std::result; -use unicode_segmentation::UnicodeSegmentation; -use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; +use std::sync::{Arc, Mutex, RwLock}; +use unicode_width::UnicodeWidthStr; -use tty::{RawMode, RawReader, Terminal, Term}; +use tty::{RawMode, RawReader, Renderer, Term, Terminal}; -use encode_unicode::CharExt; -use completion::{Completer, longest_common_prefix}; -use history::{Direction, History}; -use line_buffer::{LineBuffer, MAX_LINE, WordAction}; -pub use keymap::{Anchor, At, CharSearch, Cmd, Movement, RepeatCount, Word}; -use keymap::EditState; -use kill_ring::{Mode, KillRing}; +use completion::{longest_common_prefix, Candidate, Completer}; pub use config::{CompletionType, Config, EditMode, HistoryDuplicates}; pub use consts::KeyPress; +use edit::State; +use highlight::Highlighter; +use hint::Hinter; +use history::{Direction, History}; +pub use keymap::{Anchor, At, CharSearch, Cmd, Movement, RepeatCount, Word}; +use keymap::{InputState, Refresher}; +use kill_ring::{KillRing, Mode}; +use line_buffer::WordAction; /// The error type for I/O and Linux Syscalls (Errno) pub type Result<T> = result::Result<T, error::ReadlineError>; -// Represent the state during line editing. -struct State<'out, 'prompt> { - out: &'out mut Write, - prompt: &'prompt str, // Prompt to display - prompt_size: Position, // Prompt Unicode width and height - line: LineBuffer, // Edited line buffer - cursor: Position, // Cursor position (relative to the start of the prompt for `row`) - cols: usize, // Number of columns in terminal - old_rows: usize, // Number of rows used so far (from start of prompt to end of input) - history_index: usize, // The history index we are currently editing - snapshot: LineBuffer, // Current edited line before history browsing/completion - term: Terminal, // terminal - edit_state: EditState, -} - -#[derive(Copy, Clone, Debug, Default)] -struct Position { - col: usize, - row: usize, -} - -impl<'out, 'prompt> State<'out, 'prompt> { - fn new(out: &'out mut Write, - term: Terminal, - config: &Config, - prompt: &'prompt str, - history_index: usize, - custom_bindings: Rc<RefCell<HashMap<KeyPress, Cmd>>>) - -> State<'out, 'prompt> { - let capacity = MAX_LINE; - let cols = term.get_columns(); - let prompt_size = calculate_position(prompt, Position::default(), cols); - State { - out: out, - prompt: prompt, - prompt_size: prompt_size, - line: LineBuffer::with_capacity(capacity), - cursor: prompt_size, - cols: cols, - old_rows: prompt_size.row, - history_index: history_index, - snapshot: LineBuffer::with_capacity(capacity), - term: term, - edit_state: EditState::new(config, custom_bindings), - } - } - - fn next_cmd<R: RawReader>(&mut self, rdr: &mut R) -> Result<Cmd> { - loop { - let rc = self.edit_state.next_cmd(rdr); - if rc.is_err() && self.term.sigwinch() { - self.update_columns(); - try!(self.refresh_line()); - continue; - } - return rc; - } - } - - fn snapshot(&mut self) { - mem::swap(&mut self.line, &mut self.snapshot); - } - - fn backup(&mut self) { - self.snapshot.backup(&self.line); - } - - /// 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<()> { - let prompt_size = self.prompt_size; - self.refresh(self.prompt, prompt_size) - } - - fn refresh_prompt_and_line(&mut self, prompt: &str) -> Result<()> { - let prompt_size = calculate_position(prompt, Position::default(), self.cols); - self.refresh(prompt, prompt_size) - } - - #[cfg(unix)] - fn refresh(&mut self, prompt: &str, prompt_size: Position) -> Result<()> { - use std::fmt::Write; - - // calculate the position of the end of the input line - let end_pos = calculate_position(&self.line, prompt_size, self.cols); - // calculate the desired position of the cursor - let cursor = calculate_position(&self.line[..self.line.pos()], prompt_size, self.cols); - - let mut ab = String::new(); - - let cursor_row_movement = self.old_rows - self.cursor.row; - // move the cursor down as required - if cursor_row_movement > 0 { - write!(ab, "\x1b[{}B", cursor_row_movement).unwrap(); - } - // clear old rows - for _ in 0..self.old_rows { - ab.push_str("\r\x1b[0K\x1b[1A"); - } - // clear the line - ab.push_str("\r\x1b[0K"); - - // display the prompt - ab.push_str(prompt); - // display the input line - ab.push_str(&self.line); - // we have to generate our own newline on line wrap - if end_pos.col == 0 && end_pos.row > 0 { - ab.push_str("\n"); - } - // position the cursor - let cursor_row_movement = end_pos.row - cursor.row; - // move the cursor up as required - if cursor_row_movement > 0 { - write!(ab, "\x1b[{}A", cursor_row_movement).unwrap(); - } - // position the cursor within the line - if cursor.col > 0 { - write!(ab, "\r\x1b[{}C", cursor.col).unwrap(); - } else { - ab.push('\r'); - } - - self.cursor = cursor; - self.old_rows = end_pos.row; - - write_and_flush(self.out, ab.as_bytes()) - } - - #[cfg(windows)] - fn refresh(&mut self, prompt: &str, prompt_size: Position) -> Result<()> { - // calculate the position of the end of the input line - let end_pos = calculate_position(&self.line, prompt_size, self.cols); - // calculate the desired position of the cursor - let cursor = calculate_position(&self.line[..self.line.pos()], prompt_size, self.cols); - - // position at the start of the prompt, clear to end of previous input - let mut info = try!(self.term.get_console_screen_buffer_info()); - info.dwCursorPosition.X = 0; - info.dwCursorPosition.Y -= self.cursor.row as i16; - try!(self.term - .set_console_cursor_position(info.dwCursorPosition)); - let mut _count = 0; - try!(self.term - .fill_console_output_character((info.dwSize.X * (self.old_rows as i16 + 1)) as - u32, - info.dwCursorPosition)); - let mut ab = String::new(); - // display the prompt - ab.push_str(prompt); // TODO handle ansi escape code (SetConsoleTextAttribute) - // display the input line - ab.push_str(&self.line); - try!(write_and_flush(self.out, ab.as_bytes())); - - // position the cursor - let mut info = try!(self.term.get_console_screen_buffer_info()); - info.dwCursorPosition.X = cursor.col as i16; - info.dwCursorPosition.Y -= (end_pos.row - cursor.row) as i16; - try!(self.term - .set_console_cursor_position(info.dwCursorPosition)); - - self.cursor = cursor; - self.old_rows = end_pos.row; - - Ok(()) - } - - fn update_columns(&mut self) { - self.cols = self.term.get_columns(); - } -} - -impl<'out, 'prompt> fmt::Debug for State<'out, 'prompt> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("State") - .field("prompt", &self.prompt) - .field("prompt_size", &self.prompt_size) - .field("buf", &self.line) - .field("cursor", &self.cursor) - .field("cols", &self.cols) - .field("old_rows", &self.old_rows) - .field("history_index", &self.history_index) - .field("snapshot", &self.snapshot) - .finish() - } -} - -fn write_and_flush(w: &mut Write, buf: &[u8]) -> Result<()> { - try!(w.write_all(buf)); - try!(w.flush()); - Ok(()) -} - -/// Beep, used for completion when there is nothing to complete or when all -/// the choices were already shown. -fn beep() -> Result<()> { - write_and_flush(&mut io::stderr(), b"\x07") // TODO bell-style -} - -/// Calculate the number of columns and rows used to display `s` on a `cols` width terminal -/// starting at `orig`. -/// Control characters are treated as having zero width. -/// Characters with 2 column width are correctly handled (not splitted). -#[allow(if_same_then_else)] -fn calculate_position(s: &str, orig: Position, cols: usize) -> Position { - let mut pos = orig; - let mut esc_seq = 0; - for c in s.chars() { - let cw = if esc_seq == 1 { - if c == '[' { - // CSI - esc_seq = 2; - } else { - // two-character sequence - esc_seq = 0; - } - None - } else if esc_seq == 2 { - if c == ';' || (c >= '0' && c <= '9') { - } else if c == 'm' { - // last - esc_seq = 0; - } else { - // not supported - esc_seq = 0; - } - None - } else if c == '\x1b' { - esc_seq = 1; - None - } else if c == '\n' { - pos.col = 0; - pos.row += 1; - None - } else { - c.width() - }; - if let Some(cw) = cw { - pos.col += cw; - if pos.col > cols { - pos.row += 1; - pos.col = cw; - } - } - } - if pos.col == cols { - pos.col = 0; - pos.row += 1; - } - pos -} - -/// Insert the character `ch` at cursor current position. -fn edit_insert(s: &mut State, ch: char, n: RepeatCount) -> Result<()> { - if let Some(push) = s.line.insert(ch, n) { - if push { - if n == 1 && s.cursor.col + ch.width().unwrap_or(0) < s.cols { - // Avoid a full update of the line in the trivial case. - let cursor = calculate_position(&s.line[..s.line.pos()], s.prompt_size, s.cols); - s.cursor = cursor; - write_and_flush(s.out, ch.to_utf8().as_bytes()) - } else { - s.refresh_line() - } - } else { - s.refresh_line() - } - } else { - Ok(()) - } -} - -/// Replace a single (or n) character(s) under the cursor (Vi mode) -fn edit_replace_char(s: &mut State, ch: char, n: RepeatCount) -> Result<()> { - if let Some(chars) = s.line.delete(n) { - let count = chars.graphemes(true).count(); - s.line.insert(ch, count); - s.line.move_backward(1); - s.refresh_line() - } else { - Ok(()) - } -} - -// Yank/paste `text` at current position. -fn edit_yank(s: &mut State, text: &str, anchor: Anchor, n: RepeatCount) -> Result<()> { - if let Anchor::After = anchor { - s.line.move_forward(1); - } - if s.line.yank(text, n).is_some() { - if !s.edit_state.is_emacs_mode() { - s.line.move_backward(1); - } - s.refresh_line() - } else { - Ok(()) - } -} - -// Delete previously yanked text and yank/paste `text` at current position. -fn edit_yank_pop(s: &mut State, yank_size: usize, text: &str) -> Result<()> { - s.line.yank_pop(yank_size, text); - edit_yank(s, text, Anchor::Before, 1) -} - -/// Move cursor on the left. -fn edit_move_backward(s: &mut State, n: RepeatCount) -> Result<()> { - if s.line.move_backward(n) { - s.refresh_line() - } else { - Ok(()) - } -} - -/// Move cursor on the right. -fn edit_move_forward(s: &mut State, n: RepeatCount) -> Result<()> { - if s.line.move_forward(n) { - s.refresh_line() - } else { - Ok(()) - } -} - -/// Move cursor to the start of the line. -fn edit_move_home(s: &mut State) -> Result<()> { - if s.line.move_home() { - s.refresh_line() - } else { - Ok(()) - } -} - -/// Move cursor to the end of the line. -fn edit_move_end(s: &mut State) -> Result<()> { - if s.line.move_end() { - s.refresh_line() - } else { - Ok(()) - } -} - -/// Delete the character at the right of the cursor without altering the cursor -/// position. Basically this is what happens with the "Delete" keyboard key. -fn edit_delete(s: &mut State, n: RepeatCount) -> Result<()> { - if s.line.delete(n).is_some() { - s.refresh_line() - } else { - Ok(()) - } -} - -/// Backspace implementation. -fn edit_backspace(s: &mut State, n: RepeatCount) -> Result<()> { - if s.line.backspace(n).is_some() { - s.refresh_line() - } else { - Ok(()) - } -} - -/// Kill the text from point to the end of the line. -fn edit_kill_line(s: &mut State) -> Result<Option<String>> { - if let Some(text) = s.line.kill_line() { - try!(s.refresh_line()); - Ok(Some(text)) - } else { - Ok(None) - } -} - -/// Kill backward from point to the beginning of the line. -fn edit_discard_line(s: &mut State) -> Result<Option<String>> { - if let Some(text) = s.line.discard_line() { - try!(s.refresh_line()); - Ok(Some(text)) - } else { - Ok(None) - } -} - -/// Exchange the char before cursor with the character at cursor. -fn edit_transpose_chars(s: &mut State) -> Result<()> { - if s.line.transpose_chars() { - s.refresh_line() - } else { - Ok(()) - } -} - -fn edit_move_to_prev_word(s: &mut State, word_def: Word, n: RepeatCount) -> Result<()> { - if s.line.move_to_prev_word(word_def, n) { - s.refresh_line() - } else { - Ok(()) - } -} - -/// Delete the previous word, maintaining the cursor at the start of the -/// current word. -fn edit_delete_prev_word(s: &mut State, word_def: Word, n: RepeatCount) -> Result<Option<String>> { - if let Some(text) = s.line.delete_prev_word(word_def, n) { - try!(s.refresh_line()); - Ok(Some(text)) - } else { - Ok(None) - } -} - -fn edit_move_to_next_word(s: &mut State, at: At, word_def: Word, n: RepeatCount) -> Result<()> { - if s.line.move_to_next_word(at, word_def, n) { - s.refresh_line() - } else { - Ok(()) - } -} - -fn edit_move_to(s: &mut State, cs: CharSearch, n: RepeatCount) -> Result<()> { - if s.line.move_to(cs, n) { - s.refresh_line() - } else { - Ok(()) - } -} - -/// Kill from the cursor to the end of the current word, or, if between words, to the end of the next word. -fn edit_delete_word(s: &mut State, - at: At, - word_def: Word, - n: RepeatCount) - -> Result<Option<String>> { - if let Some(text) = s.line.delete_word(at, word_def, n) { - try!(s.refresh_line()); - Ok(Some(text)) - } else { - Ok(None) - } -} - -fn edit_delete_to(s: &mut State, cs: CharSearch, n: RepeatCount) -> Result<Option<String>> { - if let Some(text) = s.line.delete_to(cs, n) { - try!(s.refresh_line()); - Ok(Some(text)) - } else { - Ok(None) - } -} - -fn edit_word(s: &mut State, a: WordAction) -> Result<()> { - if s.line.edit_word(a) { - s.refresh_line() - } else { - Ok(()) - } -} - -fn edit_transpose_words(s: &mut State, n: RepeatCount) -> Result<()> { - if s.line.transpose_words(n) { - s.refresh_line() - } else { - Ok(()) - } -} - -/// Substitute the currently edited line with the next or previous history -/// entry. -fn edit_history_next(s: &mut State, history: &History, prev: bool) -> Result<()> { - if history.is_empty() { - return Ok(()); - } - if s.history_index == history.len() { - if prev { - // Save the current edited line before to overwrite it - s.snapshot(); - } else { - return Ok(()); - } - } else if s.history_index == 0 && prev { - return Ok(()); - } - if prev { - s.history_index -= 1; - } else { - s.history_index += 1; - } - if s.history_index < history.len() { - let buf = history.get(s.history_index).unwrap(); - s.line.update(buf, buf.len()); - } else { - // Restore current edited line - s.snapshot(); - } - s.refresh_line() -} - -fn edit_history_search(s: &mut State, history: &History, dir: Direction) -> Result<()> { - if history.is_empty() { - return beep(); - } - if s.history_index == history.len() && dir == Direction::Forward { - return beep(); - } else if s.history_index == 0 && dir == Direction::Reverse { - return beep(); - } - if dir == Direction::Reverse { - s.history_index -= 1; - } else { - s.history_index += 1; - } - if let Some(history_index) = - history.starts_with(&s.line.as_str()[..s.line.pos()], s.history_index, dir) { - s.history_index = history_index; - let buf = history.get(history_index).unwrap(); - s.line.update(buf, buf.len()); - s.refresh_line() - } else { - beep() - } -} - -/// Substitute the currently edited line with the first/last history entry. -fn edit_history(s: &mut State, history: &History, first: bool) -> Result<()> { - if history.is_empty() { - return Ok(()); - } - if s.history_index == history.len() { - if first { - // Save the current edited line before to overwrite it - s.snapshot(); - } else { - return Ok(()); - } - } else if s.history_index == 0 && first { - return Ok(()); - } - if first { - s.history_index = 0; - let buf = history.get(s.history_index).unwrap(); - s.line.update(buf, buf.len()); - } else { - s.history_index = history.len(); - // Restore current edited line - s.snapshot(); - } - s.refresh_line() -} - /// Completes the line/word -fn complete_line<R: RawReader>(rdr: &mut R, - s: &mut State, - completer: &Completer, - config: &Config) - -> Result<Option<Cmd>> { +fn complete_line<R: RawReader, C: Completer>( + rdr: &mut R, + s: &mut State, + input_state: &mut InputState, + completer: &C, + highlighter: Option<&Highlighter>, + config: &Config, +) -> 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 if candidates.is_empty() { - try!(beep()); + try!(s.out.beep()); Ok(None) } else if CompletionType::Circular == config.completion_type() { - // Save the current edited line before to overwrite it - s.backup(); + let mark = s.changes.borrow_mut().begin(); + // Save the current edited line before overwriting it + let backup = s.line.as_str().to_owned(); + let backup_pos = s.line.pos(); let mut cmd; let mut i = 0; loop { // Show completion or original buffer if i < candidates.len() { - completer.update(&mut s.line, start, &candidates[i]); + let candidate = candidates[i].replacement(); + // TODO we can't highlight the line buffer directly + /*let candidate = if let Some(highlighter) = s.highlighter { + highlighter.highlight_candidate(candidate, CompletionType::Circular) + } else { + Borrowed(candidate) + };*/ + completer.update(&mut s.line, start, candidate); try!(s.refresh_line()); } else { // Restore current edited line - s.snapshot(); + s.line.update(&backup, backup_pos); try!(s.refresh_line()); - s.snapshot(); } - cmd = try!(s.next_cmd(rdr)); + cmd = try!(s.next_cmd(input_state, rdr, true)); match cmd { Cmd::Complete => { i = (i + 1) % (candidates.len() + 1); // Circular if i == candidates.len() { - try!(beep()); + try!(s.out.beep()); } } Cmd::Abort => { // Re-show original buffer - s.snapshot(); if i < candidates.len() { + s.line.update(&backup, backup_pos); try!(s.refresh_line()); } + s.changes.borrow_mut().truncate(mark); return Ok(None); } _ => { - if i == candidates.len() { - s.snapshot(); - } + s.changes.borrow_mut().end(); break; } } } Ok(Some(cmd)) } else if CompletionType::List == config.completion_type() { - // beep if ambiguous - if candidates.len() > 1 { - try!(beep()); - } if let Some(lcp) = longest_common_prefix(&candidates) { - // if we can extend the item, extend it and return to main loop + // if we can extend the item, extend it if lcp.len() > s.line.pos() - start { completer.update(&mut s.line, start, lcp); try!(s.refresh_line()); - return Ok(None); } } + // beep if ambiguous + if candidates.len() > 1 { + try!(s.out.beep()); + } else { + return Ok(None); + } // we can't complete any further, wait for second tab - let mut cmd = try!(s.next_cmd(rdr)); + let mut cmd = try!(s.next_cmd(input_state, rdr, true)); // if any character other than tab, pass it to the main loop if cmd != Cmd::Complete { return Ok(Some(cmd)); } // move cursor to EOL to avoid overwriting the command line let save_pos = s.line.pos(); - try!(edit_move_end(s)); + try!(s.edit_move_end()); s.line.set_pos(save_pos); // we got a second tab, maybe show list of possible completions let show_completions = if candidates.len() > config.completion_prompt_limit() { let msg = format!("\nDisplay all {} possibilities? (y or n)", candidates.len()); - try!(write_and_flush(s.out, msg.as_bytes())); + try!(s.out.write_and_flush(msg.as_bytes())); s.old_rows += 1; - while cmd != Cmd::SelfInsert(1, 'y') && cmd != Cmd::SelfInsert(1, 'Y') && - cmd != Cmd::SelfInsert(1, 'n') && - cmd != Cmd::SelfInsert(1, 'N') && - cmd != Cmd::Kill(Movement::BackwardChar(1)) { - cmd = try!(s.next_cmd(rdr)); + while cmd != Cmd::SelfInsert(1, 'y') + && cmd != Cmd::SelfInsert(1, 'Y') + && cmd != Cmd::SelfInsert(1, 'n') + && cmd != Cmd::SelfInsert(1, 'N') + && cmd != Cmd::Kill(Movement::BackwardChar(1)) + { + cmd = try!(s.next_cmd(input_state, rdr, false)); } match cmd { - Cmd::SelfInsert(1, 'y') | - Cmd::SelfInsert(1, 'Y') => true, + Cmd::SelfInsert(1, 'y') | Cmd::SelfInsert(1, 'Y') => true, _ => false, } } else { true }; if show_completions { - page_completions(rdr, s, &candidates) + page_completions(rdr, s, input_state, highlighter, &candidates) } else { try!(s.refresh_line()); Ok(None) @@ -725,60 +194,71 @@ fn complete_line<R: RawReader>(rdr: &mut R, } } -fn page_completions<R: RawReader>(rdr: &mut R, - s: &mut State, - candidates: &[String]) - -> Result<Option<Cmd>> { +fn page_completions<R: RawReader, C: Candidate>( + rdr: &mut R, + s: &mut State, + input_state: &mut InputState, + highlighter: Option<&Highlighter>, + candidates: &[C], +) -> Result<Option<Cmd>> { use std::cmp; let min_col_pad = 2; - let max_width = cmp::min(s.cols, - candidates - .into_iter() - .map(|s| s.as_str().width()) - .max() - .unwrap() + min_col_pad); - let num_cols = s.cols / max_width; - - let mut pause_row = s.term.get_rows() - 1; + let cols = s.out.get_columns(); + let max_width = cmp::min( + cols, + candidates + .into_iter() + .map(|s| s.display().width()) + .max() + .unwrap() + + min_col_pad, + ); + let num_cols = cols / max_width; + + let mut pause_row = s.out.get_rows() - 1; let num_rows = (candidates.len() + num_cols - 1) / num_cols; let mut ab = String::new(); for row in 0..num_rows { if row == pause_row { - try!(write_and_flush(s.out, b"\n--More--")); + try!(s.out.write_and_flush(b"\n--More--")); let mut cmd = Cmd::Noop; - while cmd != Cmd::SelfInsert(1, 'y') && cmd != Cmd::SelfInsert(1_, 'Y') && - cmd != Cmd::SelfInsert(1, 'n') && - cmd != Cmd::SelfInsert(1_, 'N') && - cmd != Cmd::SelfInsert(1, 'q') && - cmd != Cmd::SelfInsert(1, 'Q') && - cmd != Cmd::SelfInsert(1, ' ') && - cmd != Cmd::Kill(Movement::BackwardChar(1)) && - cmd != Cmd::AcceptLine { - cmd = try!(s.next_cmd(rdr)); + while cmd != Cmd::SelfInsert(1, 'y') + && cmd != Cmd::SelfInsert(1, 'Y') + && cmd != Cmd::SelfInsert(1, 'n') + && cmd != Cmd::SelfInsert(1, 'N') + && cmd != Cmd::SelfInsert(1, 'q') + && cmd != Cmd::SelfInsert(1, 'Q') + && cmd != Cmd::SelfInsert(1, ' ') + && cmd != Cmd::Kill(Movement::BackwardChar(1)) + && cmd != Cmd::AcceptLine + { + cmd = try!(s.next_cmd(input_state, rdr, false)); } match cmd { - Cmd::SelfInsert(1, 'y') | - Cmd::SelfInsert(1, 'Y') | - Cmd::SelfInsert(1, ' ') => { - pause_row += s.term.get_rows() - 1; + Cmd::SelfInsert(1, 'y') | Cmd::SelfInsert(1, 'Y') | Cmd::SelfInsert(1, ' ') => { + pause_row += s.out.get_rows() - 1; } Cmd::AcceptLine => { pause_row += 1; } _ => break, } - try!(write_and_flush(s.out, b"\n")); + try!(s.out.write_and_flush(b"\n")); } else { - try!(write_and_flush(s.out, b"\n")); + try!(s.out.write_and_flush(b"\n")); } ab.clear(); for col in 0..num_cols { let i = (col * num_rows) + row; if i < candidates.len() { - let candidate = &candidates[i]; - ab.push_str(candidate); - let width = candidate.as_str().width(); + let candidate = &candidates[i].display(); + let width = candidate.width(); + if let Some(highlighter) = highlighter { + ab.push_str(&highlighter.highlight_candidate(candidate, CompletionType::List)); + } else { + ab.push_str(candidate); + } if ((col + 1) * num_rows) + row < candidates.len() { for _ in width..max_width { ab.push(' '); @@ -786,23 +266,27 @@ fn page_completions<R: RawReader>(rdr: &mut R, } } } - try!(write_and_flush(s.out, ab.as_bytes())); + try!(s.out.write_and_flush(ab.as_bytes())); } - try!(write_and_flush(s.out, b"\n")); + try!(s.out.write_and_flush(b"\n")); try!(s.refresh_line()); Ok(None) } /// Incremental search -fn reverse_incremental_search<R: RawReader>(rdr: &mut R, - s: &mut State, - history: &History) - -> Result<Option<Cmd>> { +fn reverse_incremental_search<R: RawReader>( + rdr: &mut R, + s: &mut State, + input_state: &mut InputState, + history: &History, +) -> Result<Option<Cmd>> { if history.is_empty() { return Ok(None); } - // Save the current edited line (and cursor position) before to overwrite it - s.snapshot(); + let mark = s.changes.borrow_mut().begin(); + // Save the current edited line (and cursor position) before overwriting it + let backup = s.line.as_str().to_owned(); + let backup_pos = s.line.pos(); let mut search_buf = String::new(); let mut history_idx = history.len() - 1; @@ -819,7 +303,7 @@ fn reverse_incremental_search<R: RawReader>(rdr: &mut R, }; try!(s.refresh_prompt_and_line(&prompt)); - cmd = try!(s.next_cmd(rdr)); + cmd = try!(s.next_cmd(input_state, rdr, true)); if let Cmd::SelfInsert(_, c) = cmd { search_buf.push(c); } else { @@ -848,10 +332,15 @@ fn reverse_incremental_search<R: RawReader>(rdr: &mut R, } Cmd::Abort => { // Restore current edited line (before search) - s.snapshot(); + s.line.update(&backup, backup_pos); try!(s.refresh_line()); + s.changes.borrow_mut().truncate(mark); return Ok(None); } + Cmd::Move(_) => { + try!(s.refresh_line()); // restore prompt + break; + } _ => break, } } @@ -866,43 +355,69 @@ fn reverse_incremental_search<R: RawReader>(rdr: &mut R, _ => false, }; } + s.changes.borrow_mut().end(); Ok(Some(cmd)) } /// Handles reading and editting the readline buffer. /// It will also handle special inputs in an appropriate fashion /// (e.g., C-c will exit readline) -#[allow(let_unit_value)] -fn readline_edit<C: Completer>(prompt: &str, - editor: &mut Editor<C>, - original_mode: tty::Mode) - -> Result<String> { - let completer = editor.completer.as_ref().map(|c| c as &Completer); +fn readline_edit<H: Helper>( + prompt: &str, + initial: Option<(&str, &str)>, + editor: &mut Editor<H>, + original_mode: &tty::Mode, +) -> Result<String> { + let completer = editor.helper.as_ref(); + let hinter = editor.helper.as_ref().map(|h| h as &Hinter); + let highlighter = if editor.term.colors_enabled() { + editor.helper.as_ref().map(|h| h as &Highlighter) + } else { + None + }; let mut stdout = editor.term.create_writer(); - editor.kill_ring.reset(); - let mut s = State::new(&mut stdout, - editor.term.clone(), - &editor.config, - prompt, - editor.history.len(), - editor.custom_bindings.clone()); + editor.reset_kill_ring(); // TODO recreate a new kill ring vs Arc<Mutex<KillRing>> + let mut s = State::new( + &mut stdout, + prompt, + editor.history.len(), + hinter, + highlighter, + ); + let mut input_state = InputState::new(&editor.config, Arc::clone(&editor.custom_bindings)); + + s.line.set_delete_listener(editor.kill_ring.clone()); + s.line.set_change_listener(s.changes.clone()); + + if let Some((left, right)) = initial { + s.line + .update((left.to_owned() + right).as_ref(), left.len()); + } + try!(s.refresh_line()); - let mut rdr = try!(s.term.create_reader(&editor.config)); + let mut rdr = try!(editor.term.create_reader(&editor.config)); loop { - let rc = s.next_cmd(&mut rdr); + let rc = s.next_cmd(&mut input_state, &mut rdr, false); let mut cmd = try!(rc); if cmd.should_reset_kill_ring() { - editor.kill_ring.reset(); + editor.reset_kill_ring(); } // autocomplete if cmd == Cmd::Complete && completer.is_some() { - let next = try!(complete_line(&mut rdr, &mut s, completer.unwrap(), &editor.config)); + let next = try!(complete_line( + &mut rdr, + &mut s, + &mut input_state, + completer.unwrap(), + highlighter, + &editor.config, + )); if next.is_some() { cmd = next.unwrap(); } else { @@ -911,16 +426,21 @@ fn readline_edit<C: Completer>(prompt: &str, } if let Cmd::SelfInsert(n, c) = cmd { - try!(edit_insert(&mut s, c, n)); + try!(s.edit_insert(c, n)); continue; } else if let Cmd::Insert(n, text) = cmd { - try!(edit_yank(&mut s, &text, Anchor::Before, n)); + try!(s.edit_yank(&input_state, &text, Anchor::Before, n)); continue; } if cmd == Cmd::ReverseSearchHistory { // Search history backward - let next = try!(reverse_incremental_search(&mut rdr, &mut s, &editor.history)); + let next = try!(reverse_incremental_search( + &mut rdr, + &mut s, + &mut input_state, + &editor.history, + )); if next.is_some() { cmd = next.unwrap(); } else { @@ -931,164 +451,147 @@ fn readline_edit<C: Completer>(prompt: &str, match cmd { Cmd::Move(Movement::BeginningOfLine) => { // Move to the beginning of line. - try!(edit_move_home(&mut s)) + try!(s.edit_move_home()) } Cmd::Move(Movement::ViFirstPrint) => { - try!(edit_move_home(&mut s)); - try!(edit_move_to_next_word(&mut s, At::Start, Word::Big, 1)) + try!(s.edit_move_home()); + try!(s.edit_move_to_next_word(At::Start, Word::Big, 1)) } Cmd::Move(Movement::BackwardChar(n)) => { // Move back a character. - try!(edit_move_backward(&mut s, n)) - } - Cmd::Kill(Movement::ForwardChar(n)) => { - // Delete (forward) one character at point. - try!(edit_delete(&mut s, n)) - } - Cmd::Replace(n, c) => { - try!(edit_replace_char(&mut s, c, n)); + try!(s.edit_move_backward(n)) } - Cmd::EndOfFile => { - if !s.edit_state.is_emacs_mode() && !s.line.is_empty() { - try!(edit_move_end(&mut s)); - break; - } else if s.line.is_empty() { - return Err(error::ReadlineError::Eof); - } else { - try!(edit_delete(&mut s, 1)) + Cmd::ReplaceChar(n, c) => try!(s.edit_replace_char(c, n)), + Cmd::Replace(mvt, text) => { + try!(s.edit_kill(&mvt)); + if let Some(text) = text { + try!(s.edit_insert_text(&text)) } } + Cmd::Overwrite(c) => { + try!(s.edit_overwrite_char(c)); + } + Cmd::EndOfFile => if !input_state.is_emacs_mode() && !s.line.is_empty() { + try!(s.edit_move_end()); + break; + } else if s.line.is_empty() { + return Err(error::ReadlineError::Eof); + } else { + try!(s.edit_delete(1)) + }, Cmd::Move(Movement::EndOfLine) => { // Move to the end of line. - try!(edit_move_end(&mut s)) + try!(s.edit_move_end()) } Cmd::Move(Movement::ForwardChar(n)) => { // Move forward a character. - try!(edit_move_forward(&mut s, n)) - } - Cmd::Kill(Movement::BackwardChar(n)) => { - // Delete one character backward. - try!(edit_backspace(&mut s, n)) - } - Cmd::Kill(Movement::EndOfLine) => { - // 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) - } - } - Cmd::Kill(Movement::WholeLine) => { - try!(edit_move_home(&mut s)); - if let Some(text) = try!(edit_kill_line(&mut s)) { - editor.kill_ring.kill(&text, Mode::Append) - } + try!(s.edit_move_forward(n)) } 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.out.clear_screen()); try!(s.refresh_line()) } Cmd::NextHistory => { // Fetch the next command from the history list. - try!(edit_history_next(&mut s, &editor.history, false)) + try!(s.edit_history_next(&editor.history, false)) } Cmd::PreviousHistory => { // Fetch the previous command from the history list. - try!(edit_history_next(&mut s, &editor.history, true)) + try!(s.edit_history_next(&editor.history, true)) } Cmd::HistorySearchBackward => { - try!(edit_history_search(&mut s, &editor.history, Direction::Reverse)) + try!(s.edit_history_search(&editor.history, Direction::Reverse)) } Cmd::HistorySearchForward => { - try!(edit_history_search(&mut s, &editor.history, Direction::Forward)) + try!(s.edit_history_search(&editor.history, Direction::Forward)) } Cmd::TransposeChars => { // Exchange the char before cursor with the character at cursor. - try!(edit_transpose_chars(&mut s)) - } - Cmd::Kill(Movement::BeginningOfLine) => { - // 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) - } + try!(s.edit_transpose_chars()) } #[cfg(unix)] Cmd::QuotedInsert => { // Quoted insert let c = try!(rdr.next_char()); - try!(edit_insert(&mut s, c, 1)) // FIXME + try!(s.edit_insert(c, 1)) // FIXME } Cmd::Yank(n, anchor) => { // retrieve (yank) last item killed - if let Some(text) = editor.kill_ring.yank() { - try!(edit_yank(&mut s, text, anchor, n)) - } - } - Cmd::ViYankTo(mvt) => { - if let Some(text) = s.line.copy(mvt) { - editor.kill_ring.kill(&text, Mode::Append) + let mut kill_ring = editor.kill_ring.lock().unwrap(); + if let Some(text) = kill_ring.yank() { + try!(s.edit_yank(&input_state, text, anchor, n)) } } + Cmd::ViYankTo(ref mvt) => if let Some(text) = s.line.copy(mvt) { + let mut kill_ring = editor.kill_ring.lock().unwrap(); + kill_ring.kill(&text, Mode::Append) + }, // TODO CTRL-_ // undo Cmd::AcceptLine => { + #[cfg(test)] + { + editor.term.cursor = s.cursor.col; + } // Accept the line regardless of where the cursor is. - try!(edit_move_end(&mut s)); - break; - } - Cmd::Kill(Movement::BackwardWord(n, word_def)) => { - // kill one word backward (until start of word) - if let Some(text) = try!(edit_delete_prev_word(&mut s, word_def, n)) { - editor.kill_ring.kill(&text, Mode::Prepend) + try!(s.edit_move_end()); + if s.hinter.is_some() { + // Force a refresh without hints to leave the previous + // line as the user typed it after a newline. + s.hinter = None; + try!(s.refresh_line()); } + break; } Cmd::BeginningOfHistory => { // move to first entry in history - try!(edit_history(&mut s, &editor.history, true)) + try!(s.edit_history(&editor.history, true)) } Cmd::EndOfHistory => { // move to last entry in history - try!(edit_history(&mut s, &editor.history, false)) + try!(s.edit_history(&editor.history, false)) } Cmd::Move(Movement::BackwardWord(n, word_def)) => { // move backwards one word - try!(edit_move_to_prev_word(&mut s, word_def, n)) + try!(s.edit_move_to_prev_word(word_def, n)) } Cmd::CapitalizeWord => { // capitalize word after point - try!(edit_word(&mut s, WordAction::CAPITALIZE)) + try!(s.edit_word(WordAction::CAPITALIZE)) } - Cmd::Kill(Movement::ForwardWord(n, at, word_def)) => { - // kill one word forward (until start/end of word) - if let Some(text) = try!(edit_delete_word(&mut s, at, word_def, n)) { - editor.kill_ring.kill(&text, Mode::Append) - } + Cmd::Kill(ref mvt) => { + try!(s.edit_kill(mvt)); } Cmd::Move(Movement::ForwardWord(n, at, word_def)) => { // move forwards one word - try!(edit_move_to_next_word(&mut s, at, word_def, n)) + try!(s.edit_move_to_next_word(at, word_def, n)) } Cmd::DowncaseWord => { // lowercase word after point - try!(edit_word(&mut s, WordAction::LOWERCASE)) + try!(s.edit_word(WordAction::LOWERCASE)) } Cmd::TransposeWords(n) => { // transpose words - try!(edit_transpose_words(&mut s, n)) + try!(s.edit_transpose_words(n)) } Cmd::UpcaseWord => { // uppercase word after point - try!(edit_word(&mut s, WordAction::UPPERCASE)) + try!(s.edit_word(WordAction::UPPERCASE)) } Cmd::YankPop => { // yank-pop - if let Some((yank_size, text)) = editor.kill_ring.yank_pop() { - try!(edit_yank_pop(&mut s, yank_size, text)) + let mut kill_ring = editor.kill_ring.lock().unwrap(); + if let Some((yank_size, text)) = kill_ring.yank_pop() { + try!(s.edit_yank_pop(yank_size, text)) } } - Cmd::Move(Movement::ViCharSearch(n, cs)) => try!(edit_move_to(&mut s, cs, n)), - Cmd::Kill(Movement::ViCharSearch(n, cs)) => { - if let Some(text) = try!(edit_delete_to(&mut s, cs, n)) { - editor.kill_ring.kill(&text, Mode::Append) + Cmd::Move(Movement::ViCharSearch(n, cs)) => try!(s.edit_move_to(cs, n)), + Cmd::Undo(n) => { + s.line.remove_change_listener(); + if s.changes.borrow_mut().undo(&mut s.line, n) { + try!(s.refresh_line()); } + s.line.set_change_listener(s.changes.clone()); } Cmd::Interrupt => { return Err(error::ReadlineError::Interrupted); @@ -1097,7 +600,7 @@ fn readline_edit<C: Completer>(prompt: &str, Cmd::Suspend => { try!(original_mode.disable_raw_mode()); try!(tty::suspend()); - try!(s.term.enable_raw_mode()); // TODO original_mode may have changed + try!(editor.term.enable_raw_mode()); // TODO original_mode may have changed try!(s.refresh_line()); continue; } @@ -1107,13 +610,16 @@ fn readline_edit<C: Completer>(prompt: &str, } } } + if cfg!(windows) { + let _ = original_mode; // silent warning + } Ok(s.line.into_string()) } -struct Guard(tty::Mode); +struct Guard<'m>(&'m tty::Mode); #[allow(unused_must_use)] -impl Drop for Guard { +impl<'m> Drop for Guard<'m> { fn drop(&mut self) { let Guard(mode) = *self; mode.disable_raw_mode(); @@ -1122,12 +628,21 @@ impl Drop for Guard { /// Readline method that will enable RAW mode, call the `readline_edit()` /// method and disable raw mode -fn readline_raw<C: Completer>(prompt: &str, editor: &mut Editor<C>) -> Result<String> { +fn readline_raw<H: Helper>( + prompt: &str, + initial: Option<(&str, &str)>, + editor: &mut Editor<H>, +) -> Result<String> { let original_mode = try!(editor.term.enable_raw_mode()); - let guard = Guard(original_mode); - let user_input = readline_edit(prompt, editor, original_mode); + let guard = Guard(&original_mode); + let user_input = readline_edit(prompt, initial, editor, &original_mode); + if editor.config.auto_add_history() { + if let Ok(ref line) = user_input { + editor.add_history_entry(line.as_ref()); + } + } drop(guard); // try!(disable_raw_mode(original_mode)); - println!(""); + println!(); user_input } @@ -1140,40 +655,78 @@ fn readline_direct() -> Result<String> { } } +/// Syntax specific helper. +/// +/// TODO Tokenizer/parser used for both completion, suggestion, highlighting. +/// (parse current line once) +pub trait Helper +where + Self: Completer, + Self: Hinter, + Self: Highlighter, +{ +} + +impl Helper for () {} + /// Line editor -pub struct Editor<C: Completer> { +pub struct Editor<H: Helper> { term: Terminal, history: History, - completer: Option<C>, - kill_ring: KillRing, + helper: Option<H>, + kill_ring: Arc<Mutex<KillRing>>, config: Config, - custom_bindings: Rc<RefCell<HashMap<KeyPress, Cmd>>>, + custom_bindings: Arc<RwLock<HashMap<KeyPress, Cmd>>>, } -impl<C: Completer> Editor<C> { - pub fn new() -> Editor<C> { +#[allow(new_without_default)] +impl<H: Helper> Editor<H> { + /// Create an editor with the default configuration + pub fn new() -> Editor<H> { Self::with_config(Config::default()) } - pub fn with_config(config: Config) -> Editor<C> { - let term = Terminal::new(); + /// Create an editor with a specific configuration. + pub fn with_config(config: Config) -> Editor<H> { + let term = Terminal::new(config.color_mode()); Editor { - term: term, + term, history: History::with_config(config), - completer: None, - kill_ring: KillRing::new(60), - config: config, - custom_bindings: Rc::new(RefCell::new(HashMap::new())), + helper: None, + kill_ring: Arc::new(Mutex::new(KillRing::new(60))), + config, + custom_bindings: Arc::new(RwLock::new(HashMap::new())), } } - /// This method will read a line from STDIN and will display a `prompt` + /// This method will read a line from STDIN and will display a `prompt`. + /// + /// It uses terminal-style interaction if `stdin` is connected to a + /// terminal. + /// Otherwise (e.g., if `stdin` is a pipe or the terminal is not supported), + /// it uses file-style interaction. pub fn readline(&mut self, prompt: &str) -> Result<String> { + self.readline_with(prompt, None) + } + + /// This function behaves in the exact same manner as `readline`, except + /// that it pre-populates the input area. + /// + /// The text that resides in the input area is given as a 2-tuple. + /// The string on the left of the tuple is what will appear to the left of + /// the cursor and the string on the right is what will appear to the + /// right of the cursor. + pub fn readline_with_initial(&mut self, prompt: &str, initial: (&str, &str)) -> Result<String> { + self.readline_with(prompt, Some(initial)) + } + + fn readline_with(&mut self, prompt: &str, initial: Option<(&str, &str)>) -> Result<String> { if self.term.is_unsupported() { debug!(target: "rustyline", "unsupported terminal"); // Write prompt and flush it to stdout let mut stdout = io::stdout(); - try!(write_and_flush(&mut stdout, prompt.as_bytes())); + try!(stdout.write_all(prompt.as_bytes())); + try!(stdout.flush()); readline_direct() } else if !self.term.is_stdin_tty() { @@ -1181,7 +734,7 @@ impl<C: Completer> Editor<C> { // Not a tty: read from file / pipe. readline_direct() } else { - readline_raw(prompt, self) + readline_raw(prompt, initial, self) } } @@ -1189,39 +742,53 @@ impl<C: Completer> Editor<C> { pub fn load_history<P: AsRef<Path> + ?Sized>(&mut self, path: &P) -> Result<()> { self.history.load(path) } + /// Save the history in the specified file. pub fn save_history<P: AsRef<Path> + ?Sized>(&self, path: &P) -> Result<()> { self.history.save(path) } + /// Add a new entry in the history. pub fn add_history_entry<S: AsRef<str> + Into<String>>(&mut self, line: S) -> bool { self.history.add(line) } + /// Clear history. pub fn clear_history(&mut self) { self.history.clear() } + /// Return a mutable reference to the history object. - pub fn get_history(&mut self) -> &mut History { + pub fn history_mut(&mut self) -> &mut History { &mut self.history } + /// Return an immutable reference to the history object. - pub fn get_history_const(&self) -> &History { + pub fn history(&self) -> &History { &self.history } - /// Register a callback function to be called for tab-completion. - pub fn set_completer(&mut self, completer: Option<C>) { - self.completer = completer; + /// Register a callback function to be called for tab-completion + /// or to show hints to the user at the right of the prompt. + pub fn set_helper(&mut self, helper: Option<H>) { + self.helper = helper; + } + + /// Return an immutable reference to the helper. + pub fn helper(&self) -> Option<&H> { + self.helper.as_ref() } /// Bind a sequence to a command. pub fn bind_sequence(&mut self, key_seq: KeyPress, cmd: Cmd) -> Option<Cmd> { - self.custom_bindings.borrow_mut().insert(key_seq, cmd) + let mut bindings = self.custom_bindings.write().unwrap(); + bindings.insert(key_seq, cmd) } + /// Remove a binding for the given sequence. pub fn unbind_sequence(&mut self, key_seq: KeyPress) -> Option<Cmd> { - self.custom_bindings.borrow_mut().remove(&key_seq) + let mut bindings = self.custom_bindings.write().unwrap(); + bindings.remove(&key_seq) } /// ``` @@ -1230,23 +797,28 @@ impl<C: Completer> Editor<C> { /// match readline { /// Ok(line) => { /// println!("Line: {}", line); - /// }, + /// } /// Err(err) => { /// println!("Error: {:?}", err); - /// break + /// break; /// } /// } /// } /// ``` - pub fn iter<'a>(&'a mut self, prompt: &'a str) -> Iter<C> { + pub fn iter<'a>(&'a mut self, prompt: &'a str) -> Iter<H> { Iter { editor: self, - prompt: prompt, + prompt, } } + + fn reset_kill_ring(&self) { + let mut kill_ring = self.kill_ring.lock().unwrap(); + kill_ring.reset(); + } } -impl<C: Completer> fmt::Debug for Editor<C> { +impl<H: Helper> fmt::Debug for Editor<H> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_struct("Editor") .field("term", &self.term) @@ -1255,23 +827,22 @@ impl<C: Completer> fmt::Debug for Editor<C> { } } -pub struct Iter<'a, C: Completer> - where C: 'a +/// Edited lines iterator +pub struct Iter<'a, H: Helper> +where + H: 'a, { - editor: &'a mut Editor<C>, + editor: &'a mut Editor<H>, prompt: &'a str, } -impl<'a, C: Completer> Iterator for Iter<'a, C> { +impl<'a, H: Helper> Iterator for Iter<'a, H> { type Item = Result<String>; fn next(&mut self) -> Option<Result<String>> { let readline = self.editor.readline(self.prompt); match readline { - Ok(l) => { - self.editor.add_history_entry(l.as_ref()); // TODO Validate - Some(Ok(l)) - } + Ok(l) => Some(Ok(l)), Err(error::ReadlineError::Eof) => None, e @ Err(_) => Some(e), } @@ -1279,177 +850,7 @@ impl<'a, C: Completer> Iterator for Iter<'a, C> { } #[cfg(test)] -mod test { - use std::cell::RefCell; - use std::collections::HashMap; - use std::io::Write; - use std::rc::Rc; - use line_buffer::LineBuffer; - use history::History; - use completion::Completer; - use config::Config; - use consts::KeyPress; - use keymap::{Cmd, EditState}; - use super::{Editor, Position, Result, State}; - use tty::{Terminal, Term}; - - fn init_state<'out>(out: &'out mut Write, - line: &str, - pos: usize, - cols: usize) - -> State<'out, 'static> { - let term = Terminal::new(); - let config = Config::default(); - State { - out: out, - prompt: "", - prompt_size: Position::default(), - line: LineBuffer::init(line, pos), - cursor: Position::default(), - cols: cols, - old_rows: 0, - history_index: 0, - snapshot: LineBuffer::with_capacity(100), - term: term, - edit_state: EditState::new(&config, Rc::new(RefCell::new(HashMap::new()))), - } - } - - fn init_editor(keys: &[KeyPress]) -> Editor<()> { - let mut editor = Editor::<()>::new(); - editor.term.keys.extend(keys.iter().cloned()); - editor - } - - #[test] - fn edit_history_next() { - let mut out = ::std::io::sink(); - let line = "current edited line"; - let mut s = init_state(&mut out, line, 6, 80); - let mut history = History::new(); - history.add("line0"); - history.add("line1"); - s.history_index = history.len(); - - for _ in 0..2 { - super::edit_history_next(&mut s, &history, false).unwrap(); - assert_eq!(line, s.line.as_str()); - } - - super::edit_history_next(&mut s, &history, true).unwrap(); - assert_eq!(line, s.snapshot.as_str()); - assert_eq!(1, s.history_index); - assert_eq!("line1", s.line.as_str()); - - for _ in 0..2 { - super::edit_history_next(&mut s, &history, true).unwrap(); - assert_eq!(line, s.snapshot.as_str()); - assert_eq!(0, s.history_index); - assert_eq!("line0", s.line.as_str()); - } - - super::edit_history_next(&mut s, &history, false).unwrap(); - assert_eq!(line, s.snapshot.as_str()); - assert_eq!(1, s.history_index); - assert_eq!("line1", s.line.as_str()); - - super::edit_history_next(&mut s, &history, false).unwrap(); - // assert_eq!(line, s.snapshot); - assert_eq!(2, s.history_index); - assert_eq!(line, s.line.as_str()); - } - - struct SimpleCompleter; - impl Completer for SimpleCompleter { - fn complete(&self, line: &str, _pos: usize) -> Result<(usize, Vec<String>)> { - Ok((0, vec![line.to_string() + "t"])) - } - } - - #[test] - fn complete_line() { - let mut out = ::std::io::sink(); - let mut s = init_state(&mut out, "rus", 3, 80); - let keys = &[KeyPress::Enter]; - let mut rdr = keys.iter(); - let completer = SimpleCompleter; - 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()); - } - - #[test] - fn prompt_with_ansi_escape_codes() { - let pos = super::calculate_position("\x1b[1;32m>>\x1b[0m ", Position::default(), 80); - assert_eq!(3, pos.col); - assert_eq!(0, pos.row); - } - - 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/line_buffer.rs b/src/line_buffer.rs index 80699e21a94b0fa07eedd628da73f31f15685453..e17fd621f0a9080d04612147defca7baf02c8e80 100644 --- a/src/line_buffer.rs +++ b/src/line_buffer.rs @@ -1,22 +1,69 @@ //! Line buffer with current cursor position +use keymap::{At, CharSearch, Movement, RepeatCount, Word}; +use std::cell::RefCell; +use std::fmt; use std::iter; -use std::ops::{Deref, Range}; +use std::ops::{Deref, Index, Range}; +use std::rc::Rc; +use std::string::Drain; +use std::sync::{Arc, Mutex}; use unicode_segmentation::UnicodeSegmentation; -use keymap::{At, CharSearch, Movement, RepeatCount, Word}; /// Maximum buffer size for the line read -pub static MAX_LINE: usize = 4096; +pub(crate) static MAX_LINE: usize = 4096; +/// Word's case change +#[derive(Clone, Copy)] pub enum WordAction { CAPITALIZE, LOWERCASE, UPPERCASE, } -#[derive(Debug)] +/// Delete (kill) direction +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum Direction { + Forward, + Backward, +} + +impl Default for Direction { + fn default() -> Direction { + Direction::Forward + } +} + +/// Listener to be notified when some text is deleted. +pub(crate) trait DeleteListener { + fn start_killing(&mut self); + fn delete(&mut self, idx: usize, string: &str, dir: Direction); + fn stop_killing(&mut self); +} + +/// Listener to be notified when the line is modified. +pub(crate) trait ChangeListener: DeleteListener { + fn insert_char(&mut self, idx: usize, c: char); + fn insert_str(&mut self, idx: usize, string: &str); + fn replace(&mut self, idx: usize, old: &str, new: &str); +} + +/// Represent the current input (text and cursor position). +/// +/// The methods do text manipulations or/and cursor movements. pub struct LineBuffer { - buf: String, // Edited line buffer - pos: usize, // Current cursor position (byte position) + buf: String, // Edited line buffer (rl_line_buffer) + pos: usize, // Current cursor position (byte position) (rl_point) + dl: Option<Arc<Mutex<DeleteListener>>>, + cl: Option<Rc<RefCell<ChangeListener>>>, +} + +impl fmt::Debug for LineBuffer { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("LineBuffer") + .field("buf", &self.buf) + .field("pos", &self.pos) + .finish() + } } impl LineBuffer { @@ -25,17 +72,36 @@ impl LineBuffer { LineBuffer { buf: String::with_capacity(capacity), pos: 0, + dl: None, + cl: None, } } #[cfg(test)] - pub fn init(line: &str, pos: usize) -> LineBuffer { + pub(crate) fn init( + line: &str, + pos: usize, + cl: Option<Rc<RefCell<ChangeListener>>>, + ) -> LineBuffer { let mut lb = Self::with_capacity(MAX_LINE); assert!(lb.insert_str(0, line)); lb.set_pos(pos); + lb.cl = cl; lb } + pub(crate) fn set_delete_listener(&mut self, dl: Arc<Mutex<DeleteListener>>) { + self.dl = Some(dl); + } + + pub(crate) fn set_change_listener(&mut self, dl: Rc<RefCell<ChangeListener>>) { + self.cl = Some(dl); + } + + pub(crate) fn remove_change_listener(&mut self) { + self.cl = None; + } + /// Extracts a string slice containing the entire buffer. pub fn as_str(&self) -> &str { &self.buf @@ -50,6 +116,8 @@ impl LineBuffer { pub fn pos(&self) -> usize { self.pos } + + /// Set cursor position (byte position) pub fn set_pos(&mut self, pos: usize) { assert!(pos <= self.buf.len()); self.pos = pos; @@ -59,6 +127,7 @@ impl LineBuffer { pub fn len(&self) -> usize { self.buf.len() } + /// Returns `true` if this buffer has a length of zero. pub fn is_empty(&self) -> bool { self.buf.is_empty() @@ -67,30 +136,24 @@ impl LineBuffer { /// Set line content (`buf`) and cursor position (`pos`). pub fn update(&mut self, buf: &str, pos: usize) { assert!(pos <= buf.len()); - self.buf.clear(); + let end = self.len(); + self.drain(0..end, Direction::default()); let max = self.buf.capacity(); if buf.len() > max { - self.buf.push_str(&buf[..max]); + self.insert_str(0, &buf[..max]); if pos > max { self.pos = max; } else { self.pos = pos; } } else { - self.buf.push_str(buf); + self.insert_str(0, buf); self.pos = pos; } } - /// Backup `src` - pub fn backup(&mut self, src: &LineBuffer) { - self.buf.clear(); - self.buf.push_str(&src.buf); - self.pos = src.pos; - } - /// Returns the character at current cursor position. - fn grapheme_at_cursor(&self) -> Option<&str> { + pub(crate) fn grapheme_at_cursor(&self) -> Option<&str> { if self.pos == self.buf.len() { None } else { @@ -98,7 +161,9 @@ impl LineBuffer { } } - fn next_pos(&self, n: RepeatCount) -> Option<usize> { + /// Returns the position of the character just after the current cursor + /// position. + pub fn next_pos(&self, n: RepeatCount) -> Option<usize> { if self.pos == self.buf.len() { return None; } @@ -108,7 +173,9 @@ impl LineBuffer { .last() .map(|(i, s)| i + self.pos + s.len()) } - /// Returns the position of the character just before the current cursor position. + + /// Returns the position of the character just before the current cursor + /// position. fn prev_pos(&self, n: RepeatCount) -> Option<usize> { if self.pos == 0 { return None; @@ -131,13 +198,11 @@ impl LineBuffer { return None; } let push = self.pos == self.buf.len(); - if push { - self.buf.reserve(shift); - for _ in 0..n { - self.buf.push(ch); - } - } else if n == 1 { + if n == 1 { self.buf.insert(self.pos, ch); + for cl in &self.cl { + cl.borrow_mut().insert_char(self.pos, ch); + } } else { let text = iter::repeat(ch).take(n).collect::<String>(); let pos = self.pos; @@ -156,14 +221,11 @@ impl LineBuffer { return None; } let push = self.pos == self.buf.len(); - if push { - self.buf.reserve(shift); - for _ in 0..n { - self.buf.push_str(text); - } + let pos = self.pos; + if n == 1 { + self.insert_str(pos, text); } else { let text = iter::repeat(text).take(n).collect::<String>(); - let pos = self.pos; self.insert_str(pos, &text); } self.pos += shift; @@ -172,7 +234,9 @@ impl LineBuffer { /// Delete previously yanked text and yank/paste `text` at current position. pub fn yank_pop(&mut self, yank_size: usize, text: &str) -> Option<bool> { - self.buf.drain((self.pos - yank_size)..self.pos); + let end = self.pos; + let start = end - yank_size; + self.drain(start..end, Direction::default()); self.pos -= yank_size; self.yank(text, 1) } @@ -219,13 +283,17 @@ impl LineBuffer { } } - /// Delete the character at the right of the cursor without altering the cursor - /// position. Basically this is what happens with the "Delete" keyboard key. + /// Delete the character at the right of the cursor without altering the + /// cursor position. Basically this is what happens with the "Delete" + /// keyboard key. /// Return the number of characters deleted. pub fn delete(&mut self, n: RepeatCount) -> Option<String> { match self.next_pos(n) { Some(pos) => { - let chars = self.buf.drain(self.pos..pos).collect::<String>(); + let start = self.pos; + let chars = self + .drain(start..pos, Direction::Forward) + .collect::<String>(); Some(chars) } None => None, @@ -234,35 +302,39 @@ impl LineBuffer { /// Delete the character at the left of the cursor. /// Basically that is what happens with the "Backspace" keyboard key. - pub fn backspace(&mut self, n: RepeatCount) -> Option<String> { + pub fn backspace(&mut self, n: RepeatCount) -> bool { match self.prev_pos(n) { Some(pos) => { - let chars = self.buf.drain(pos..self.pos).collect::<String>(); + let end = self.pos; + self.drain(pos..end, Direction::Backward); self.pos = pos; - Some(chars) + true } - None => None, + None => false, } } /// Kill the text from point to the end of the line. - pub fn kill_line(&mut self) -> Option<String> { + pub fn kill_line(&mut self) -> bool { if !self.buf.is_empty() && self.pos < self.buf.len() { - let text = self.buf.drain(self.pos..).collect(); - Some(text) + let start = self.pos; + let end = self.buf.len(); + self.drain(start..end, Direction::Forward); + true } else { - None + false } } /// Kill backward from point to the beginning of the line. - pub fn discard_line(&mut self) -> Option<String> { + pub fn discard_line(&mut self) -> bool { if self.pos > 0 && !self.buf.is_empty() { - let text = self.buf.drain(..self.pos).collect(); + let end = self.pos; + self.drain(0..end, Direction::Backward); self.pos = 0; - Some(text) + true } else { - None + false } } @@ -289,6 +361,7 @@ impl LineBuffer { let mut sow = 0; let mut gis = self.buf[..pos].grapheme_indices(true).rev(); 'outer: for _ in 0..n { + sow = 0; let mut gj = gis.next(); 'inner: loop { match gj { @@ -328,13 +401,14 @@ impl LineBuffer { /// Delete the previous word, maintaining the cursor at the start of the /// current word. - pub fn delete_prev_word(&mut self, word_def: Word, n: RepeatCount) -> Option<String> { + pub fn delete_prev_word(&mut self, word_def: Word, n: RepeatCount) -> bool { if let Some(pos) = self.prev_word_pos(self.pos, word_def, n) { - let word = self.buf.drain(pos..self.pos).collect(); + let end = self.pos; + self.drain(pos..end, Direction::Backward); self.pos = pos; - Some(word) + true } else { - None + false } } @@ -344,13 +418,14 @@ impl LineBuffer { } let mut wp = 0; let mut gis = self.buf[pos..].grapheme_indices(true); - let mut gi = if at != At::Start { + let mut gi = if at == At::BeforeEnd { // TODO Validate gis.next() } else { None }; 'outer: for _ in 0..n { + wp = 0; gi = gis.next(); 'inner: loop { match gi { @@ -406,21 +481,17 @@ impl LineBuffer { } } - fn search_char_pos(&self, cs: &CharSearch, n: RepeatCount) -> Option<usize> { + fn search_char_pos(&self, cs: CharSearch, n: RepeatCount) -> Option<usize> { let mut shift = 0; - let search_result = match *cs { - CharSearch::Backward(c) | - CharSearch::BackwardAfter(c) => { - self.buf[..self.pos] - .char_indices() - .rev() - .filter(|&(_, ch)| ch == c) - .take(n) - .last() - .map(|(i, _)| i) - } - CharSearch::Forward(c) | - CharSearch::ForwardBefore(c) => { + let search_result = match cs { + CharSearch::Backward(c) | CharSearch::BackwardAfter(c) => self.buf[..self.pos] + .char_indices() + .rev() + .filter(|&(_, ch)| ch == c) + .take(n) + .last() + .map(|(i, _)| i), + CharSearch::Forward(c) | CharSearch::ForwardBefore(c) => { if let Some(cc) = self.grapheme_at_cursor() { shift = self.pos + cc.len(); if shift < self.buf.len() { @@ -439,26 +510,27 @@ impl LineBuffer { } }; if let Some(pos) = search_result { - Some(match *cs { - CharSearch::Backward(_) => pos, - CharSearch::BackwardAfter(c) => pos + c.len_utf8(), - CharSearch::Forward(_) => shift + pos, - CharSearch::ForwardBefore(_) => { - shift + pos - - self.buf[..shift + pos] - .chars() - .next_back() - .unwrap() - .len_utf8() - } - }) + Some(match cs { + CharSearch::Backward(_) => pos, + CharSearch::BackwardAfter(c) => pos + c.len_utf8(), + CharSearch::Forward(_) => shift + pos, + CharSearch::ForwardBefore(_) => { + shift + pos - self.buf[..shift + pos] + .chars() + .next_back() + .unwrap() + .len_utf8() + } + }) } else { None } } + /// Move cursor to the matching character position. + /// Return `true` when the search succeeds. pub fn move_to(&mut self, cs: CharSearch, n: RepeatCount) -> bool { - if let Some(pos) = self.search_char_pos(&cs, n) { + if let Some(pos) = self.search_char_pos(cs, n) { self.pos = pos; true } else { @@ -468,34 +540,40 @@ impl LineBuffer { /// Kill from the cursor to the end of the current word, /// or, if between words, to the end of the next word. - pub fn delete_word(&mut self, at: At, word_def: Word, n: RepeatCount) -> Option<String> { + pub fn delete_word(&mut self, at: At, word_def: Word, n: RepeatCount) -> bool { if let Some(pos) = self.next_word_pos(self.pos, at, word_def, n) { - let word = self.buf.drain(self.pos..pos).collect(); - Some(word) + let start = self.pos; + self.drain(start..pos, Direction::Forward); + true } else { - None + false } } - pub fn delete_to(&mut self, cs: CharSearch, n: RepeatCount) -> Option<String> { + pub fn delete_to(&mut self, cs: CharSearch, n: RepeatCount) -> bool { let search_result = match cs { - CharSearch::ForwardBefore(c) => self.search_char_pos(&CharSearch::Forward(c), n), - _ => self.search_char_pos(&cs, n), + CharSearch::ForwardBefore(c) => self.search_char_pos(CharSearch::Forward(c), n), + _ => self.search_char_pos(cs, n), }; if let Some(pos) = search_result { - let chunk = match cs { - CharSearch::Backward(_) | - CharSearch::BackwardAfter(_) => { + match cs { + CharSearch::Backward(_) | CharSearch::BackwardAfter(_) => { let end = self.pos; self.pos = pos; - self.buf.drain(pos..end).collect() + self.drain(pos..end, Direction::Backward); + } + CharSearch::ForwardBefore(_) => { + let start = self.pos; + self.drain(start..pos, Direction::Forward); + } + CharSearch::Forward(c) => { + let start = self.pos; + self.drain(start..pos + c.len_utf8(), Direction::Forward); } - CharSearch::ForwardBefore(_) => self.buf.drain(self.pos..pos).collect(), - CharSearch::Forward(c) => self.buf.drain(self.pos..pos + c.len_utf8()).collect(), }; - Some(chunk) + true } else { - None + false } } @@ -510,6 +588,7 @@ impl LineBuffer { .next() .map(|i| i + self.pos) } + /// Alter the next word. pub fn edit_word(&mut self, a: WordAction) -> bool { if let Some(start) = self.skip_whitespace() { @@ -517,7 +596,9 @@ impl LineBuffer { if start == end { return false; } - let word = self.buf.drain(start..end).collect::<String>(); + let word = self + .drain(start..end, Direction::default()) + .collect::<String>(); let result = match a { WordAction::CAPITALIZE => { let ch = (&word).graphemes(true).next().unwrap(); @@ -550,12 +631,14 @@ impl LineBuffer { return false; } - let w1 = self.buf[w1_beg..w1_end].to_string(); + let w1 = self.buf[w1_beg..w1_end].to_owned(); - let w2 = self.buf.drain(w2_beg..w2_end).collect::<String>(); + let w2 = self + .drain(w2_beg..w2_end, Direction::default()) + .collect::<String>(); self.insert_str(w2_beg, &w1); - self.buf.drain(w1_beg..w1_end); + self.drain(w1_beg..w1_end, Direction::default()); self.insert_str(w1_beg, &w2); self.pos = w2_end; @@ -566,99 +649,176 @@ impl LineBuffer { /// and positions the cursor to the end of text. pub fn replace(&mut self, range: Range<usize>, text: &str) { let start = range.start; + for cl in &self.cl { + cl.borrow_mut() + .replace(start, self.buf.index(range.clone()), text); + } self.buf.drain(range); - self.insert_str(start, text); + if start == self.buf.len() { + self.buf.push_str(text); + } else { + self.buf.insert_str(start, text); + } self.pos = start + text.len(); } - fn insert_str(&mut self, idx: usize, s: &str) -> bool { + /// Insert the `s`tring at the specified position. + /// Return `true` if the text has been inserted at the end of the line. + pub fn insert_str(&mut self, idx: usize, s: &str) -> bool { + for cl in &self.cl { + cl.borrow_mut().insert_str(idx, s); + } if idx == self.buf.len() { self.buf.push_str(s); true } else { - insert_str(&mut self.buf, idx, s); + self.buf.insert_str(idx, s); false } } - pub fn copy(&self, mvt: Movement) -> Option<String> { + /// Remove the specified `range` in the line. + pub fn delete_range(&mut self, range: Range<usize>) { + self.set_pos(range.start); + self.drain(range, Direction::default()); + } + + fn drain(&mut self, range: Range<usize>, dir: Direction) -> Drain { + for dl in &self.dl { + let mut lock = dl.try_lock(); + if let Ok(mut dl) = lock { + dl.delete(range.start, &self.buf[range.start..range.end], dir); + } + } + for cl in &self.cl { + cl.borrow_mut() + .delete(range.start, &self.buf[range.start..range.end], dir); + } + self.buf.drain(range) + } + + /// Return the content between current cursor position and `mvt` position. + /// Return `None` when the buffer is empty or when the movement fails. + pub fn copy(&self, mvt: &Movement) -> Option<String> { if self.is_empty() { return None; } - match mvt { + match *mvt { Movement::WholeLine => Some(self.buf.clone()), - Movement::BeginningOfLine => { - if self.pos == 0 { - None - } else { - Some(self.buf[..self.pos].to_string()) - } - } - Movement::ViFirstPrint => { - if self.pos == 0 { - None - } else if let Some(pos) = self.next_word_pos(0, At::Start, Word::Big, 1) { - Some(self.buf[pos..self.pos].to_owned()) - } else { - None - } - } - Movement::EndOfLine => { - if self.pos == self.buf.len() { - None - } else { - Some(self.buf[self.pos..].to_string()) - } - } + Movement::BeginningOfLine => if self.pos == 0 { + None + } else { + Some(self.buf[..self.pos].to_owned()) + }, + Movement::ViFirstPrint => if self.pos == 0 { + None + } else if let Some(pos) = self.next_word_pos(0, At::Start, Word::Big, 1) { + Some(self.buf[pos..self.pos].to_owned()) + } else { + None + }, + Movement::EndOfLine => if self.pos == self.buf.len() { + None + } else { + Some(self.buf[self.pos..].to_owned()) + }, Movement::BackwardWord(n, word_def) => { if let Some(pos) = self.prev_word_pos(self.pos, word_def, n) { - Some(self.buf[pos..self.pos].to_string()) + Some(self.buf[pos..self.pos].to_owned()) } else { None } } Movement::ForwardWord(n, at, word_def) => { if let Some(pos) = self.next_word_pos(self.pos, at, word_def, n) { - Some(self.buf[self.pos..pos].to_string()) + Some(self.buf[self.pos..pos].to_owned()) } else { None } } Movement::ViCharSearch(n, cs) => { let search_result = match cs { - CharSearch::ForwardBefore(c) => { - self.search_char_pos(&CharSearch::Forward(c), n) - } - _ => self.search_char_pos(&cs, n), + CharSearch::ForwardBefore(c) => self.search_char_pos(CharSearch::Forward(c), n), + _ => self.search_char_pos(cs, n), }; if let Some(pos) = search_result { Some(match cs { - CharSearch::Backward(_) | - CharSearch::BackwardAfter(_) => self.buf[pos..self.pos].to_string(), - CharSearch::ForwardBefore(_) => self.buf[self.pos..pos].to_string(), - CharSearch::Forward(c) => { - self.buf[self.pos..pos + c.len_utf8()].to_string() - } - }) + CharSearch::Backward(_) | CharSearch::BackwardAfter(_) => { + self.buf[pos..self.pos].to_owned() + } + CharSearch::ForwardBefore(_) => self.buf[self.pos..pos].to_owned(), + CharSearch::Forward(c) => self.buf[self.pos..pos + c.len_utf8()].to_owned(), + }) } else { None } } - Movement::BackwardChar(n) => { - if let Some(pos) = self.prev_pos(n) { - Some(self.buf[pos..self.pos].to_string()) - } else { - None - } + Movement::BackwardChar(n) => if let Some(pos) = self.prev_pos(n) { + Some(self.buf[pos..self.pos].to_owned()) + } else { + None + }, + Movement::ForwardChar(n) => if let Some(pos) = self.next_pos(n) { + Some(self.buf[self.pos..pos].to_owned()) + } else { + None + }, + } + } + + pub fn kill(&mut self, mvt: &Movement) -> bool { + let notify = match *mvt { + Movement::ForwardChar(_) => false, + Movement::BackwardChar(_) => false, + _ => true, + }; + if notify { + if let Some(dl) = self.dl.as_ref() { + let mut dl = dl.lock().unwrap(); + dl.start_killing() } + } + let killed = match *mvt { Movement::ForwardChar(n) => { - if let Some(pos) = self.next_pos(n) { - Some(self.buf[self.pos..pos].to_string()) - } else { - None - } + // Delete (forward) `n` characters at point. + self.delete(n).is_some() + } + Movement::BackwardChar(n) => { + // Delete `n` characters backward. + self.backspace(n) + } + Movement::EndOfLine => { + // Kill the text from point to the end of the line. + self.kill_line() + } + Movement::WholeLine => { + self.move_home(); + self.kill_line() + } + Movement::BeginningOfLine => { + // Kill backward from point to the beginning of the line. + self.discard_line() + } + Movement::BackwardWord(n, word_def) => { + // kill `n` words backward (until start of word) + self.delete_prev_word(word_def, n) + } + Movement::ForwardWord(n, at, word_def) => { + // kill `n` words forward (until start/end of word) + self.delete_word(at, word_def, n) + } + Movement::ViCharSearch(n, cs) => self.delete_to(cs, n), + Movement::ViFirstPrint => { + false // TODO + } + }; + if notify { + if let Some(dl) = self.dl.as_ref() { + let mut dl = dl.lock().unwrap(); + dl.stop_killing() } } + killed } } @@ -670,73 +830,89 @@ impl Deref for LineBuffer { } } -fn insert_str(buf: &mut String, idx: usize, s: &str) { - use std::ptr; - - let len = buf.len(); - assert!(idx <= len); - assert!(buf.is_char_boundary(idx)); - let amt = s.len(); - buf.reserve(amt); - - unsafe { - let v = buf.as_mut_vec(); - ptr::copy(v.as_ptr().offset(idx as isize), - v.as_mut_ptr().offset((idx + amt) as isize), - len - idx); - ptr::copy_nonoverlapping(s.as_ptr(), v.as_mut_ptr().offset(idx as isize), amt); - v.set_len(len + amt); - } -} - fn is_start_of_word(word_def: Word, previous: &str, grapheme: &str) -> bool { - (!is_word_char(word_def, previous) && is_word_char(word_def, grapheme)) || - (word_def == Word::Vi && !is_other_char(previous) && is_other_char(grapheme)) + (!is_word_char(word_def, previous) && is_word_char(word_def, grapheme)) + || (word_def == Word::Vi && !is_other_char(previous) && is_other_char(grapheme)) } fn is_end_of_word(word_def: Word, grapheme: &str, next: &str) -> bool { - (!is_word_char(word_def, next) && is_word_char(word_def, grapheme)) || - (word_def == Word::Vi && !is_other_char(next) && is_other_char(grapheme)) + (!is_word_char(word_def, next) && is_word_char(word_def, grapheme)) + || (word_def == Word::Vi && !is_other_char(next) && is_other_char(grapheme)) } fn is_word_char(word_def: Word, grapheme: &str) -> bool { match word_def { Word::Emacs => grapheme.chars().all(|c| c.is_alphanumeric()), Word::Vi => is_vi_word_char(grapheme), - Word::Big => !grapheme.chars().all(|c| c.is_whitespace()), + Word::Big => !grapheme.chars().any(|c| c.is_whitespace()), } } fn is_vi_word_char(grapheme: &str) -> bool { grapheme.chars().all(|c| c.is_alphanumeric()) || grapheme == "_" } fn is_other_char(grapheme: &str) -> bool { - !(grapheme.chars().all(|c| c.is_whitespace()) || is_vi_word_char(grapheme)) + !(grapheme.chars().any(|c| c.is_whitespace()) || is_vi_word_char(grapheme)) } #[cfg(test)] mod test { + use super::{ChangeListener, DeleteListener, Direction, LineBuffer, WordAction, MAX_LINE}; use keymap::{At, CharSearch, Word}; - use super::{LineBuffer, MAX_LINE, WordAction}; + use std::cell::RefCell; + use std::rc::Rc; + + struct Listener { + deleted_str: Option<String>, + } + + impl Listener { + fn new() -> Rc<RefCell<Listener>> { + let l = Listener { deleted_str: None }; + Rc::new(RefCell::new(l)) + } + + fn assert_deleted_str_eq(&self, expected: &str) { + let actual = self.deleted_str.as_ref().expect("no deleted string"); + assert_eq!(expected, actual) + } + } + + impl DeleteListener for Listener { + fn start_killing(&mut self) {} + + fn delete(&mut self, _: usize, string: &str, _: Direction) { + self.deleted_str = Some(string.to_owned()); + } + + fn stop_killing(&mut self) {} + } + impl ChangeListener for Listener { + fn insert_char(&mut self, _: usize, _: char) {} + + fn insert_str(&mut self, _: usize, _: &str) {} + + fn replace(&mut self, _: usize, _: &str, _: &str) {} + } #[test] fn next_pos() { - let s = LineBuffer::init("ö̲g̈", 0); + let s = LineBuffer::init("ö̲g̈", 0, None); assert_eq!(7, s.len()); let pos = s.next_pos(1); assert_eq!(Some(4), pos); - let s = LineBuffer::init("ö̲g̈", 4); + let s = LineBuffer::init("ö̲g̈", 4, None); let pos = s.next_pos(1); assert_eq!(Some(7), pos); } #[test] fn prev_pos() { - let s = LineBuffer::init("ö̲g̈", 4); + let s = LineBuffer::init("ö̲g̈", 4, None); assert_eq!(7, s.len()); let pos = s.prev_pos(1); assert_eq!(Some(0), pos); - let s = LineBuffer::init("ö̲g̈", 7); + let s = LineBuffer::init("ö̲g̈", 7, None); let pos = s.prev_pos(1); assert_eq!(Some(4), pos); } @@ -763,7 +939,7 @@ mod test { #[test] fn yank_after() { - let mut s = LineBuffer::init("αß", 2); + let mut s = LineBuffer::init("αß", 2, None); s.move_forward(1); let ok = s.yank("γδε", 1); assert_eq!(Some(true), ok); @@ -773,7 +949,7 @@ mod test { #[test] fn yank_before() { - let mut s = LineBuffer::init("αε", 2); + let mut s = LineBuffer::init("αε", 2, None); let ok = s.yank("ßγδ", 1); assert_eq!(Some(false), ok); assert_eq!("αßγδε", s.buf); @@ -782,7 +958,7 @@ mod test { #[test] fn moves() { - let mut s = LineBuffer::init("αß", 4); + let mut s = LineBuffer::init("αß", 4, None); let ok = s.move_backward(1); assert_eq!("αß", s.buf); assert_eq!(2, s.pos); @@ -806,7 +982,7 @@ mod test { #[test] fn move_grapheme() { - let mut s = LineBuffer::init("ag̈", 4); + let mut s = LineBuffer::init("ag̈", 4, None); assert_eq!(4, s.len()); let ok = s.move_backward(1); assert_eq!(true, ok); @@ -819,36 +995,41 @@ mod test { #[test] fn delete() { - let mut s = LineBuffer::init("αß", 2); + let cl = Listener::new(); + let mut s = LineBuffer::init("αß", 2, Some(cl.clone())); let chars = s.delete(1); assert_eq!("α", s.buf); assert_eq!(2, s.pos); - assert_eq!(Some("ß".to_string()), chars); + assert_eq!(Some("ß".to_owned()), chars); - let chars = s.backspace(1); + let ok = s.backspace(1); assert_eq!("", s.buf); assert_eq!(0, s.pos); - assert_eq!(Some("α".to_string()), chars); + assert_eq!(true, ok); + cl.borrow().assert_deleted_str_eq("α"); } #[test] fn kill() { - let mut s = LineBuffer::init("αßγδε", 6); - let text = s.kill_line(); + let cl = Listener::new(); + let mut s = LineBuffer::init("αßγδε", 6, Some(cl.clone())); + let ok = s.kill_line(); assert_eq!("αßγ", s.buf); assert_eq!(6, s.pos); - assert_eq!(Some("δε".to_string()), text); + assert_eq!(true, ok); + cl.borrow().assert_deleted_str_eq("δε"); s.pos = 4; - let text = s.discard_line(); + let ok = s.discard_line(); assert_eq!("γ", s.buf); assert_eq!(0, s.pos); - assert_eq!(Some("αß".to_string()), text); + assert_eq!(true, ok); + cl.borrow().assert_deleted_str_eq("αß"); } #[test] fn transpose() { - let mut s = LineBuffer::init("aßc", 1); + let mut s = LineBuffer::init("aßc", 1, None); let ok = s.transpose_chars(); assert_eq!("ßac", s.buf); assert_eq!(3, s.pos); @@ -871,16 +1052,26 @@ mod test { #[test] fn move_to_prev_word() { - let mut s = LineBuffer::init("a ß c", 6); + let mut s = LineBuffer::init("a ß c", 6, None); // before 'c' let ok = s.move_to_prev_word(Word::Emacs, 1); assert_eq!("a ß c", s.buf); - assert_eq!(2, s.pos); - assert_eq!(true, ok); + assert_eq!(2, s.pos); // before 'ß' + assert!(true, ok); + + assert!(s.move_end()); // after 'c' + assert_eq!(7, s.pos); + let ok = s.move_to_prev_word(Word::Emacs, 1); + assert!(true, ok); + assert_eq!(6, s.pos); // before 'c' + + let ok = s.move_to_prev_word(Word::Emacs, 2); + assert!(true, ok); + assert_eq!(0, s.pos); } #[test] fn move_to_prev_vi_word() { - let mut s = LineBuffer::init("alpha ,beta/rho; mu", 19); + let mut s = LineBuffer::init("alpha ,beta/rho; mu", 19, None); let ok = s.move_to_prev_word(Word::Vi, 1); assert!(ok); assert_eq!(17, s.pos); @@ -908,7 +1099,7 @@ mod test { #[test] fn move_to_prev_big_word() { - let mut s = LineBuffer::init("alpha ,beta/rho; mu", 19); + let mut s = LineBuffer::init("alpha ,beta/rho; mu", 19, None); let ok = s.move_to_prev_word(Word::Big, 1); assert!(ok); assert_eq!(17, s.pos); @@ -924,17 +1115,17 @@ mod test { #[test] fn move_to_forward() { - let mut s = LineBuffer::init("αßγδε", 2); + let mut s = LineBuffer::init("αßγδε", 2, None); let ok = s.move_to(CharSearch::ForwardBefore('ε'), 1); assert_eq!(true, ok); assert_eq!(6, s.pos); - let mut s = LineBuffer::init("αßγδε", 2); + let mut s = LineBuffer::init("αßγδε", 2, None); let ok = s.move_to(CharSearch::Forward('ε'), 1); assert_eq!(true, ok); assert_eq!(8, s.pos); - let mut s = LineBuffer::init("αßγδε", 2); + let mut s = LineBuffer::init("αßγδε", 2, None); let ok = s.move_to(CharSearch::Forward('ε'), 10); assert_eq!(true, ok); assert_eq!(8, s.pos); @@ -942,12 +1133,12 @@ mod test { #[test] fn move_to_backward() { - let mut s = LineBuffer::init("αßγδε", 8); + let mut s = LineBuffer::init("αßγδε", 8, None); let ok = s.move_to(CharSearch::BackwardAfter('ß'), 1); assert_eq!(true, ok); assert_eq!(4, s.pos); - let mut s = LineBuffer::init("αßγδε", 8); + let mut s = LineBuffer::init("αßγδε", 8, None); let ok = s.move_to(CharSearch::Backward('ß'), 1); assert_eq!(true, ok); assert_eq!(2, s.pos); @@ -955,25 +1146,40 @@ mod test { #[test] fn delete_prev_word() { - let mut s = LineBuffer::init("a ß c", 6); - let text = s.delete_prev_word(Word::Big, 1); + let cl = Listener::new(); + let mut s = LineBuffer::init("a ß c", 6, Some(cl.clone())); + let ok = s.delete_prev_word(Word::Big, 1); assert_eq!("a c", s.buf); assert_eq!(2, s.pos); - assert_eq!(Some("ß ".to_string()), text); + assert_eq!(true, ok); + cl.borrow().assert_deleted_str_eq("ß "); } #[test] fn move_to_next_word() { - let mut s = LineBuffer::init("a ß c", 1); + let mut s = LineBuffer::init("a ß c", 1, None); // after 'a' let ok = s.move_to_next_word(At::AfterEnd, Word::Emacs, 1); assert_eq!("a ß c", s.buf); - assert_eq!(4, s.pos); assert_eq!(true, ok); + assert_eq!(4, s.pos); // after 'ß' + + let ok = s.move_to_next_word(At::AfterEnd, Word::Emacs, 1); + assert_eq!(true, ok); + assert_eq!(7, s.pos); // after 'c' + + s.move_home(); + let ok = s.move_to_next_word(At::AfterEnd, Word::Emacs, 1); + assert_eq!(true, ok); + assert_eq!(1, s.pos); // after 'a' + + let ok = s.move_to_next_word(At::AfterEnd, Word::Emacs, 2); + assert_eq!(true, ok); + assert_eq!(7, s.pos); // after 'c' } #[test] fn move_to_end_of_word() { - let mut s = LineBuffer::init("a ßeta c", 1); + let mut s = LineBuffer::init("a ßeta c", 1, None); let ok = s.move_to_next_word(At::BeforeEnd, Word::Vi, 1); assert_eq!("a ßeta c", s.buf); assert_eq!(6, s.pos); @@ -982,7 +1188,7 @@ mod test { #[test] fn move_to_end_of_vi_word() { - let mut s = LineBuffer::init("alpha ,beta/rho; mu", 0); + let mut s = LineBuffer::init("alpha ,beta/rho; mu", 0, None); let ok = s.move_to_next_word(At::BeforeEnd, Word::Vi, 1); assert!(ok); assert_eq!(4, s.pos); @@ -1010,7 +1216,7 @@ mod test { #[test] fn move_to_end_of_big_word() { - let mut s = LineBuffer::init("alpha ,beta/rho; mu", 0); + let mut s = LineBuffer::init("alpha ,beta/rho; mu", 0, None); let ok = s.move_to_next_word(At::BeforeEnd, Word::Big, 1); assert!(ok); assert_eq!(4, s.pos); @@ -1026,7 +1232,7 @@ mod test { #[test] fn move_to_start_of_word() { - let mut s = LineBuffer::init("a ß c", 2); + let mut s = LineBuffer::init("a ß c", 2, None); let ok = s.move_to_next_word(At::Start, Word::Emacs, 1); assert_eq!("a ß c", s.buf); assert_eq!(6, s.pos); @@ -1035,7 +1241,7 @@ mod test { #[test] fn move_to_start_of_vi_word() { - let mut s = LineBuffer::init("alpha ,beta/rho; mu", 0); + let mut s = LineBuffer::init("alpha ,beta/rho; mu", 0, None); let ok = s.move_to_next_word(At::Start, Word::Vi, 1); assert!(ok); assert_eq!(6, s.pos); @@ -1063,7 +1269,7 @@ mod test { #[test] fn move_to_start_of_big_word() { - let mut s = LineBuffer::init("alpha ,beta/rho; mu", 0); + let mut s = LineBuffer::init("alpha ,beta/rho; mu", 0, None); let ok = s.move_to_next_word(At::Start, Word::Big, 1); assert!(ok); assert_eq!(6, s.pos); @@ -1079,76 +1285,87 @@ mod test { #[test] fn delete_word() { - let mut s = LineBuffer::init("a ß c", 1); - let text = s.delete_word(At::AfterEnd, Word::Emacs, 1); + let cl = Listener::new(); + let mut s = LineBuffer::init("a ß c", 1, Some(cl.clone())); + let ok = s.delete_word(At::AfterEnd, Word::Emacs, 1); assert_eq!("a c", s.buf); assert_eq!(1, s.pos); - assert_eq!(Some(" ß".to_string()), text); + assert_eq!(true, ok); + cl.borrow().assert_deleted_str_eq(" ß"); - let mut s = LineBuffer::init("test", 0); - let text = s.delete_word(At::AfterEnd, Word::Vi, 1); + let mut s = LineBuffer::init("test", 0, Some(cl.clone())); + let ok = s.delete_word(At::AfterEnd, Word::Vi, 1); assert_eq!("", s.buf); assert_eq!(0, s.pos); - assert_eq!(Some("test".to_string()), text); + assert_eq!(true, ok); + cl.borrow().assert_deleted_str_eq("test"); } #[test] fn delete_til_start_of_word() { - let mut s = LineBuffer::init("a ß c", 2); - let text = s.delete_word(At::Start, Word::Emacs, 1); + let cl = Listener::new(); + let mut s = LineBuffer::init("a ß c", 2, Some(cl.clone())); + let ok = s.delete_word(At::Start, Word::Emacs, 1); assert_eq!("a c", s.buf); assert_eq!(2, s.pos); - assert_eq!(Some("ß ".to_string()), text); + assert_eq!(true, ok); + cl.borrow().assert_deleted_str_eq("ß "); } #[test] fn delete_to_forward() { - let mut s = LineBuffer::init("αßγδε", 2); - let text = s.delete_to(CharSearch::ForwardBefore('ε'), 1); - assert_eq!(Some("ßγδ".to_string()), text); + let cl = Listener::new(); + let mut s = LineBuffer::init("αßγδε", 2, Some(cl.clone())); + let ok = s.delete_to(CharSearch::ForwardBefore('ε'), 1); + assert_eq!(true, ok); + cl.borrow().assert_deleted_str_eq("ßγδ"); assert_eq!("αε", s.buf); assert_eq!(2, s.pos); - let mut s = LineBuffer::init("αßγδε", 2); - let text = s.delete_to(CharSearch::Forward('ε'), 1); - assert_eq!(Some("ßγδε".to_string()), text); + let mut s = LineBuffer::init("αßγδε", 2, Some(cl.clone())); + let ok = s.delete_to(CharSearch::Forward('ε'), 1); + assert_eq!(true, ok); + cl.borrow().assert_deleted_str_eq("ßγδε"); assert_eq!("α", s.buf); assert_eq!(2, s.pos); } #[test] fn delete_to_backward() { - let mut s = LineBuffer::init("αßγδε", 8); - let text = s.delete_to(CharSearch::BackwardAfter('α'), 1); - assert_eq!(Some("ßγδ".to_string()), text); + let cl = Listener::new(); + let mut s = LineBuffer::init("αßγδε", 8, Some(cl.clone())); + let ok = s.delete_to(CharSearch::BackwardAfter('α'), 1); + assert_eq!(true, ok); + cl.borrow().assert_deleted_str_eq("ßγδ"); assert_eq!("αε", s.buf); assert_eq!(2, s.pos); - let mut s = LineBuffer::init("αßγδε", 8); - let text = s.delete_to(CharSearch::Backward('ß'), 1); - assert_eq!(Some("ßγδ".to_string()), text); + let mut s = LineBuffer::init("αßγδε", 8, Some(cl.clone())); + let ok = s.delete_to(CharSearch::Backward('ß'), 1); + assert_eq!(true, ok); + cl.borrow().assert_deleted_str_eq("ßγδ"); assert_eq!("αε", s.buf); assert_eq!(2, s.pos); } #[test] fn edit_word() { - let mut s = LineBuffer::init("a ßeta c", 1); + let mut s = LineBuffer::init("a ßeta c", 1, None); assert!(s.edit_word(WordAction::UPPERCASE)); assert_eq!("a SSETA c", s.buf); assert_eq!(7, s.pos); - let mut s = LineBuffer::init("a ßetA c", 1); + let mut s = LineBuffer::init("a ßetA c", 1, None); assert!(s.edit_word(WordAction::LOWERCASE)); assert_eq!("a ßeta c", s.buf); assert_eq!(7, s.pos); - let mut s = LineBuffer::init("a ßETA c", 1); + let mut s = LineBuffer::init("a ßETA c", 1, None); assert!(s.edit_word(WordAction::CAPITALIZE)); assert_eq!("a SSeta c", s.buf); assert_eq!(7, s.pos); - let mut s = LineBuffer::init("test", 1); + let mut s = LineBuffer::init("test", 1, None); assert!(s.edit_word(WordAction::CAPITALIZE)); assert_eq!("tEst", s.buf); assert_eq!(4, s.pos); @@ -1156,20 +1373,20 @@ mod test { #[test] fn transpose_words() { - let mut s = LineBuffer::init("ßeta / δelta__", 15); + let mut s = LineBuffer::init("ßeta / δelta__", 15, None); assert!(s.transpose_words(1)); assert_eq!("δelta__ / ßeta", s.buf); assert_eq!(16, s.pos); - let mut s = LineBuffer::init("ßeta / δelta", 14); + let mut s = LineBuffer::init("ßeta / δelta", 14, None); assert!(s.transpose_words(1)); assert_eq!("δelta / ßeta", s.buf); assert_eq!(14, s.pos); - let mut s = LineBuffer::init(" / δelta", 8); + let mut s = LineBuffer::init(" / δelta", 8, None); assert!(!s.transpose_words(1)); - let mut s = LineBuffer::init("ßeta / __", 9); + let mut s = LineBuffer::init("ßeta / __", 9, None); assert!(!s.transpose_words(1)); } } diff --git a/src/test/common.rs b/src/test/common.rs new file mode 100644 index 0000000000000000000000000000000000000000..7fe8b173487bc8e672d75e8c7360bc9b44107c80 --- /dev/null +++ b/src/test/common.rs @@ -0,0 +1,401 @@ +///! Basic commands tests. +use super::{assert_cursor, assert_line, assert_line_with_initial, init_editor}; +use config::EditMode; +use consts::KeyPress; +use error::ReadlineError; + +#[test] +fn home_key() { + for mode in &[EditMode::Emacs, EditMode::Vi] { + assert_cursor( + *mode, + ("", ""), + &[KeyPress::Home, KeyPress::Enter], + ("", ""), + ); + assert_cursor( + *mode, + ("Hi", ""), + &[KeyPress::Home, KeyPress::Enter], + ("", "Hi"), + ); + if *mode == EditMode::Vi { + // vi command mode + assert_cursor( + *mode, + ("Hi", ""), + &[KeyPress::Esc, KeyPress::Home, KeyPress::Enter], + ("", "Hi"), + ); + } + } +} + +#[test] +fn end_key() { + for mode in &[EditMode::Emacs, EditMode::Vi] { + assert_cursor(*mode, ("", ""), &[KeyPress::End, KeyPress::Enter], ("", "")); + assert_cursor( + *mode, + ("H", "i"), + &[KeyPress::End, KeyPress::Enter], + ("Hi", ""), + ); + assert_cursor( + *mode, + ("", "Hi"), + &[KeyPress::End, KeyPress::Enter], + ("Hi", ""), + ); + if *mode == EditMode::Vi { + // vi command mode + assert_cursor( + *mode, + ("", "Hi"), + &[KeyPress::Esc, KeyPress::End, KeyPress::Enter], + ("Hi", ""), + ); + } + } +} + +#[test] +fn left_key() { + for mode in &[EditMode::Emacs, EditMode::Vi] { + assert_cursor( + *mode, + ("Hi", ""), + &[KeyPress::Left, KeyPress::Enter], + ("H", "i"), + ); + assert_cursor( + *mode, + ("H", "i"), + &[KeyPress::Left, KeyPress::Enter], + ("", "Hi"), + ); + assert_cursor( + *mode, + ("", "Hi"), + &[KeyPress::Left, KeyPress::Enter], + ("", "Hi"), + ); + if *mode == EditMode::Vi { + // vi command mode + assert_cursor( + *mode, + ("Bye", ""), + &[KeyPress::Esc, KeyPress::Left, KeyPress::Enter], + ("B", "ye"), + ); + } + } +} + +#[test] +fn right_key() { + for mode in &[EditMode::Emacs, EditMode::Vi] { + assert_cursor( + *mode, + ("", ""), + &[KeyPress::Right, KeyPress::Enter], + ("", ""), + ); + assert_cursor( + *mode, + ("", "Hi"), + &[KeyPress::Right, KeyPress::Enter], + ("H", "i"), + ); + assert_cursor( + *mode, + ("B", "ye"), + &[KeyPress::Right, KeyPress::Enter], + ("By", "e"), + ); + assert_cursor( + *mode, + ("H", "i"), + &[KeyPress::Right, KeyPress::Enter], + ("Hi", ""), + ); + if *mode == EditMode::Vi { + // vi command mode + assert_cursor( + *mode, + ("", "Hi"), + &[KeyPress::Esc, KeyPress::Right, KeyPress::Enter], + ("H", "i"), + ); + } + } +} + +#[test] +fn enter_key() { + for mode in &[EditMode::Emacs, EditMode::Vi] { + assert_line(*mode, &[KeyPress::Enter], ""); + assert_line(*mode, &[KeyPress::Char('a'), KeyPress::Enter], "a"); + assert_line_with_initial(*mode, ("Hi", ""), &[KeyPress::Enter], "Hi"); + assert_line_with_initial(*mode, ("", "Hi"), &[KeyPress::Enter], "Hi"); + assert_line_with_initial(*mode, ("H", "i"), &[KeyPress::Enter], "Hi"); + if *mode == EditMode::Vi { + // vi command mode + assert_line(*mode, &[KeyPress::Esc, KeyPress::Enter], ""); + assert_line( + *mode, + &[KeyPress::Char('a'), KeyPress::Esc, KeyPress::Enter], + "a", + ); + assert_line_with_initial(*mode, ("Hi", ""), &[KeyPress::Esc, KeyPress::Enter], "Hi"); + assert_line_with_initial(*mode, ("", "Hi"), &[KeyPress::Esc, KeyPress::Enter], "Hi"); + assert_line_with_initial(*mode, ("H", "i"), &[KeyPress::Esc, KeyPress::Enter], "Hi"); + } + } +} + +#[test] +fn newline_key() { + for mode in &[EditMode::Emacs, EditMode::Vi] { + assert_line(*mode, &[KeyPress::Ctrl('J')], ""); + assert_line(*mode, &[KeyPress::Char('a'), KeyPress::Ctrl('J')], "a"); + if *mode == EditMode::Vi { + // vi command mode + assert_line(*mode, &[KeyPress::Esc, KeyPress::Ctrl('J')], ""); + assert_line( + *mode, + &[KeyPress::Char('a'), KeyPress::Esc, KeyPress::Ctrl('J')], + "a", + ); + } + } +} + +#[test] +fn eof_key() { + for mode in &[EditMode::Emacs, EditMode::Vi] { + let mut editor = init_editor(*mode, &[KeyPress::Ctrl('D')]); + let err = editor.readline(">>"); + assert_matches!(err, Err(ReadlineError::Eof)); + } + assert_line( + EditMode::Emacs, + &[KeyPress::Char('a'), KeyPress::Ctrl('D'), KeyPress::Enter], + "a", + ); + assert_line( + EditMode::Vi, + &[KeyPress::Char('a'), KeyPress::Ctrl('D')], + "a", + ); + assert_line( + EditMode::Vi, + &[KeyPress::Char('a'), KeyPress::Esc, KeyPress::Ctrl('D')], + "a", + ); + assert_line_with_initial( + EditMode::Emacs, + ("", "Hi"), + &[KeyPress::Ctrl('D'), KeyPress::Enter], + "i", + ); + assert_line_with_initial(EditMode::Vi, ("", "Hi"), &[KeyPress::Ctrl('D')], "Hi"); + assert_line_with_initial( + EditMode::Vi, + ("", "Hi"), + &[KeyPress::Esc, KeyPress::Ctrl('D')], + "Hi", + ); +} + +#[test] +fn interrupt_key() { + for mode in &[EditMode::Emacs, EditMode::Vi] { + let mut editor = init_editor(*mode, &[KeyPress::Ctrl('C')]); + let err = editor.readline(">>"); + assert_matches!(err, Err(ReadlineError::Interrupted)); + + let mut editor = init_editor(*mode, &[KeyPress::Ctrl('C')]); + let err = editor.readline_with_initial(">>", ("Hi", "")); + assert_matches!(err, Err(ReadlineError::Interrupted)); + if *mode == EditMode::Vi { + // vi command mode + let mut editor = init_editor(*mode, &[KeyPress::Esc, KeyPress::Ctrl('C')]); + let err = editor.readline_with_initial(">>", ("Hi", "")); + assert_matches!(err, Err(ReadlineError::Interrupted)); + } + } +} + +#[test] +fn delete_key() { + for mode in &[EditMode::Emacs, EditMode::Vi] { + assert_cursor( + *mode, + ("a", ""), + &[KeyPress::Delete, KeyPress::Enter], + ("a", ""), + ); + assert_cursor( + *mode, + ("", "a"), + &[KeyPress::Delete, KeyPress::Enter], + ("", ""), + ); + if *mode == EditMode::Vi { + // vi command mode + assert_cursor( + *mode, + ("", "a"), + &[KeyPress::Esc, KeyPress::Delete, KeyPress::Enter], + ("", ""), + ); + } + } +} + +#[test] +fn ctrl_t() { + for mode in &[EditMode::Emacs, EditMode::Vi] { + assert_cursor( + *mode, + ("a", "b"), + &[KeyPress::Ctrl('T'), KeyPress::Enter], + ("ba", ""), + ); + assert_cursor( + *mode, + ("ab", "cd"), + &[KeyPress::Ctrl('T'), KeyPress::Enter], + ("acb", "d"), + ); + if *mode == EditMode::Vi { + // vi command mode + assert_cursor( + *mode, + ("ab", ""), + &[KeyPress::Esc, KeyPress::Ctrl('T'), KeyPress::Enter], + ("ba", ""), + ); + } + } +} + +#[test] +fn ctrl_u() { + for mode in &[EditMode::Emacs, EditMode::Vi] { + assert_cursor( + *mode, + ("start of line ", "end"), + &[KeyPress::Ctrl('U'), KeyPress::Enter], + ("", "end"), + ); + assert_cursor( + *mode, + ("", "end"), + &[KeyPress::Ctrl('U'), KeyPress::Enter], + ("", "end"), + ); + if *mode == EditMode::Vi { + // vi command mode + assert_cursor( + *mode, + ("start of line ", "end"), + &[KeyPress::Esc, KeyPress::Ctrl('U'), KeyPress::Enter], + ("", " end"), + ); + } + } +} + +#[cfg(unix)] +#[test] +fn ctrl_v() { + for mode in &[EditMode::Emacs, EditMode::Vi] { + assert_cursor( + *mode, + ("", ""), + &[KeyPress::Ctrl('V'), KeyPress::Char('\t'), KeyPress::Enter], + ("\t", ""), + ); + if *mode == EditMode::Vi { + // vi command mode + assert_cursor( + *mode, + ("", ""), + &[ + KeyPress::Esc, + KeyPress::Ctrl('V'), + KeyPress::Char('\t'), + KeyPress::Enter, + ], + ("\t", ""), + ); + } + } +} + +#[test] +fn ctrl_w() { + for mode in &[EditMode::Emacs, EditMode::Vi] { + assert_cursor( + *mode, + ("Hello, ", "world"), + &[KeyPress::Ctrl('W'), KeyPress::Enter], + ("", "world"), + ); + assert_cursor( + *mode, + ("Hello, world.", ""), + &[KeyPress::Ctrl('W'), KeyPress::Enter], + ("Hello, ", ""), + ); + if *mode == EditMode::Vi { + // vi command mode + assert_cursor( + *mode, + ("Hello, world.", ""), + &[KeyPress::Esc, KeyPress::Ctrl('W'), KeyPress::Enter], + ("Hello, ", "."), + ); + } + } +} + +#[test] +fn ctrl_y() { + for mode in &[EditMode::Emacs /* FIXME, EditMode::Vi */] { + assert_cursor( + *mode, + ("Hello, ", "world"), + &[KeyPress::Ctrl('W'), KeyPress::Ctrl('Y'), KeyPress::Enter], + ("Hello, ", "world"), + ); + } +} + +#[test] +fn ctrl__() { + for mode in &[EditMode::Emacs, EditMode::Vi] { + assert_cursor( + *mode, + ("Hello, ", "world"), + &[KeyPress::Ctrl('W'), KeyPress::Ctrl('_'), KeyPress::Enter], + ("Hello, ", "world"), + ); + if *mode == EditMode::Vi { + // vi command mode + assert_cursor( + *mode, + ("Hello, ", "world"), + &[ + KeyPress::Esc, + KeyPress::Ctrl('W'), + KeyPress::Ctrl('_'), + KeyPress::Enter, + ], + ("Hello,", " world"), + ); + } + } +} diff --git a/src/test/emacs.rs b/src/test/emacs.rs new file mode 100644 index 0000000000000000000000000000000000000000..67a1b10e93fe5a8e90be074d1717e739ae982a8b --- /dev/null +++ b/src/test/emacs.rs @@ -0,0 +1,378 @@ +//! Emacs specific key bindings +use super::{assert_cursor, assert_history}; +use config::EditMode; +use consts::KeyPress; + +#[test] +fn ctrl_a() { + assert_cursor( + EditMode::Emacs, + ("Hi", ""), + &[KeyPress::Ctrl('A'), KeyPress::Enter], + ("", "Hi"), + ); +} + +#[test] +fn ctrl_e() { + assert_cursor( + EditMode::Emacs, + ("", "Hi"), + &[KeyPress::Ctrl('E'), KeyPress::Enter], + ("Hi", ""), + ); +} + +#[test] +fn ctrl_b() { + assert_cursor( + EditMode::Emacs, + ("Hi", ""), + &[KeyPress::Ctrl('B'), KeyPress::Enter], + ("H", "i"), + ); + assert_cursor( + EditMode::Emacs, + ("Hi", ""), + &[KeyPress::Meta('2'), KeyPress::Ctrl('B'), KeyPress::Enter], + ("", "Hi"), + ); + assert_cursor( + EditMode::Emacs, + ("", "Hi"), + &[ + KeyPress::Meta('-'), + KeyPress::Meta('2'), + KeyPress::Ctrl('B'), + KeyPress::Enter, + ], + ("Hi", ""), + ); +} + +#[test] +fn ctrl_f() { + assert_cursor( + EditMode::Emacs, + ("", "Hi"), + &[KeyPress::Ctrl('F'), KeyPress::Enter], + ("H", "i"), + ); + assert_cursor( + EditMode::Emacs, + ("", "Hi"), + &[KeyPress::Meta('2'), KeyPress::Ctrl('F'), KeyPress::Enter], + ("Hi", ""), + ); + assert_cursor( + EditMode::Emacs, + ("Hi", ""), + &[ + KeyPress::Meta('-'), + KeyPress::Meta('2'), + KeyPress::Ctrl('F'), + KeyPress::Enter, + ], + ("", "Hi"), + ); +} + +#[test] +fn ctrl_h() { + assert_cursor( + EditMode::Emacs, + ("Hi", ""), + &[KeyPress::Ctrl('H'), KeyPress::Enter], + ("H", ""), + ); + assert_cursor( + EditMode::Emacs, + ("Hi", ""), + &[KeyPress::Meta('2'), KeyPress::Ctrl('H'), KeyPress::Enter], + ("", ""), + ); + assert_cursor( + EditMode::Emacs, + ("", "Hi"), + &[ + KeyPress::Meta('-'), + KeyPress::Meta('2'), + KeyPress::Ctrl('H'), + KeyPress::Enter, + ], + ("", ""), + ); +} + +#[test] +fn backspace() { + assert_cursor( + EditMode::Emacs, + ("", ""), + &[KeyPress::Backspace, KeyPress::Enter], + ("", ""), + ); + assert_cursor( + EditMode::Emacs, + ("Hi", ""), + &[KeyPress::Backspace, KeyPress::Enter], + ("H", ""), + ); + assert_cursor( + EditMode::Emacs, + ("", "Hi"), + &[KeyPress::Backspace, KeyPress::Enter], + ("", "Hi"), + ); +} + +#[test] +fn ctrl_k() { + assert_cursor( + EditMode::Emacs, + ("Hi", ""), + &[KeyPress::Ctrl('K'), KeyPress::Enter], + ("Hi", ""), + ); + assert_cursor( + EditMode::Emacs, + ("", "Hi"), + &[KeyPress::Ctrl('K'), KeyPress::Enter], + ("", ""), + ); + assert_cursor( + EditMode::Emacs, + ("B", "ye"), + &[KeyPress::Ctrl('K'), KeyPress::Enter], + ("B", ""), + ); +} + +#[test] +fn ctrl_n() { + assert_history( + EditMode::Emacs, + &["line1", "line2"], + &[ + KeyPress::Ctrl('P'), + KeyPress::Ctrl('P'), + KeyPress::Ctrl('N'), + KeyPress::Enter, + ], + ("line2", ""), + ); +} + +#[test] +fn ctrl_p() { + assert_history( + EditMode::Emacs, + &["line1"], + &[KeyPress::Ctrl('P'), KeyPress::Enter], + ("line1", ""), + ); +} + +#[test] +fn ctrl_t() { + /* FIXME + assert_cursor( + ("ab", "cd"), + &[KeyPress::Meta('2'), KeyPress::Ctrl('T'), KeyPress::Enter], + ("acdb", ""), + );*/ +} + +#[test] +fn ctrl_x_ctrl_u() { + assert_cursor( + EditMode::Emacs, + ("Hello, ", "world"), + &[ + KeyPress::Ctrl('W'), + KeyPress::Ctrl('X'), + KeyPress::Ctrl('U'), + KeyPress::Enter, + ], + ("Hello, ", "world"), + ); +} + +#[test] +fn meta_b() { + assert_cursor( + EditMode::Emacs, + ("Hello, world!", ""), + &[KeyPress::Meta('B'), KeyPress::Enter], + ("Hello, ", "world!"), + ); + assert_cursor( + EditMode::Emacs, + ("Hello, world!", ""), + &[KeyPress::Meta('2'), KeyPress::Meta('B'), KeyPress::Enter], + ("", "Hello, world!"), + ); + assert_cursor( + EditMode::Emacs, + ("", "Hello, world!"), + &[KeyPress::Meta('-'), KeyPress::Meta('B'), KeyPress::Enter], + ("Hello", ", world!"), + ); +} + +#[test] +fn meta_f() { + assert_cursor( + EditMode::Emacs, + ("", "Hello, world!"), + &[KeyPress::Meta('F'), KeyPress::Enter], + ("Hello", ", world!"), + ); + assert_cursor( + EditMode::Emacs, + ("", "Hello, world!"), + &[KeyPress::Meta('2'), KeyPress::Meta('F'), KeyPress::Enter], + ("Hello, world", "!"), + ); + assert_cursor( + EditMode::Emacs, + ("Hello, world!", ""), + &[KeyPress::Meta('-'), KeyPress::Meta('F'), KeyPress::Enter], + ("Hello, ", "world!"), + ); +} + +#[test] +fn meta_c() { + assert_cursor( + EditMode::Emacs, + ("hi", ""), + &[KeyPress::Meta('C'), KeyPress::Enter], + ("hi", ""), + ); + assert_cursor( + EditMode::Emacs, + ("", "hi"), + &[KeyPress::Meta('C'), KeyPress::Enter], + ("Hi", ""), + ); + /* FIXME + assert_cursor( + ("", "hi test"), + &[KeyPress::Meta('2'), KeyPress::Meta('C'), KeyPress::Enter], + ("Hi Test", ""), + );*/ +} + +#[test] +fn meta_l() { + assert_cursor( + EditMode::Emacs, + ("Hi", ""), + &[KeyPress::Meta('L'), KeyPress::Enter], + ("Hi", ""), + ); + assert_cursor( + EditMode::Emacs, + ("", "HI"), + &[KeyPress::Meta('L'), KeyPress::Enter], + ("hi", ""), + ); + /* FIXME + assert_cursor( + ("", "HI TEST"), + &[KeyPress::Meta('2'), KeyPress::Meta('L'), KeyPress::Enter], + ("hi test", ""), + );*/ +} + +#[test] +fn meta_u() { + assert_cursor( + EditMode::Emacs, + ("hi", ""), + &[KeyPress::Meta('U'), KeyPress::Enter], + ("hi", ""), + ); + assert_cursor( + EditMode::Emacs, + ("", "hi"), + &[KeyPress::Meta('U'), KeyPress::Enter], + ("HI", ""), + ); + /* FIXME + assert_cursor( + ("", "hi test"), + &[KeyPress::Meta('2'), KeyPress::Meta('U'), KeyPress::Enter], + ("HI TEST", ""), + );*/ +} + +#[test] +fn meta_d() { + assert_cursor( + EditMode::Emacs, + ("Hello", ", world!"), + &[KeyPress::Meta('D'), KeyPress::Enter], + ("Hello", "!"), + ); + assert_cursor( + EditMode::Emacs, + ("Hello", ", world!"), + &[KeyPress::Meta('2'), KeyPress::Meta('D'), KeyPress::Enter], + ("Hello", ""), + ); +} + +#[test] +fn meta_t() { + assert_cursor( + EditMode::Emacs, + ("Hello", ", world!"), + &[KeyPress::Meta('T'), KeyPress::Enter], + ("world, Hello", "!"), + ); + /* FIXME + assert_cursor( + ("One Two", " Three Four"), + &[KeyPress::Meta('T'), KeyPress::Enter], + ("One Four Three Two", ""), + );*/ +} + +#[test] +fn meta_y() { + assert_cursor( + EditMode::Emacs, + ("Hello, world", "!"), + &[ + KeyPress::Ctrl('W'), + KeyPress::Left, + KeyPress::Ctrl('W'), + KeyPress::Ctrl('Y'), + KeyPress::Meta('Y'), + KeyPress::Enter, + ], + ("world", " !"), + ); +} + +#[test] +fn meta_backspace() { + assert_cursor( + EditMode::Emacs, + ("Hello, wor", "ld!"), + &[KeyPress::Meta('\x08'), KeyPress::Enter], + ("Hello, ", "ld!"), + ); +} + +#[test] +fn meta_digit() { + assert_cursor( + EditMode::Emacs, + ("", ""), + &[KeyPress::Meta('3'), KeyPress::Char('h'), KeyPress::Enter], + ("hhh", ""), + ); +} diff --git a/src/test/history.rs b/src/test/history.rs new file mode 100644 index 0000000000000000000000000000000000000000..faa7a3f52964246c850734c0807b6a06005913e9 --- /dev/null +++ b/src/test/history.rs @@ -0,0 +1,206 @@ +//! History related commands tests +use super::assert_history; +use config::EditMode; +use consts::KeyPress; + +#[test] +fn down_key() { + for mode in &[EditMode::Emacs, EditMode::Vi] { + assert_history( + *mode, + &["line1"], + &[KeyPress::Down, KeyPress::Enter], + ("", ""), + ); + assert_history( + *mode, + &["line1", "line2"], + &[KeyPress::Up, KeyPress::Up, KeyPress::Down, KeyPress::Enter], + ("line2", ""), + ); + assert_history( + *mode, + &["line1"], + &[ + KeyPress::Char('a'), + KeyPress::Up, + KeyPress::Down, // restore original line + KeyPress::Enter, + ], + ("a", ""), + ); + assert_history( + *mode, + &["line1"], + &[ + KeyPress::Char('a'), + KeyPress::Down, // noop + KeyPress::Enter, + ], + ("a", ""), + ); + } +} + +#[test] +fn up_key() { + for mode in &[EditMode::Emacs, EditMode::Vi] { + assert_history(*mode, &[], &[KeyPress::Up, KeyPress::Enter], ("", "")); + assert_history( + *mode, + &["line1"], + &[KeyPress::Up, KeyPress::Enter], + ("line1", ""), + ); + assert_history( + *mode, + &["line1", "line2"], + &[KeyPress::Up, KeyPress::Up, KeyPress::Enter], + ("line1", ""), + ); + } +} + +#[test] +fn ctrl_r() { + for mode in &[EditMode::Emacs, EditMode::Vi] { + assert_history( + *mode, + &[], + &[KeyPress::Ctrl('R'), KeyPress::Char('o'), KeyPress::Enter], + ("o", ""), + ); + assert_history( + *mode, + &["rustc", "cargo"], + &[ + KeyPress::Ctrl('R'), + KeyPress::Char('o'), + KeyPress::Right, // just to assert cursor pos + KeyPress::Enter, + ], + ("cargo", ""), + ); + assert_history( + *mode, + &["rustc", "cargo"], + &[ + KeyPress::Ctrl('R'), + KeyPress::Char('u'), + KeyPress::Right, // just to assert cursor pos + KeyPress::Enter, + ], + ("ru", "stc"), + ); + assert_history( + *mode, + &["rustc", "cargo"], + &[ + KeyPress::Ctrl('R'), + KeyPress::Char('r'), + KeyPress::Char('u'), + KeyPress::Right, // just to assert cursor pos + KeyPress::Enter, + ], + ("r", "ustc"), + ); + assert_history( + *mode, + &["rustc", "cargo"], + &[ + KeyPress::Ctrl('R'), + KeyPress::Char('r'), + KeyPress::Ctrl('R'), + KeyPress::Right, // just to assert cursor pos + KeyPress::Enter, + ], + ("r", "ustc"), + ); + assert_history( + *mode, + &["rustc", "cargo"], + &[ + KeyPress::Ctrl('R'), + KeyPress::Char('r'), + KeyPress::Char('z'), // no match + KeyPress::Right, // just to assert cursor pos + KeyPress::Enter, + ], + ("car", "go"), + ); + assert_history( + EditMode::Emacs, + &["rustc", "cargo"], + &[ + KeyPress::Char('a'), + KeyPress::Ctrl('R'), + KeyPress::Char('r'), + KeyPress::Ctrl('G'), // abort (FIXME: doesn't work with vi mode) + KeyPress::Enter, + ], + ("a", ""), + ); + } +} + +#[test] +fn ctrl_s() { + for mode in &[EditMode::Emacs, EditMode::Vi] { + assert_history( + *mode, + &["rustc", "cargo"], + &[ + KeyPress::Ctrl('R'), + KeyPress::Char('r'), + KeyPress::Ctrl('R'), + KeyPress::Ctrl('S'), + KeyPress::Right, // just to assert cursor pos + KeyPress::Enter, + ], + ("car", "go"), + ); + } +} + +#[test] +fn meta_lt() { + assert_history( + EditMode::Emacs, + &[""], + &[KeyPress::Meta('<'), KeyPress::Enter], + ("", ""), + ); + assert_history( + EditMode::Emacs, + &["rustc", "cargo"], + &[KeyPress::Meta('<'), KeyPress::Enter], + ("rustc", ""), + ); +} + +#[test] +fn meta_gt() { + assert_history( + EditMode::Emacs, + &[""], + &[KeyPress::Meta('>'), KeyPress::Enter], + ("", ""), + ); + assert_history( + EditMode::Emacs, + &["rustc", "cargo"], + &[KeyPress::Meta('<'), KeyPress::Meta('>'), KeyPress::Enter], + ("", ""), + ); + assert_history( + EditMode::Emacs, + &["rustc", "cargo"], + &[ + KeyPress::Char('a'), + KeyPress::Meta('<'), + KeyPress::Meta('>'), // restore original line + KeyPress::Enter, + ], + ("a", ""), + ); +} diff --git a/src/test/mod.rs b/src/test/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..1ce9881e2b96c192e2d21ba2a13edb5520a95d76 --- /dev/null +++ b/src/test/mod.rs @@ -0,0 +1,106 @@ +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; + +use super::{Editor, Result}; +use completion::Completer; +use config::{Config, EditMode}; +use consts::KeyPress; +use edit::init_state; +use keymap::{Cmd, InputState}; +use tty::Sink; + +mod common; +mod emacs; +mod history; +mod vi_cmd; +mod vi_insert; + +fn init_editor(mode: EditMode, keys: &[KeyPress]) -> Editor<()> { + let config = Config::builder().edit_mode(mode).build(); + let mut editor = Editor::<()>::with_config(config); + editor.term.keys.extend(keys.iter().cloned()); + editor +} + +struct SimpleCompleter; +impl Completer for SimpleCompleter { + type Candidate = String; + + 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, Arc::new(RwLock::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, + None, + &Config::default(), + ).unwrap(); + assert_eq!(Some(Cmd::AcceptLine), cmd); + assert_eq!("rust", s.line.as_str()); + assert_eq!(4, s.line.pos()); +} + +// `keys`: keys to press +// `expected_line`: line after enter key +fn assert_line(mode: EditMode, keys: &[KeyPress], expected_line: &str) { + let mut editor = init_editor(mode, keys); + let actual_line = editor.readline(">>").unwrap(); + assert_eq!(expected_line, actual_line); +} + +// `initial`: line status before `keys` pressed: strings before and after cursor +// `keys`: keys to press +// `expected_line`: line after enter key +fn assert_line_with_initial( + mode: EditMode, + initial: (&str, &str), + keys: &[KeyPress], + expected_line: &str, +) { + let mut editor = init_editor(mode, keys); + let actual_line = editor.readline_with_initial(">>", initial).unwrap(); + assert_eq!(expected_line, actual_line); +} + +// `initial`: line status before `keys` pressed: strings before and after cursor +// `keys`: keys to press +// `expected`: line status before enter key: strings before and after cursor +fn assert_cursor(mode: EditMode, initial: (&str, &str), keys: &[KeyPress], expected: (&str, &str)) { + let mut editor = init_editor(mode, keys); + let actual_line = editor.readline_with_initial("", initial).unwrap(); + assert_eq!(expected.0.to_owned() + expected.1, actual_line); + assert_eq!(expected.0.len(), editor.term.cursor); +} + +// `entries`: history entries before `keys` pressed +// `keys`: keys to press +// `expected`: line status before enter key: strings before and after cursor +fn assert_history(mode: EditMode, entries: &[&str], keys: &[KeyPress], expected: (&str, &str)) { + let mut editor = init_editor(mode, keys); + for entry in entries { + editor.history.add(*entry); + } + let actual_line = editor.readline("").unwrap(); + assert_eq!(expected.0.to_owned() + expected.1, actual_line); + assert_eq!(expected.0.len(), editor.term.cursor); +} + +#[test] +fn unknown_esc_key() { + for mode in &[EditMode::Emacs, EditMode::Vi] { + assert_line(*mode, &[KeyPress::UnknownEscSeq, KeyPress::Enter], ""); + } +} diff --git a/src/test/vi_cmd.rs b/src/test/vi_cmd.rs new file mode 100644 index 0000000000000000000000000000000000000000..1c1e4e9e6ad8da586746c78e6f0b864da3e4cc03 --- /dev/null +++ b/src/test/vi_cmd.rs @@ -0,0 +1,611 @@ +//! Vi command mode specific key bindings +use super::{assert_cursor, assert_history}; +use config::EditMode; +use consts::KeyPress; + +#[test] +fn dollar() { + assert_cursor( + EditMode::Vi, + ("", "Hi"), + &[KeyPress::Esc, KeyPress::Char('$'), KeyPress::Enter], + ("Hi", ""), // FIXME + ); +} + +/*#[test] +fn dot() { + // TODO +}*/ + +#[test] +fn semi_colon() { + assert_cursor( + EditMode::Vi, + ("", "Hello, world!"), + &[ + KeyPress::Esc, + KeyPress::Char('f'), + KeyPress::Char('o'), + KeyPress::Char(';'), + KeyPress::Enter, + ], + ("Hello, w", "orld!"), + ); +} + +#[test] +fn comma() { + assert_cursor( + EditMode::Vi, + ("Hello, w", "orld!"), + &[ + KeyPress::Esc, + KeyPress::Char('f'), + KeyPress::Char('l'), + KeyPress::Char(','), + KeyPress::Enter, + ], + ("Hel", "lo, world!"), + ); +} + +#[test] +fn zero() { + assert_cursor( + EditMode::Vi, + ("Hi", ""), + &[KeyPress::Esc, KeyPress::Char('0'), KeyPress::Enter], + ("", "Hi"), + ); +} + +#[test] +fn caret() { + assert_cursor( + EditMode::Vi, + (" Hi", ""), + &[KeyPress::Esc, KeyPress::Char('^'), KeyPress::Enter], + (" ", "Hi"), + ); +} + +#[test] +fn a() { + assert_cursor( + EditMode::Vi, + ("B", "e"), + &[ + KeyPress::Esc, + KeyPress::Char('a'), + KeyPress::Char('y'), + KeyPress::Enter, + ], + ("By", "e"), + ); +} + +#[test] +fn uppercase_a() { + assert_cursor( + EditMode::Vi, + ("", "By"), + &[ + KeyPress::Esc, + KeyPress::Char('A'), + KeyPress::Char('e'), + KeyPress::Enter, + ], + ("Bye", ""), + ); +} + +#[test] +fn b() { + assert_cursor( + EditMode::Vi, + ("Hello, world!", ""), + &[KeyPress::Esc, KeyPress::Char('b'), KeyPress::Enter], + ("Hello, ", "world!"), + ); + assert_cursor( + EditMode::Vi, + ("Hello, world!", ""), + &[ + KeyPress::Esc, + KeyPress::Char('2'), + KeyPress::Char('b'), + KeyPress::Enter, + ], + ("Hello", ", world!"), + ); +} + +#[test] +fn uppercase_b() { + assert_cursor( + EditMode::Vi, + ("Hello, world!", ""), + &[KeyPress::Esc, KeyPress::Char('B'), KeyPress::Enter], + ("Hello, ", "world!"), + ); + assert_cursor( + EditMode::Vi, + ("Hello, world!", ""), + &[ + KeyPress::Esc, + KeyPress::Char('2'), + KeyPress::Char('B'), + KeyPress::Enter, + ], + ("", "Hello, world!"), + ); +} + +#[test] +fn uppercase_c() { + assert_cursor( + EditMode::Vi, + ("Hello, w", "orld!"), + &[ + KeyPress::Esc, + KeyPress::Char('C'), + KeyPress::Char('i'), + KeyPress::Enter, + ], + ("Hello, i", ""), + ); +} + +#[test] +fn ctrl_k() { + for key in &[KeyPress::Char('D'), KeyPress::Ctrl('K')] { + assert_cursor( + EditMode::Vi, + ("Hi", ""), + &[KeyPress::Esc, *key, KeyPress::Enter], + ("H", ""), + ); + assert_cursor( + EditMode::Vi, + ("", "Hi"), + &[KeyPress::Esc, *key, KeyPress::Enter], + ("", ""), + ); + assert_cursor( + EditMode::Vi, + ("By", "e"), + &[KeyPress::Esc, *key, KeyPress::Enter], + ("B", ""), + ); + } +} + +#[test] +fn e() { + assert_cursor( + EditMode::Vi, + ("", "Hello, world!"), + &[KeyPress::Esc, KeyPress::Char('e'), KeyPress::Enter], + ("Hell", "o, world!"), + ); + assert_cursor( + EditMode::Vi, + ("", "Hello, world!"), + &[ + KeyPress::Esc, + KeyPress::Char('2'), + KeyPress::Char('e'), + KeyPress::Enter, + ], + ("Hello, worl", "d!"), + ); +} + +#[test] +fn uppercase_e() { + assert_cursor( + EditMode::Vi, + ("", "Hello, world!"), + &[KeyPress::Esc, KeyPress::Char('E'), KeyPress::Enter], + ("Hello", ", world!"), + ); + assert_cursor( + EditMode::Vi, + ("", "Hello, world!"), + &[ + KeyPress::Esc, + KeyPress::Char('2'), + KeyPress::Char('E'), + KeyPress::Enter, + ], + ("Hello, world", "!"), + ); +} + +#[test] +fn f() { + assert_cursor( + EditMode::Vi, + ("", "Hello, world!"), + &[ + KeyPress::Esc, + KeyPress::Char('f'), + KeyPress::Char('r'), + KeyPress::Enter, + ], + ("Hello, wo", "rld!"), + ); + assert_cursor( + EditMode::Vi, + ("", "Hello, world!"), + &[ + KeyPress::Esc, + KeyPress::Char('3'), + KeyPress::Char('f'), + KeyPress::Char('l'), + KeyPress::Enter, + ], + ("Hello, wor", "ld!"), + ); +} + +#[test] +fn uppercase_f() { + assert_cursor( + EditMode::Vi, + ("Hello, world!", ""), + &[ + KeyPress::Esc, + KeyPress::Char('F'), + KeyPress::Char('r'), + KeyPress::Enter, + ], + ("Hello, wo", "rld!"), + ); + assert_cursor( + EditMode::Vi, + ("Hello, world!", ""), + &[ + KeyPress::Esc, + KeyPress::Char('3'), + KeyPress::Char('F'), + KeyPress::Char('l'), + KeyPress::Enter, + ], + ("He", "llo, world!"), + ); +} + +#[test] +fn i() { + assert_cursor( + EditMode::Vi, + ("Be", ""), + &[ + KeyPress::Esc, + KeyPress::Char('i'), + KeyPress::Char('y'), + KeyPress::Enter, + ], + ("By", "e"), + ); +} + +#[test] +fn uppercase_i() { + assert_cursor( + EditMode::Vi, + ("Be", ""), + &[ + KeyPress::Esc, + KeyPress::Char('I'), + KeyPress::Char('y'), + KeyPress::Enter, + ], + ("y", "Be"), + ); +} + +#[test] +fn u() { + assert_cursor( + EditMode::Vi, + ("Hello, ", "world"), + &[ + KeyPress::Esc, + KeyPress::Ctrl('W'), + KeyPress::Char('u'), + KeyPress::Enter, + ], + ("Hello,", " world"), + ); +} + +#[test] +fn w() { + assert_cursor( + EditMode::Vi, + ("", "Hello, world!"), + &[KeyPress::Esc, KeyPress::Char('w'), KeyPress::Enter], + ("Hello", ", world!"), + ); + assert_cursor( + EditMode::Vi, + ("", "Hello, world!"), + &[ + KeyPress::Esc, + KeyPress::Char('2'), + KeyPress::Char('w'), + KeyPress::Enter, + ], + ("Hello, ", "world!"), + ); +} + +#[test] +fn uppercase_w() { + assert_cursor( + EditMode::Vi, + ("", "Hello, world!"), + &[KeyPress::Esc, KeyPress::Char('W'), KeyPress::Enter], + ("Hello, ", "world!"), + ); + assert_cursor( + EditMode::Vi, + ("", "Hello, world!"), + &[ + KeyPress::Esc, + KeyPress::Char('2'), + KeyPress::Char('W'), + KeyPress::Enter, + ], + ("Hello, world", "!"), + ); +} + +#[test] +fn x() { + assert_cursor( + EditMode::Vi, + ("", "a"), + &[KeyPress::Esc, KeyPress::Char('x'), KeyPress::Enter], + ("", ""), + ); +} + +#[test] +fn uppercase_x() { + assert_cursor( + EditMode::Vi, + ("Hi", ""), + &[KeyPress::Esc, KeyPress::Char('X'), KeyPress::Enter], + ("", "i"), + ); +} + +#[test] +fn h() { + for key in &[ + KeyPress::Char('h'), + KeyPress::Ctrl('H'), + KeyPress::Backspace, + ] { + assert_cursor( + EditMode::Vi, + ("Bye", ""), + &[KeyPress::Esc, *key, KeyPress::Enter], + ("B", "ye"), + ); + assert_cursor( + EditMode::Vi, + ("Bye", ""), + &[KeyPress::Esc, KeyPress::Char('2'), *key, KeyPress::Enter], + ("", "Bye"), + ); + } +} + +#[test] +fn l() { + for key in &[KeyPress::Char('l'), KeyPress::Char(' ')] { + assert_cursor( + EditMode::Vi, + ("", "Hi"), + &[KeyPress::Esc, *key, KeyPress::Enter], + ("H", "i"), + ); + assert_cursor( + EditMode::Vi, + ("", "Hi"), + &[KeyPress::Esc, KeyPress::Char('2'), *key, KeyPress::Enter], + ("Hi", ""), + ); + } +} + +#[test] +fn j() { + for key in &[ + KeyPress::Char('j'), + KeyPress::Char('+'), + KeyPress::Ctrl('N'), + ] { + assert_history( + EditMode::Vi, + &["line1", "line2"], + &[ + KeyPress::Esc, + KeyPress::Ctrl('P'), + KeyPress::Ctrl('P'), + *key, + KeyPress::Enter, + ], + ("line2", ""), + ); + } +} + +#[test] +fn k() { + for key in &[ + KeyPress::Char('k'), + KeyPress::Char('-'), + KeyPress::Ctrl('P'), + ] { + assert_history( + EditMode::Vi, + &["line1"], + &[KeyPress::Esc, *key, KeyPress::Enter], + ("line1", ""), + ); + } +} + +#[test] +fn p() { + assert_cursor( + EditMode::Vi, + ("Hello, ", "world"), + &[ + KeyPress::Esc, + KeyPress::Ctrl('W'), + KeyPress::Char('p'), + KeyPress::Enter, + ], + (" Hello", ",world"), + ); +} + +#[test] +fn uppercase_p() { + assert_cursor( + EditMode::Vi, + ("Hello, ", "world"), + &[ + KeyPress::Esc, + KeyPress::Ctrl('W'), + KeyPress::Char('P'), + KeyPress::Enter, + ], + ("Hello", ", world"), + ); +} + +#[test] +fn r() { + assert_cursor( + EditMode::Vi, + ("Hi", ", world!"), + &[ + KeyPress::Esc, + KeyPress::Char('r'), + KeyPress::Char('o'), + KeyPress::Enter, + ], + ("H", "o, world!"), + ); + assert_cursor( + EditMode::Vi, + ("He", "llo, world!"), + &[ + KeyPress::Esc, + KeyPress::Char('4'), + KeyPress::Char('r'), + KeyPress::Char('i'), + KeyPress::Enter, + ], + ("Hiii", "i, world!"), + ); +} + +#[test] +fn s() { + assert_cursor( + EditMode::Vi, + ("Hi", ", world!"), + &[ + KeyPress::Esc, + KeyPress::Char('s'), + KeyPress::Char('o'), + KeyPress::Enter, + ], + ("Ho", ", world!"), + ); + assert_cursor( + EditMode::Vi, + ("He", "llo, world!"), + &[ + KeyPress::Esc, + KeyPress::Char('4'), + KeyPress::Char('s'), + KeyPress::Char('i'), + KeyPress::Enter, + ], + ("Hi", ", world!"), + ); +} + +#[test] +fn uppercase_s() { + assert_cursor( + EditMode::Vi, + ("Hello, ", "world"), + &[KeyPress::Esc, KeyPress::Char('S'), KeyPress::Enter], + ("", ""), + ); +} + +#[test] +fn t() { + assert_cursor( + EditMode::Vi, + ("", "Hello, world!"), + &[ + KeyPress::Esc, + KeyPress::Char('t'), + KeyPress::Char('r'), + KeyPress::Enter, + ], + ("Hello, w", "orld!"), + ); + assert_cursor( + EditMode::Vi, + ("", "Hello, world!"), + &[ + KeyPress::Esc, + KeyPress::Char('3'), + KeyPress::Char('t'), + KeyPress::Char('l'), + KeyPress::Enter, + ], + ("Hello, wo", "rld!"), + ); +} + +#[test] +fn uppercase_t() { + assert_cursor( + EditMode::Vi, + ("Hello, world!", ""), + &[ + KeyPress::Esc, + KeyPress::Char('T'), + KeyPress::Char('r'), + KeyPress::Enter, + ], + ("Hello, wor", "ld!"), + ); + assert_cursor( + EditMode::Vi, + ("Hello, world!", ""), + &[ + KeyPress::Esc, + KeyPress::Char('3'), + KeyPress::Char('T'), + KeyPress::Char('l'), + KeyPress::Enter, + ], + ("Hel", "lo, world!"), + ); +} diff --git a/src/test/vi_insert.rs b/src/test/vi_insert.rs new file mode 100644 index 0000000000000000000000000000000000000000..cf8668056ff8ba62c0818077a1e6dc94da3d9f71 --- /dev/null +++ b/src/test/vi_insert.rs @@ -0,0 +1,56 @@ +//! Vi insert mode specific key bindings +use super::assert_cursor; +use config::EditMode; +use consts::KeyPress; + +#[test] +fn insert_mode_by_default() { + assert_cursor( + EditMode::Vi, + ("", ""), + &[KeyPress::Char('a'), KeyPress::Enter], + ("a", ""), + ); +} + +#[test] +fn ctrl_h() { + assert_cursor( + EditMode::Vi, + ("Hi", ""), + &[KeyPress::Ctrl('H'), KeyPress::Enter], + ("H", ""), + ); +} + +#[test] +fn backspace() { + assert_cursor( + EditMode::Vi, + ("", ""), + &[KeyPress::Backspace, KeyPress::Enter], + ("", ""), + ); + assert_cursor( + EditMode::Vi, + ("Hi", ""), + &[KeyPress::Backspace, KeyPress::Enter], + ("H", ""), + ); + assert_cursor( + EditMode::Vi, + ("", "Hi"), + &[KeyPress::Backspace, KeyPress::Enter], + ("", "Hi"), + ); +} + +#[test] +fn esc() { + assert_cursor( + EditMode::Vi, + ("", ""), + &[KeyPress::Char('a'), KeyPress::Esc, KeyPress::Enter], + ("", "a"), + ); +} diff --git a/src/tty/mod.rs b/src/tty/mod.rs index f7ce05114809c83570bfd118a172eebb2d356a69..c79c89fcc3b5708cba13d35070a544b5379cb586 100644 --- a/src/tty/mod.rs +++ b/src/tty/mod.rs @@ -1,49 +1,203 @@ //! This module implements and describes common TTY methods & traits -use std::io::Write; -use Result; -use config::Config; +use std::io::{self, Write}; +use unicode_segmentation::UnicodeSegmentation; +use unicode_width::UnicodeWidthStr; + +use config::{ColorMode, Config}; use consts::KeyPress; +use highlight::Highlighter; +use line_buffer::LineBuffer; +use Result; /// Terminal state -pub trait RawMode: Copy + Sized { +pub trait RawMode: Sized { /// Disable RAW mode for the terminal. fn disable_raw_mode(&self) -> Result<()>; } /// Translate bytes read from stdin to keys. -pub trait RawReader: Sized { +pub trait RawReader { /// Blocking read of key pressed. - fn next_key(&mut self) -> Result<KeyPress>; + fn next_key(&mut self, single_esc_abort: bool) -> Result<KeyPress>; /// For CTRL-V support #[cfg(unix)] fn next_char(&mut self) -> Result<char>; } +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub struct Position { + pub col: usize, + pub row: usize, +} + +/// Display prompt, line and cursor in terminal output +pub trait Renderer { + fn move_cursor(&mut self, old: Position, new: Position) -> Result<()>; + + /// Display prompt, line and cursor in terminal output + fn refresh_line( + &mut self, + prompt: &str, + prompt_size: Position, + line: &LineBuffer, + hint: Option<String>, + current_row: usize, + old_rows: usize, + highlighter: Option<&Highlighter>, + ) -> Result<(Position, Position)>; + + /// Calculate the number of columns and rows used to display `s` on a + /// `cols` width terminal + /// starting at `orig`. + fn calculate_position(&self, s: &str, orig: Position) -> Position; + + fn write_and_flush(&mut self, buf: &[u8]) -> Result<()>; + + /// Beep, used for completion when there is nothing to complete or when all + /// the choices were already shown. + fn beep(&mut self) -> Result<()> { + // TODO bell-style + try!(io::stderr().write_all(b"\x07")); + try!(io::stderr().flush()); + Ok(()) + } + + /// Clear the screen. Used to handle ctrl+l + fn clear_screen(&mut self) -> Result<()>; + + /// Check if a SIGWINCH signal has been received + fn sigwinch(&self) -> bool; + /// Update the number of columns/rows in the current terminal. + fn update_size(&mut self); + /// Get the number of columns in the current terminal. + fn get_columns(&self) -> usize; + /// Get the number of rows in the current terminal. + fn get_rows(&self) -> usize; +} + +impl<'a, R: Renderer + ?Sized> Renderer for &'a mut R { + fn move_cursor(&mut self, old: Position, new: Position) -> Result<()> { + (**self).move_cursor(old, new) + } + + fn refresh_line( + &mut self, + prompt: &str, + prompt_size: Position, + line: &LineBuffer, + hint: Option<String>, + current_row: usize, + old_rows: usize, + highlighter: Option<&Highlighter>, + ) -> Result<(Position, Position)> { + (**self).refresh_line( + prompt, + prompt_size, + line, + hint, + current_row, + old_rows, + highlighter, + ) + } + + fn calculate_position(&self, s: &str, orig: Position) -> Position { + (**self).calculate_position(s, orig) + } + + fn write_and_flush(&mut self, buf: &[u8]) -> Result<()> { + (**self).write_and_flush(buf) + } + + fn beep(&mut self) -> Result<()> { + (**self).beep() + } + + fn clear_screen(&mut self) -> Result<()> { + (**self).clear_screen() + } + + fn sigwinch(&self) -> bool { + (**self).sigwinch() + } + + fn update_size(&mut self) { + (**self).update_size() + } + + fn get_columns(&self) -> usize { + (**self).get_columns() + } + + fn get_rows(&self) -> usize { + (**self).get_rows() + } +} + /// Terminal contract -pub trait Term: Clone { - type Reader: RawReader; - type Writer: Write; +pub trait Term { + type Reader: RawReader; // rl_instream + type Writer: Renderer; // rl_outstream type Mode: RawMode; - fn new() -> Self; - /// Check if current terminal can provide a rich line-editing user interface. + fn new(color_mode: ColorMode) -> Self; + /// Check if current terminal can provide a rich line-editing user + /// interface. fn is_unsupported(&self) -> bool; /// check if stdin is connected to a terminal. fn is_stdin_tty(&self) -> bool; - /// Get the number of columns in the current terminal. - fn get_columns(&self) -> usize; - /// Get the number of rows in the current terminal. - fn get_rows(&self) -> usize; - /// Check if a SIGWINCH signal has been received - fn sigwinch(&self) -> bool; + /// Check if output supports colors. + fn colors_enabled(&self) -> bool; /// Enable RAW mode for the terminal. - fn enable_raw_mode(&self) -> Result<Self::Mode>; + fn enable_raw_mode(&mut self) -> Result<Self::Mode>; /// Create a RAW reader fn create_reader(&self, config: &Config) -> Result<Self::Reader>; /// Create a writer fn create_writer(&self) -> Self::Writer; - /// Clear the screen. Used to handle ctrl+l - fn clear_screen(&mut self, w: &mut Write) -> Result<()>; +} + +fn truncate(text: &str, col: usize, max_col: usize) -> &str { + let mut col = col; + let mut esc_seq = 0; + let mut end = text.len(); + for (i, s) in text.grapheme_indices(true) { + col += width(s, &mut esc_seq); + if col > max_col { + end = i; + break; + } + } + &text[..end] +} + +fn width(s: &str, esc_seq: &mut u8) -> usize { + if *esc_seq == 1 { + if s == "[" { + // CSI + *esc_seq = 2; + } else { + // two-character sequence + *esc_seq = 0; + } + 0 + } else if *esc_seq == 2 { + if s == ";" || (s.as_bytes()[0] >= b'0' && s.as_bytes()[0] <= b'9') { + /*} else if s == "m" { + // last + *esc_seq = 0;*/ + } else { + // not supported + *esc_seq = 0; + } + 0 + } else if s == "\x1b" { + *esc_seq = 1; + 0 + } else if s == "\n" { + 0 + } else { + s.width() + } } // If on Windows platform import Windows TTY module diff --git a/src/tty/test.rs b/src/tty/test.rs index 8813fa577a6953ae8251fba9848009c878f017ad..752bae38e936fc125e469617522323c811cf3531 100644 --- a/src/tty/test.rs +++ b/src/tty/test.rs @@ -1,17 +1,15 @@ //! Tests specific definitions -use std::io::{self, Sink, Write}; use std::iter::IntoIterator; use std::slice::Iter; use std::vec::IntoIter; -#[cfg(windows)] -use winapi; - -use config::Config; +use super::{truncate, Position, RawMode, RawReader, Renderer, Term}; +use config::{ColorMode, Config}; use consts::KeyPress; use error::ReadlineError; +use highlight::Highlighter; +use line_buffer::LineBuffer; use Result; -use super::{RawMode, RawReader, Term}; pub type Mode = (); @@ -22,12 +20,13 @@ impl RawMode for Mode { } impl<'a> RawReader for Iter<'a, KeyPress> { - fn next_key(&mut self) -> Result<KeyPress> { + fn next_key(&mut self, _: bool) -> Result<KeyPress> { match self.next() { Some(key) => Ok(*key), None => Err(ReadlineError::Eof), } } + #[cfg(unix)] fn next_char(&mut self) -> Result<char> { unimplemented!(); @@ -35,114 +34,133 @@ impl<'a> RawReader for Iter<'a, KeyPress> { } impl RawReader for IntoIter<KeyPress> { - fn next_key(&mut self) -> Result<KeyPress> { + fn next_key(&mut self, _: bool) -> Result<KeyPress> { match self.next() { Some(key) => Ok(key), None => Err(ReadlineError::Eof), } } + #[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 type Terminal = DummyTerminal; +pub struct Sink {} -#[derive(Clone,Debug)] -pub struct DummyTerminal { - pub keys: Vec<KeyPress>, +impl Sink { + pub fn new() -> Sink { + Sink {} + } } -impl DummyTerminal { - #[cfg(windows)] - pub fn get_console_screen_buffer_info(&self) -> Result<winapi::CONSOLE_SCREEN_BUFFER_INFO> { - let dw_size = winapi::COORD { X: 80, Y: 24 }; - let dw_cursor_osition = winapi::COORD { X: 0, Y: 0 }; - let sr_window = winapi::SMALL_RECT { - Left: 0, - Top: 0, - Right: 0, - Bottom: 0, - }; - let info = winapi::CONSOLE_SCREEN_BUFFER_INFO { - dwSize: dw_size, - dwCursorPosition: dw_cursor_osition, - wAttributes: 0, - srWindow: sr_window, - dwMaximumWindowSize: dw_size, - }; - Ok(info) - } - - #[cfg(windows)] - pub fn set_console_cursor_position(&mut self, _: winapi::COORD) -> Result<()> { +impl Renderer for Sink { + fn move_cursor(&mut self, _: Position, _: Position) -> Result<()> { + Ok(()) + } + + fn refresh_line( + &mut self, + _: &str, + prompt_size: Position, + line: &LineBuffer, + hint: Option<String>, + _: usize, + _: usize, + _: Option<&Highlighter>, + ) -> Result<(Position, Position)> { + let cursor = self.calculate_position(&line[..line.pos()], prompt_size); + if let Some(hint) = hint { + truncate(&hint, 0, 80); + } + let end = self.calculate_position(&line, prompt_size); + Ok((cursor, end)) + } + + fn calculate_position(&self, s: &str, orig: Position) -> Position { + let mut pos = orig; + pos.col += s.len(); + pos + } + + fn write_and_flush(&mut self, _: &[u8]) -> Result<()> { Ok(()) } - #[cfg(windows)] - pub fn fill_console_output_character(&mut self, - _: winapi::DWORD, - _: winapi::COORD) - -> Result<()> { + fn beep(&mut self) -> Result<()> { Ok(()) } + + fn clear_screen(&mut self) -> Result<()> { + Ok(()) + } + + fn sigwinch(&self) -> bool { + false + } + + fn update_size(&mut self) {} + + fn get_columns(&self) -> usize { + 80 + } + + fn get_rows(&self) -> usize { + 24 + } +} + +pub type Terminal = DummyTerminal; + +#[derive(Clone, Debug)] +pub struct DummyTerminal { + pub keys: Vec<KeyPress>, + pub cursor: usize, // cursor position before last command } impl Term for DummyTerminal { + type Mode = Mode; type Reader = IntoIter<KeyPress>; type Writer = Sink; - type Mode = Mode; - fn new() -> DummyTerminal { - DummyTerminal { keys: Vec::new() } + fn new(_color_mode: ColorMode) -> DummyTerminal { + DummyTerminal { + keys: Vec::new(), + cursor: 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 } - // Interactive loop: - - /// Get the number of columns in the current terminal. - fn get_columns(&self) -> usize { - 80 - } - - /// Get the number of rows in the current terminal. - fn get_rows(&self) -> usize { - 24 - } - - /// Check if a SIGWINCH signal has been received - fn sigwinch(&self) -> bool { + fn colors_enabled(&self) -> bool { false } - fn enable_raw_mode(&self) -> Result<Mode> { + // Interactive loop: + + fn enable_raw_mode(&mut self) -> Result<Mode> { 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() - } - - /// Clear the screen. Used to handle ctrl+l - fn clear_screen(&mut self, _: &mut Write) -> Result<()> { - Ok(()) + Sink {} } } diff --git a/src/tty/unix.rs b/src/tty/unix.rs index 17a439e9511ad5b830973522fa66aece8780aa47..5cc97e351d1fc4ff62c8b7dfdd2e23b059c2f109 100644 --- a/src/tty/unix.rs +++ b/src/tty/unix.rs @@ -3,18 +3,23 @@ use std; use std::io::{self, Read, Stdout, Write}; use std::sync; use std::sync::atomic; + use libc; use nix; -use nix::poll; +use nix::poll::{self, EventFlags}; use nix::sys::signal; use nix::sys::termios; +use nix::sys::termios::SetArg; +use unicode_segmentation::UnicodeSegmentation; +use utf8parse::{Parser, Receiver}; -use char_iter; -use config::Config; +use super::{truncate, width, Position, RawMode, RawReader, Renderer, Term}; +use config::{ColorMode, Config}; use consts::{self, KeyPress}; -use Result; use error; -use super::{RawMode, RawReader, Term}; +use highlight::Highlighter; +use line_buffer::LineBuffer; +use Result; const STDIN_FILENO: libc::c_int = libc::STDIN_FILENO; const STDOUT_FILENO: libc::c_int = libc::STDOUT_FILENO; @@ -27,7 +32,8 @@ fn get_win_size() -> (usize, usize) { unsafe { let mut size: libc::winsize = zeroed(); - match libc::ioctl(STDOUT_FILENO, libc::TIOCGWINSZ, &mut size) { + // https://github.com/rust-lang/libc/pull/704 + match libc::ioctl(STDOUT_FILENO, libc::TIOCGWINSZ as libc::c_ulong, &mut size) { 0 => (size.ws_col as usize, size.ws_row as usize), // TODO getCursorPosition _ => (80, 24), } @@ -37,7 +43,6 @@ fn get_win_size() -> (usize, usize) { /// Check TERM environment variable to see if current term is in our /// unsupported list fn is_unsupported_term() -> bool { - use std::ascii::AsciiExt; match std::env::var("TERM") { Ok(term) => { for iter in &UNSUPPORTED_TERM { @@ -51,7 +56,6 @@ fn is_unsupported_term() -> bool { } } - /// Return whether or not STDIN, STDOUT or STDERR is a TTY fn is_a_tty(fd: libc::c_int) -> bool { unsafe { libc::isatty(fd) != 0 } @@ -62,7 +66,7 @@ pub type Mode = termios::Termios; impl RawMode for Mode { /// Disable RAW mode for the terminal. fn disable_raw_mode(&self) -> Result<()> { - try!(termios::tcsetattr(STDIN_FILENO, termios::TCSADRAIN, self)); + try!(termios::tcsetattr(STDIN_FILENO, SetArg::TCSADRAIN, self)); Ok(()) } } @@ -75,13 +79,17 @@ impl Read for StdinRaw { fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { loop { let res = unsafe { - libc::read(STDIN_FILENO, - buf.as_mut_ptr() as *mut libc::c_void, - buf.len() as libc::size_t) + libc::read( + STDIN_FILENO, + buf.as_mut_ptr() as *mut libc::c_void, + buf.len() as libc::size_t, + ) }; if res == -1 { let error = io::Error::last_os_error(); - if error.kind() != io::ErrorKind::Interrupted { + if error.kind() != io::ErrorKind::Interrupted + || SIGWINCH.load(atomic::Ordering::Relaxed) + { return Err(error); } } else { @@ -93,110 +101,222 @@ impl Read for StdinRaw { /// Console input reader pub struct PosixRawReader { - chars: char_iter::Chars<StdinRaw>, + stdin: StdinRaw, timeout_ms: i32, + buf: [u8; 1], + parser: Parser, + receiver: Utf8, +} + +struct Utf8 { + c: Option<char>, + valid: bool, } impl PosixRawReader { - pub fn new(config: &Config) -> Result<PosixRawReader> { - let stdin = StdinRaw {}; + fn new(config: &Config) -> Result<PosixRawReader> { Ok(PosixRawReader { - chars: char_iter::chars(stdin), - timeout_ms: config.keyseq_timeout(), - }) + stdin: StdinRaw {}, + timeout_ms: config.keyseq_timeout(), + buf: [0; 1], + parser: Parser::new(), + receiver: Utf8 { + c: None, + valid: true, + }, + }) } + /// Handle ESC <seq1> sequences fn escape_sequence(&mut self) -> Result<KeyPress> { - // Read the next two bytes representing the escape sequence. + // Read the next byte representing the escape sequence. let seq1 = try!(self.next_char()); if seq1 == '[' { - // ESC [ sequences. - let seq2 = try!(self.next_char()); - if seq2.is_digit(10) { - // Extended escape, read additional byte. - let seq3 = try!(self.next_char()); - if seq3 == '~' { - Ok(match seq2 { - '1' | '7' => KeyPress::Home, // '1': xterm - '3' => KeyPress::Delete, - '4' | '8' => KeyPress::End, // '4': xterm - '5' => KeyPress::PageUp, - '6' => KeyPress::PageDown, - _ => { - debug!(target: "rustyline", "unsupported esc sequence: ESC{:?}{:?}{:?}", seq1, seq2, seq3); + // ESC [ sequences. (CSI) + self.escape_csi() + } else if seq1 == 'O' { + // xterm + // ESC O sequences. (SS3) + self.escape_o() + } else if seq1 == '\x1b' { + // ESC ESC + Ok(KeyPress::Esc) + } else { + // TODO ESC-R (r): Undo all changes made to this line. + Ok(KeyPress::Meta(seq1)) + } + } + + /// Handle ESC [ <seq2> escape sequences + fn escape_csi(&mut self) -> Result<KeyPress> { + let seq2 = try!(self.next_char()); + if seq2.is_digit(10) { + match seq2 { + '0' | '9' => { + debug!(target: "rustyline", "unsupported esc sequence: ESC [ {:?}", seq2); + Ok(KeyPress::UnknownEscSeq) + } + _ => { + // Extended escape, read additional byte. + self.extended_escape(seq2) + } + } + } else { + // ANSI + Ok(match seq2 { + 'A' => KeyPress::Up, // kcuu1 + 'B' => KeyPress::Down, // kcud1 + 'C' => KeyPress::Right, // kcuf1 + 'D' => KeyPress::Left, // kcub1 + 'F' => KeyPress::End, + 'H' => KeyPress::Home, // khome + _ => { + debug!(target: "rustyline", "unsupported esc sequence: ESC [ {:?}", seq2); + KeyPress::UnknownEscSeq + } + }) + } + } + + /// Handle ESC [ <seq2:digit> escape sequences + fn extended_escape(&mut self, seq2: char) -> Result<KeyPress> { + let seq3 = try!(self.next_char()); + if seq3 == '~' { + Ok(match seq2 { + '1' | '7' => KeyPress::Home, // tmux, xrvt + '2' => KeyPress::Insert, + '3' => KeyPress::Delete, // kdch1 + '4' | '8' => KeyPress::End, // tmux, xrvt + '5' => KeyPress::PageUp, // kpp + '6' => KeyPress::PageDown, // knp + _ => { + debug!(target: "rustyline", + "unsupported esc sequence: ESC [ {} ~", seq2); + KeyPress::UnknownEscSeq + } + }) + } else if seq3.is_digit(10) { + let seq4 = try!(self.next_char()); + if seq4 == '~' { + Ok(match (seq2, seq3) { + ('1', '1') => KeyPress::F(1), // rxvt-unicode + ('1', '2') => KeyPress::F(2), // rxvt-unicode + ('1', '3') => KeyPress::F(3), // rxvt-unicode + ('1', '4') => KeyPress::F(4), // rxvt-unicode + ('1', '5') => KeyPress::F(5), // kf5 + ('1', '7') => KeyPress::F(6), // kf6 + ('1', '8') => KeyPress::F(7), // kf7 + ('1', '9') => KeyPress::F(8), // kf8 + ('2', '0') => KeyPress::F(9), // kf9 + ('2', '1') => KeyPress::F(10), // kf10 + ('2', '3') => KeyPress::F(11), // kf11 + ('2', '4') => KeyPress::F(12), // kf12 + _ => { + debug!(target: "rustyline", + "unsupported esc sequence: ESC [ {}{} ~", seq2, seq3); KeyPress::UnknownEscSeq } - }) + }) + } else if seq4 == ';' { + let seq5 = try!(self.next_char()); + if seq5.is_digit(10) { + let seq6 = try!(self.next_char()); // '~' expected + debug!(target: "rustyline", + "unsupported esc sequence: ESC [ {}{} ; {} {}", seq2, seq3, seq5, seq6); } else { - debug!(target: "rustyline", "unsupported esc sequence: ESC{:?}{:?}{:?}", seq1, seq2, seq3); - Ok(KeyPress::UnknownEscSeq) + debug!(target: "rustyline", + "unsupported esc sequence: ESC [ {}{} ; {:?}", seq2, seq3, seq5); } + Ok(KeyPress::UnknownEscSeq) } else { - Ok(match seq2 { - 'A' => KeyPress::Up, // ANSI - 'B' => KeyPress::Down, - 'C' => KeyPress::Right, - 'D' => KeyPress::Left, - 'F' => KeyPress::End, - 'H' => KeyPress::Home, - _ => { - debug!(target: "rustyline", "unsupported esc sequence: ESC{:?}{:?}", seq1, seq2); - KeyPress::UnknownEscSeq - } - }) + debug!(target: "rustyline", + "unsupported esc sequence: ESC [ {}{} {:?}", seq2, seq3, seq4); + Ok(KeyPress::UnknownEscSeq) } - } else if seq1 == 'O' { - // ESC O sequences. - let seq2 = try!(self.next_char()); - Ok(match seq2 { - 'A' => KeyPress::Up, - 'B' => KeyPress::Down, - 'C' => KeyPress::Right, - 'D' => KeyPress::Left, - 'F' => KeyPress::End, - 'H' => KeyPress::Home, - _ => { - debug!(target: "rustyline", "unsupported esc sequence: ESC{:?}{:?}", seq1, seq2); - KeyPress::UnknownEscSeq + } else if seq3 == ';' { + let seq4 = try!(self.next_char()); + if seq4.is_digit(10) { + let seq5 = try!(self.next_char()); + if seq2 == '1' { + Ok(match (seq4, seq5) { + ('5', 'A') => KeyPress::ControlUp, + ('5', 'B') => KeyPress::ControlDown, + ('5', 'C') => KeyPress::ControlRight, + ('5', 'D') => KeyPress::ControlLeft, + ('2', 'A') => KeyPress::ShiftUp, + ('2', 'B') => KeyPress::ShiftDown, + ('2', 'C') => KeyPress::ShiftRight, + ('2', 'D') => KeyPress::ShiftLeft, + _ => { + debug!(target: "rustyline", + "unsupported esc sequence: ESC [ {} ; {} {}", seq2, seq4, seq5); + KeyPress::UnknownEscSeq + } + }) + } else { + debug!(target: "rustyline", + "unsupported esc sequence: ESC [ {} ; {} {}", seq2, seq4, seq5); + Ok(KeyPress::UnknownEscSeq) + } + } else { + debug!(target: "rustyline", + "unsupported esc sequence: ESC [ {} ; {:?}", seq2, seq4); + Ok(KeyPress::UnknownEscSeq) } - }) } else { - // TODO ESC-R (r): Undo all changes made to this line. - Ok(match seq1 { - '\x08' => KeyPress::Meta('\x08'), // Backspace - '-' => KeyPress::Meta('-'), - '0'...'9' => KeyPress::Meta(seq1), - '<' => KeyPress::Meta('<'), - '>' => KeyPress::Meta('>'), - 'b' | 'B' => KeyPress::Meta('B'), - 'c' | 'C' => KeyPress::Meta('C'), - 'd' | 'D' => KeyPress::Meta('D'), - 'f' | 'F' => KeyPress::Meta('F'), - 'l' | 'L' => KeyPress::Meta('L'), - 'n' | 'N' => KeyPress::Meta('N'), - 'p' | 'P' => KeyPress::Meta('P'), - 't' | 'T' => KeyPress::Meta('T'), - 'u' | 'U' => KeyPress::Meta('U'), - 'y' | 'Y' => KeyPress::Meta('Y'), - '\x7f' => KeyPress::Meta('\x7f'), // Delete - _ => { - debug!(target: "rustyline", "unsupported esc sequence: M-{:?}", seq1); + Ok(match (seq2, seq3) { + ('5', 'A') => KeyPress::ControlUp, + ('5', 'B') => KeyPress::ControlDown, + ('5', 'C') => KeyPress::ControlRight, + ('5', 'D') => KeyPress::ControlLeft, + _ => { + debug!(target: "rustyline", + "unsupported esc sequence: ESC [ {} {:?}", seq2, seq3); + KeyPress::UnknownEscSeq + } + }) + } + } + + /// Handle ESC O <seq2> escape sequences + fn escape_o(&mut self) -> Result<KeyPress> { + let seq2 = try!(self.next_char()); + Ok(match seq2 { + 'A' => KeyPress::Up, // kcuu1 + 'B' => KeyPress::Down, // kcud1 + 'C' => KeyPress::Right, // kcuf1 + 'D' => KeyPress::Left, // kcub1 + 'F' => KeyPress::End, // kend + 'H' => KeyPress::Home, // khome + 'P' => KeyPress::F(1), // kf1 + 'Q' => KeyPress::F(2), // kf2 + 'R' => KeyPress::F(3), // kf3 + 'S' => KeyPress::F(4), // kf4 + 'a' => KeyPress::ControlUp, + 'b' => KeyPress::ControlDown, + 'c' => KeyPress::ControlRight, + 'd' => KeyPress::ControlLeft, + _ => { + debug!(target: "rustyline", "unsupported esc sequence: ESC O {:?}", seq2); KeyPress::UnknownEscSeq } - }) - } + }) } } impl RawReader for PosixRawReader { - fn next_key(&mut self) -> Result<KeyPress> { + fn next_key(&mut self, single_esc_abort: bool) -> Result<KeyPress> { let c = try!(self.next_char()); let mut key = consts::char_to_key_press(c); if key == KeyPress::Esc { - let mut fds = - [poll::PollFd::new(STDIN_FILENO, poll::POLLIN, poll::EventFlags::empty())]; - match poll::poll(&mut fds, self.timeout_ms) { + let timeout_ms = if single_esc_abort && self.timeout_ms == -1 { + 0 + } else { + self.timeout_ms + }; + let mut fds = [poll::PollFd::new(STDIN_FILENO, EventFlags::POLLIN)]; + match poll::poll(&mut fds, timeout_ms) { Ok(n) if n == 0 => { // single escape } @@ -213,22 +333,235 @@ impl RawReader for PosixRawReader { } fn next_char(&mut self) -> Result<char> { - match self.chars.next() { - Some(c) => Ok(try!(c)), - None => Err(error::ReadlineError::Eof), + loop { + let n = try!(self.stdin.read(&mut self.buf)); + if n == 0 { + return Err(error::ReadlineError::Eof); + } + let b = self.buf[0]; + self.parser.advance(&mut self.receiver, b); + if !self.receiver.valid { + return Err(error::ReadlineError::Utf8Error); + } else if self.receiver.c.is_some() { + return Ok(self.receiver.c.take().unwrap()); + } + } + } +} + +impl Receiver for Utf8 { + /// Called whenever a codepoint is parsed successfully + fn codepoint(&mut self, c: char) { + self.c = Some(c); + self.valid = true; + } + + /// Called when an invalid_sequence is detected + fn invalid_sequence(&mut self) { + self.c = None; + self.valid = false; + } +} + +/// Console output writer +pub struct PosixRenderer { + out: Stdout, + cols: usize, // Number of columns in terminal +} + +impl PosixRenderer { + fn new() -> PosixRenderer { + let (cols, _) = get_win_size(); + PosixRenderer { + out: io::stdout(), + cols, } } } +impl Renderer for PosixRenderer { + fn move_cursor(&mut self, old: Position, new: Position) -> Result<()> { + use std::fmt::Write; + let mut ab = String::new(); + if new.row > old.row { + // move down + let row_shift = new.row - old.row; + if row_shift == 1 { + ab.push_str("\x1b[B"); + } else { + write!(ab, "\x1b[{}B", row_shift).unwrap(); + } + } else if new.row < old.row { + // move up + let row_shift = old.row - new.row; + if row_shift == 1 { + ab.push_str("\x1b[A"); + } else { + write!(ab, "\x1b[{}A", row_shift).unwrap(); + } + } + if new.col > old.col { + // move right + let col_shift = new.col - old.col; + if col_shift == 1 { + ab.push_str("\x1b[C"); + } else { + write!(ab, "\x1b[{}C", col_shift).unwrap(); + } + } else if new.col < old.col { + // move left + let col_shift = old.col - new.col; + if col_shift == 1 { + ab.push_str("\x1b[D"); + } else { + write!(ab, "\x1b[{}D", col_shift).unwrap(); + } + } + self.write_and_flush(ab.as_bytes()) + } + + fn refresh_line( + &mut self, + prompt: &str, + prompt_size: Position, + line: &LineBuffer, + hint: Option<String>, + current_row: usize, + old_rows: usize, + highlighter: Option<&Highlighter>, + ) -> Result<(Position, Position)> { + use std::fmt::Write; + let mut ab = String::new(); + + // calculate the position of the end of the input line + let end_pos = self.calculate_position(line, prompt_size); + // calculate the desired position of the cursor + let cursor = self.calculate_position(&line[..line.pos()], prompt_size); + + // self.old_rows < self.cursor.row if the prompt spans multiple lines and if + // this is the default State. + let cursor_row_movement = old_rows.checked_sub(current_row).unwrap_or(0); + // move the cursor down as required + if cursor_row_movement > 0 { + write!(ab, "\x1b[{}B", cursor_row_movement).unwrap(); + } + // clear old rows + for _ in 0..old_rows { + ab.push_str("\r\x1b[0K\x1b[A"); + } + // clear the line + ab.push_str("\r\x1b[0K"); + + if let Some(highlighter) = highlighter { + // display the prompt + ab.push_str(&highlighter.highlight_prompt(prompt)); + // display the input line + ab.push_str(&highlighter.highlight(line, line.pos())); + } else { + // display the prompt + ab.push_str(prompt); + // display the input line + ab.push_str(line); + } + // display hint + if let Some(hint) = hint { + let truncate = truncate(&hint, end_pos.col, self.cols); + if let Some(highlighter) = highlighter { + ab.push_str(&highlighter.highlight_hint(truncate)); + } else { + ab.push_str(truncate); + } + } + // we have to generate our own newline on line wrap + if end_pos.col == 0 && end_pos.row > 0 { + ab.push_str("\n"); + } + // position the cursor + let cursor_row_movement = end_pos.row - cursor.row; + // move the cursor up as required + if cursor_row_movement > 0 { + write!(ab, "\x1b[{}A", cursor_row_movement).unwrap(); + } + // position the cursor within the line + if cursor.col > 0 { + write!(ab, "\r\x1b[{}C", cursor.col).unwrap(); + } else { + ab.push('\r'); + } + + try!(self.write_and_flush(ab.as_bytes())); + Ok((cursor, end_pos)) + } + + fn write_and_flush(&mut self, buf: &[u8]) -> Result<()> { + try!(self.out.write_all(buf)); + try!(self.out.flush()); + Ok(()) + } + + /// Control characters are treated as having zero width. + /// Characters with 2 column width are correctly handled (not splitted). + fn calculate_position(&self, s: &str, orig: Position) -> Position { + let mut pos = orig; + let mut esc_seq = 0; + for c in s.graphemes(true) { + if c == "\n" { + pos.row += 1; + pos.col = 0; + continue; + } + let cw = width(c, &mut esc_seq); + pos.col += cw; + if pos.col > self.cols { + pos.row += 1; + pos.col = cw; + } + } + if pos.col == self.cols { + pos.col = 0; + pos.row += 1; + } + pos + } + + /// Clear the screen. Used to handle ctrl+l + fn clear_screen(&mut self) -> Result<()> { + self.write_and_flush(b"\x1b[H\x1b[2J") + } + + /// Check if a SIGWINCH signal has been received + fn sigwinch(&self) -> bool { + SIGWINCH.compare_and_swap(true, false, atomic::Ordering::SeqCst) + } + + /// Try to update the number of columns in the current terminal, + fn update_size(&mut self) { + let (cols, _) = get_win_size(); + self.cols = cols; + } + + fn get_columns(&self) -> usize { + self.cols + } + + /// Try to get the number of rows in the current terminal, + /// or assume 24 if it fails. + fn get_rows(&self) -> usize { + let (_, rows) = get_win_size(); + rows + } +} static SIGWINCH_ONCE: sync::Once = sync::ONCE_INIT; static SIGWINCH: atomic::AtomicBool = atomic::ATOMIC_BOOL_INIT; fn install_sigwinch_handler() { SIGWINCH_ONCE.call_once(|| unsafe { - let sigwinch = signal::SigAction::new(signal::SigHandler::Handler(sigwinch_handler), - signal::SaFlags::empty(), - signal::SigSet::empty()); + let sigwinch = signal::SigAction::new( + signal::SigHandler::Handler(sigwinch_handler), + signal::SaFlags::empty(), + signal::SigSet::empty(), + ); let _ = signal::sigaction(signal::SIGWINCH, &sigwinch); }); } @@ -240,23 +573,27 @@ extern "C" fn sigwinch_handler(_: libc::c_int) { pub type Terminal = PosixTerminal; -#[derive(Clone,Debug)] +#[derive(Clone, Debug)] pub struct PosixTerminal { unsupported: bool, stdin_isatty: bool, + stdout_isatty: bool, + color_mode: ColorMode, } impl Term for PosixTerminal { - type Reader = PosixRawReader; - type Writer = Stdout; type Mode = Mode; + type Reader = PosixRawReader; + type Writer = PosixRenderer; - fn new() -> PosixTerminal { + fn new(color_mode: ColorMode) -> PosixTerminal { let term = PosixTerminal { unsupported: is_unsupported_term(), stdin_isatty: is_a_tty(STDIN_FILENO), + stdout_isatty: is_a_tty(STDOUT_FILENO), + color_mode, }; - if !term.unsupported && term.stdin_isatty && is_a_tty(STDOUT_FILENO) { + if !term.unsupported && term.stdin_isatty && term.stdout_isatty { install_sigwinch_handler(); } term @@ -264,7 +601,8 @@ impl Term for PosixTerminal { // Init checks: - /// Check if current terminal can provide a rich line-editing user interface. + /// Check if current terminal can provide a rich line-editing user + /// interface. fn is_unsupported(&self) -> bool { self.unsupported } @@ -274,42 +612,43 @@ impl Term for PosixTerminal { self.stdin_isatty } - // Interactive loop: - - /// Try to get the number of columns in the current terminal, - /// or assume 80 if it fails. - fn get_columns(&self) -> usize { - let (cols, _) = get_win_size(); - cols + /// Check if output supports colors. + fn colors_enabled(&self) -> bool { + match self.color_mode { + ColorMode::Enabled => self.stdout_isatty, + ColorMode::Forced => true, + ColorMode::Disabled => false, + } } - /// Try to get the number of rows in the current terminal, - /// or assume 24 if it fails. - fn get_rows(&self) -> usize { - let (_, rows) = get_win_size(); - rows - } + // Interactive loop: - fn enable_raw_mode(&self) -> Result<Mode> { + fn enable_raw_mode(&mut self) -> Result<Mode> { use nix::errno::Errno::ENOTTY; - use nix::sys::termios::{BRKINT, CS8, ECHO, ICANON, ICRNL, IEXTEN, INPCK, ISIG, ISTRIP, - IXON, /* OPOST, */ VMIN, VTIME}; + use nix::sys::termios::{ControlFlags, InputFlags, LocalFlags, SpecialCharacterIndices}; if !self.stdin_isatty { try!(Err(nix::Error::from_errno(ENOTTY))); } let original_mode = try!(termios::tcgetattr(STDIN_FILENO)); - let mut raw = original_mode; + let mut raw = original_mode.clone(); // disable BREAK interrupt, CR to NL conversion on input, // input parity check, strip high bit (bit 8), output flow control - raw.c_iflag &= !(BRKINT | ICRNL | INPCK | ISTRIP | IXON); + raw.input_flags &= !(InputFlags::BRKINT + | InputFlags::ICRNL + | InputFlags::INPCK + | InputFlags::ISTRIP + | InputFlags::IXON); // we don't want raw output, it turns newlines into straight linefeeds - // raw.c_oflag = raw.c_oflag & !(OPOST); // disable all output processing - raw.c_cflag |= CS8; // character-size mark (8 bits) + // raw.c_oflag = raw.c_oflag & !(OutputFlags::OPOST); // disable all output + // processing + // character-size mark (8 bits) + raw.control_flags |= ControlFlags::CS8; // disable echoing, canonical mode, extended input processing and signals - raw.c_lflag &= !(ECHO | ICANON | IEXTEN | ISIG); - raw.c_cc[VMIN] = 1; // One character-at-a-time input - raw.c_cc[VTIME] = 0; // with blocking read - try!(termios::tcsetattr(STDIN_FILENO, termios::TCSADRAIN, &raw)); + raw.local_flags &= + !(LocalFlags::ECHO | LocalFlags::ICANON | LocalFlags::IEXTEN | LocalFlags::ISIG); + raw.control_chars[SpecialCharacterIndices::VMIN as usize] = 1; // One character-at-a-time input + raw.control_chars[SpecialCharacterIndices::VTIME as usize] = 0; // with blocking read + try!(termios::tcsetattr(STDIN_FILENO, SetArg::TCSADRAIN, &raw)); Ok(original_mode) } @@ -318,33 +657,32 @@ impl Term for PosixTerminal { PosixRawReader::new(config) } - fn create_writer(&self) -> Stdout { - io::stdout() - } - - /// Check if a SIGWINCH signal has been received - fn sigwinch(&self) -> bool { - SIGWINCH.compare_and_swap(true, false, atomic::Ordering::SeqCst) - } - - /// Clear the screen. Used to handle ctrl+l - fn clear_screen(&mut self, w: &mut Write) -> Result<()> { - try!(w.write_all(b"\x1b[H\x1b[2J")); - try!(w.flush()); - Ok(()) + fn create_writer(&self) -> PosixRenderer { + PosixRenderer::new() } } #[cfg(unix)] pub fn suspend() -> Result<()> { - // For macos: - try!(signal::kill(nix::unistd::getppid(), signal::SIGTSTP)); - try!(signal::kill(nix::unistd::getpid(), signal::SIGTSTP)); + use nix::unistd::Pid; + // suspend the whole process group + try!(signal::kill(Pid::from_raw(0), signal::SIGTSTP)); Ok(()) } -#[cfg(all(unix,test))] +#[cfg(all(unix, test))] mod test { + use super::{Position, Renderer}; + use std::io::{self, Stdout}; + + #[test] + fn prompt_with_ansi_escape_codes() { + let out = io::stdout(); + let pos = out.calculate_position("\x1b[1;32m>>\x1b[0m ", Position::default(), 80); + assert_eq!(3, pos.col); + assert_eq!(0, pos.row); + } + #[test] fn test_unsupported_term() { ::std::env::set_var("TERM", "xterm"); diff --git a/src/tty/windows.rs b/src/tty/windows.rs index 7b73e2ff58d31b63477bb33def2885a744f18d31..e3a3aa5e1b78838c7e21a540aaca59ea7939f9b4 100644 --- a/src/tty/windows.rs +++ b/src/tty/windows.rs @@ -3,167 +3,221 @@ use std::io::{self, Stdout, Write}; use std::mem; use std::sync::atomic; -use kernel32; -use winapi; +use unicode_width::UnicodeWidthChar; +use winapi::shared::minwindef::{DWORD, WORD}; +use winapi::um::winnt::{CHAR, HANDLE}; +use winapi::um::{consoleapi, handleapi, processenv, winbase, wincon, winuser}; -use config::Config; +use super::{truncate, Position, RawMode, RawReader, Renderer, Term}; +use config::{ColorMode, Config}; use consts::{self, KeyPress}; use error; +use highlight::Highlighter; +use line_buffer::LineBuffer; use Result; -use super::{RawMode, RawReader, Term}; -const STDIN_FILENO: winapi::DWORD = winapi::STD_INPUT_HANDLE; -const STDOUT_FILENO: winapi::DWORD = winapi::STD_OUTPUT_HANDLE; +const STDIN_FILENO: DWORD = winbase::STD_INPUT_HANDLE; +const STDOUT_FILENO: DWORD = winbase::STD_OUTPUT_HANDLE; -fn get_std_handle(fd: winapi::DWORD) -> Result<winapi::HANDLE> { - let handle = unsafe { kernel32::GetStdHandle(fd) }; - if handle == winapi::INVALID_HANDLE_VALUE { +fn get_std_handle(fd: DWORD) -> Result<HANDLE> { + let handle = unsafe { processenv::GetStdHandle(fd) }; + if handle == handleapi::INVALID_HANDLE_VALUE { try!(Err(io::Error::last_os_error())); } else if handle.is_null() { - try!(Err(io::Error::new(io::ErrorKind::Other, - "no stdio handle available for this process"))); + try!(Err(io::Error::new( + io::ErrorKind::Other, + "no stdio handle available for this process", + ),)); } Ok(handle) } #[macro_export] macro_rules! check { - ($funcall:expr) => { - { + ($funcall:expr) => {{ let rc = unsafe { $funcall }; if rc == 0 { try!(Err(io::Error::last_os_error())); } rc - } - }; + }}; } -fn get_win_size(handle: winapi::HANDLE) -> (usize, usize) { +fn get_win_size(handle: HANDLE) -> (usize, usize) { let mut info = unsafe { mem::zeroed() }; - match unsafe { kernel32::GetConsoleScreenBufferInfo(handle, &mut info) } { + match unsafe { wincon::GetConsoleScreenBufferInfo(handle, &mut info) } { 0 => (80, 24), - _ => (info.dwSize.X as usize, (1 + info.srWindow.Bottom - info.srWindow.Top) as usize), + _ => ( + info.dwSize.X as usize, + (1 + info.srWindow.Bottom - info.srWindow.Top) as usize, + ), // (info.srWindow.Right - info.srWindow.Left + 1) } } -fn get_console_mode(handle: winapi::HANDLE) -> Result<winapi::DWORD> { +fn get_console_mode(handle: HANDLE) -> Result<DWORD> { let mut original_mode = 0; - check!(kernel32::GetConsoleMode(handle, &mut original_mode)); + check!(consoleapi::GetConsoleMode(handle, &mut original_mode)); Ok(original_mode) } pub type Mode = ConsoleMode; -#[derive(Clone,Copy,Debug)] +#[derive(Clone, Copy, Debug)] pub struct ConsoleMode { - original_mode: winapi::DWORD, - stdin_handle: winapi::HANDLE, + original_stdin_mode: DWORD, + stdin_handle: HANDLE, + original_stdout_mode: Option<DWORD>, + stdout_handle: HANDLE, } impl RawMode for Mode { /// Disable RAW mode for the terminal. fn disable_raw_mode(&self) -> Result<()> { - check!(kernel32::SetConsoleMode(self.stdin_handle, self.original_mode)); + check!(consoleapi::SetConsoleMode( + self.stdin_handle, + self.original_stdin_mode, + )); + if let Some(original_stdout_mode) = self.original_stdout_mode { + check!(consoleapi::SetConsoleMode( + self.stdout_handle, + original_stdout_mode, + )); + } Ok(()) } } /// Console input reader pub struct ConsoleRawReader { - handle: winapi::HANDLE, - buf: Option<u16>, + handle: HANDLE, + buf: [u16; 2], } impl ConsoleRawReader { pub fn new() -> Result<ConsoleRawReader> { let handle = try!(get_std_handle(STDIN_FILENO)); Ok(ConsoleRawReader { - handle: handle, - buf: None, - }) + handle, + buf: [0; 2], + }) } } impl RawReader for ConsoleRawReader { - fn next_key(&mut self) -> Result<KeyPress> { + fn next_key(&mut self, _: bool) -> Result<KeyPress> { use std::char::decode_utf16; - // use winapi::{LEFT_ALT_PRESSED, LEFT_CTRL_PRESSED, RIGHT_ALT_PRESSED, RIGHT_CTRL_PRESSED}; - use winapi::{LEFT_ALT_PRESSED, RIGHT_ALT_PRESSED}; + use winapi::um::wincon::{ + LEFT_ALT_PRESSED, LEFT_CTRL_PRESSED, RIGHT_ALT_PRESSED, RIGHT_CTRL_PRESSED, + }; - let mut rec: winapi::INPUT_RECORD = unsafe { mem::zeroed() }; + let mut rec: wincon::INPUT_RECORD = unsafe { mem::zeroed() }; let mut count = 0; + let mut surrogate = false; loop { // TODO GetNumberOfConsoleInputEvents - check!(kernel32::ReadConsoleInputW(self.handle, - &mut rec, - 1 as winapi::DWORD, - &mut count)); - - if rec.EventType == winapi::WINDOW_BUFFER_SIZE_EVENT { + check!(consoleapi::ReadConsoleInputW( + self.handle, + &mut rec, + 1 as DWORD, + &mut count, + )); + + if rec.EventType == wincon::WINDOW_BUFFER_SIZE_EVENT { SIGWINCH.store(true, atomic::Ordering::SeqCst); debug!(target: "rustyline", "SIGWINCH"); return Err(error::ReadlineError::WindowResize); - } else if rec.EventType != winapi::KEY_EVENT { + } else if rec.EventType != wincon::KEY_EVENT { continue; } - let key_event = unsafe { rec.KeyEvent() }; + let key_event = unsafe { rec.Event.KeyEvent() }; // writeln!(io::stderr(), "key_event: {:?}", key_event).unwrap(); - if key_event.bKeyDown == 0 && - key_event.wVirtualKeyCode != winapi::VK_MENU as winapi::WORD { + if key_event.bKeyDown == 0 && key_event.wVirtualKeyCode != winuser::VK_MENU as WORD { continue; } + // key_event.wRepeatCount seems to be always set to 1 (maybe because we only + // read one character at a time) - // let alt_gr = key_event.dwControlKeyState & (LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED) != 0; + let alt_gr = key_event.dwControlKeyState & (LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED) + == (LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED); let alt = key_event.dwControlKeyState & (LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED) != 0; - // let ctrl = key_event.dwControlKeyState & (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED) != 0; - let meta = alt; + let ctrl = key_event.dwControlKeyState & (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED) != 0; + let meta = alt && !alt_gr; - let utf16 = key_event.UnicodeChar; + let utf16 = unsafe { *key_event.uChar.UnicodeChar() }; if utf16 == 0 { match key_event.wVirtualKeyCode as i32 { - winapi::VK_LEFT => return Ok(KeyPress::Left), - winapi::VK_RIGHT => return Ok(KeyPress::Right), - winapi::VK_UP => return Ok(KeyPress::Up), - winapi::VK_DOWN => return Ok(KeyPress::Down), - winapi::VK_DELETE => return Ok(KeyPress::Delete), - winapi::VK_HOME => return Ok(KeyPress::Home), - winapi::VK_END => return Ok(KeyPress::End), - winapi::VK_PRIOR => return Ok(KeyPress::PageUp), - winapi::VK_NEXT => return Ok(KeyPress::PageDown), + winuser::VK_LEFT => { + return Ok(if ctrl { + KeyPress::ControlLeft + } else { + KeyPress::Left + }) + } + winuser::VK_RIGHT => { + return Ok(if ctrl { + KeyPress::ControlRight + } else { + KeyPress::Right + }) + } + winuser::VK_UP => { + return Ok(if ctrl { + KeyPress::ControlUp + } else { + KeyPress::Up + }) + } + winuser::VK_DOWN => { + return Ok(if ctrl { + KeyPress::ControlDown + } else { + KeyPress::Down + }) + } + winuser::VK_DELETE => return Ok(KeyPress::Delete), + winuser::VK_HOME => return Ok(KeyPress::Home), + winuser::VK_END => return Ok(KeyPress::End), + winuser::VK_PRIOR => return Ok(KeyPress::PageUp), + winuser::VK_NEXT => return Ok(KeyPress::PageDown), + winuser::VK_INSERT => return Ok(KeyPress::Insert), + winuser::VK_F1 => return Ok(KeyPress::F(1)), + winuser::VK_F2 => return Ok(KeyPress::F(2)), + winuser::VK_F3 => return Ok(KeyPress::F(3)), + winuser::VK_F4 => return Ok(KeyPress::F(4)), + winuser::VK_F5 => return Ok(KeyPress::F(5)), + winuser::VK_F6 => return Ok(KeyPress::F(6)), + winuser::VK_F7 => return Ok(KeyPress::F(7)), + winuser::VK_F8 => return Ok(KeyPress::F(8)), + winuser::VK_F9 => return Ok(KeyPress::F(9)), + winuser::VK_F10 => return Ok(KeyPress::F(10)), + winuser::VK_F11 => return Ok(KeyPress::F(11)), + winuser::VK_F12 => return Ok(KeyPress::F(12)), + // winuser::VK_BACK is correctly handled because the key_event.UnicodeChar is + // also set. _ => continue, }; } else if utf16 == 27 { return Ok(KeyPress::Esc); } else { - // TODO How to support surrogate pair ? - self.buf = Some(utf16); - let orc = decode_utf16(self).next(); + if utf16 >= 0xD800 && utf16 < 0xDC00 { + surrogate = true; + self.buf[0] = utf16; + continue; + } + let buf = if surrogate { + self.buf[1] = utf16; + &self.buf[..] + } else { + self.buf[0] = utf16; + &self.buf[..1] + }; + let orc = decode_utf16(buf.iter().cloned()).next(); if orc.is_none() { return Err(error::ReadlineError::Eof); } let c = try!(orc.unwrap()); if meta { - return Ok(match c { - '-' => KeyPress::Meta('-'), - '0'...'9' => KeyPress::Meta(c), - '<' => KeyPress::Meta('<'), - '>' => KeyPress::Meta('>'), - 'b' | 'B' => KeyPress::Meta('B'), - 'c' | 'C' => KeyPress::Meta('C'), - 'd' | 'D' => KeyPress::Meta('D'), - 'f' | 'F' => KeyPress::Meta('F'), - 'l' | 'L' => KeyPress::Meta('L'), - 'n' | 'N' => KeyPress::Meta('N'), - 'p' | 'P' => KeyPress::Meta('P'), - 't' | 'T' => KeyPress::Meta('T'), - 'u' | 'U' => KeyPress::Meta('U'), - 'y' | 'Y' => KeyPress::Meta('Y'), - _ => { - debug!(target: "rustyline", "unsupported esc sequence: M-{:?}", c); - KeyPress::UnknownEscSeq - } - }); + return Ok(KeyPress::Meta(c)); } else { return Ok(consts::char_to_key_press(c)); } @@ -172,59 +226,205 @@ impl RawReader for ConsoleRawReader { } } -impl Iterator for ConsoleRawReader { - type Item = u16; - - fn next(&mut self) -> Option<u16> { - let buf = self.buf; - self.buf = None; - buf - } +pub struct ConsoleRenderer { + out: Stdout, + handle: HANDLE, + cols: usize, // Number of columns in terminal } -static SIGWINCH: atomic::AtomicBool = atomic::ATOMIC_BOOL_INIT; - -pub type Terminal = Console; - -#[derive(Clone,Debug)] -pub struct Console { - stdin_isatty: bool, - stdin_handle: winapi::HANDLE, - stdout_handle: winapi::HANDLE, -} +impl ConsoleRenderer { + fn new(handle: HANDLE) -> ConsoleRenderer { + // Multi line editing is enabled by ENABLE_WRAP_AT_EOL_OUTPUT mode + let (cols, _) = get_win_size(handle); + ConsoleRenderer { + out: io::stdout(), + handle, + cols, + } + } -impl Console { - pub fn get_console_screen_buffer_info(&self) -> Result<winapi::CONSOLE_SCREEN_BUFFER_INFO> { + fn get_console_screen_buffer_info(&self) -> Result<wincon::CONSOLE_SCREEN_BUFFER_INFO> { let mut info = unsafe { mem::zeroed() }; - check!(kernel32::GetConsoleScreenBufferInfo(self.stdout_handle, &mut info)); + check!(wincon::GetConsoleScreenBufferInfo(self.handle, &mut info)); Ok(info) } - pub fn set_console_cursor_position(&mut self, pos: winapi::COORD) -> Result<()> { - check!(kernel32::SetConsoleCursorPosition(self.stdout_handle, pos)); + fn set_console_cursor_position(&mut self, pos: wincon::COORD) -> Result<()> { + check!(wincon::SetConsoleCursorPosition(self.handle, pos)); Ok(()) } - pub fn fill_console_output_character(&mut self, - length: winapi::DWORD, - pos: winapi::COORD) - -> Result<()> { + fn clear(&mut self, length: DWORD, pos: wincon::COORD) -> Result<()> { let mut _count = 0; - check!(kernel32::FillConsoleOutputCharacterA(self.stdout_handle, - ' ' as winapi::CHAR, - length, - pos, - &mut _count)); + check!(wincon::FillConsoleOutputCharacterA( + self.handle, + ' ' as CHAR, + length, + pos, + &mut _count, + )); Ok(()) } } +impl Renderer for ConsoleRenderer { + fn move_cursor(&mut self, old: Position, new: Position) -> Result<()> { + let mut info = try!(self.get_console_screen_buffer_info()); + if new.row > old.row { + info.dwCursorPosition.Y += (new.row - old.row) as i16; + } else { + info.dwCursorPosition.Y -= (old.row - new.row) as i16; + } + if new.col > old.col { + info.dwCursorPosition.X += (new.col - old.col) as i16; + } else { + info.dwCursorPosition.X -= (old.col - new.col) as i16; + } + self.set_console_cursor_position(info.dwCursorPosition) + } + + fn refresh_line( + &mut self, + prompt: &str, + prompt_size: Position, + line: &LineBuffer, + hint: Option<String>, + current_row: usize, + old_rows: usize, + highlighter: Option<&Highlighter>, + ) -> Result<(Position, Position)> { + // calculate the position of the end of the input line + let end_pos = self.calculate_position(line, prompt_size); + // calculate the desired position of the cursor + let cursor = self.calculate_position(&line[..line.pos()], prompt_size); + + // position at the start of the prompt, clear to end of previous input + let mut info = try!(self.get_console_screen_buffer_info()); + info.dwCursorPosition.X = 0; + info.dwCursorPosition.Y -= current_row as i16; + try!(self.set_console_cursor_position(info.dwCursorPosition)); + try!(self.clear( + (info.dwSize.X * (old_rows as i16 + 1)) as DWORD, + info.dwCursorPosition, + )); + let mut ab = String::new(); + if let Some(highlighter) = highlighter { + // TODO handle ansi escape code (SetConsoleTextAttribute) + // display the prompt + ab.push_str(&highlighter.highlight_prompt(prompt)); + // display the input line + ab.push_str(&highlighter.highlight(line, line.pos())); + } else { + // display the prompt + ab.push_str(prompt); + // display the input line + ab.push_str(line); + } + // display hint + if let Some(hint) = hint { + let truncate = truncate(&hint, end_pos.col, self.cols); + if let Some(highlighter) = highlighter { + ab.push_str(&highlighter.highlight_hint(truncate)); + } else { + ab.push_str(truncate); + } + } + try!(self.write_and_flush(ab.as_bytes())); + + // position the cursor + let mut info = try!(self.get_console_screen_buffer_info()); + info.dwCursorPosition.X = cursor.col as i16; + info.dwCursorPosition.Y -= (end_pos.row - cursor.row) as i16; + try!(self.set_console_cursor_position(info.dwCursorPosition)); + Ok((cursor, end_pos)) + } + + fn write_and_flush(&mut self, buf: &[u8]) -> Result<()> { + try!(self.out.write_all(buf)); + try!(self.out.flush()); + Ok(()) + } + + /// Characters with 2 column width are correctly handled (not splitted). + fn calculate_position(&self, s: &str, orig: Position) -> Position { + let mut pos = orig; + for c in s.chars() { + let cw = if c == '\n' { + pos.col = 0; + pos.row += 1; + None + } else { + c.width() + }; + if let Some(cw) = cw { + pos.col += cw; + if pos.col > self.cols { + pos.row += 1; + pos.col = cw; + } + } + } + if pos.col == self.cols { + pos.col = 0; + pos.row += 1; + } + pos + } + + /// Clear the screen. Used to handle ctrl+l + fn clear_screen(&mut self) -> Result<()> { + let info = try!(self.get_console_screen_buffer_info()); + let coord = wincon::COORD { X: 0, Y: 0 }; + check!(wincon::SetConsoleCursorPosition(self.handle, coord)); + let n = info.dwSize.X as DWORD * info.dwSize.Y as DWORD; + self.clear(n, coord) + } + + fn sigwinch(&self) -> bool { + SIGWINCH.compare_and_swap(true, false, atomic::Ordering::SeqCst) + } + + /// Try to get the number of columns in the current terminal, + /// or assume 80 if it fails. + fn update_size(&mut self) { + let (cols, _) = get_win_size(self.handle); + self.cols = cols; + } + + fn get_columns(&self) -> usize { + self.cols + } + + /// Try to get the number of rows in the current terminal, + /// or assume 24 if it fails. + fn get_rows(&self) -> usize { + let (_, rows) = get_win_size(self.handle); + rows + } +} + +static SIGWINCH: atomic::AtomicBool = atomic::ATOMIC_BOOL_INIT; + +pub type Terminal = Console; + +#[derive(Clone, Debug)] +pub struct Console { + stdin_isatty: bool, + stdin_handle: HANDLE, + stdout_isatty: bool, + stdout_handle: HANDLE, + color_mode: ColorMode, + ansi_colors_supported: bool, +} + +impl Console {} + impl Term for Console { - type Reader = ConsoleRawReader; - type Writer = Stdout; type Mode = Mode; + type Reader = ConsoleRawReader; + type Writer = ConsoleRenderer; - fn new() -> Console { + fn new(color_mode: ColorMode) -> Console { use std::ptr; let stdin_handle = get_std_handle(STDIN_FILENO); let stdin_isatty = match stdin_handle { @@ -234,12 +434,22 @@ impl Term for Console { } Err(_) => false, }; + let stdout_handle = get_std_handle(STDOUT_FILENO); + let stdout_isatty = match stdout_handle { + Ok(handle) => { + // If this function doesn't fail then fd is a TTY + get_console_mode(handle).is_ok() + } + Err(_) => false, + }; - let stdout_handle = get_std_handle(STDOUT_FILENO).unwrap_or(ptr::null_mut()); Console { - stdin_isatty: stdin_isatty, + stdin_isatty, stdin_handle: stdin_handle.unwrap_or(ptr::null_mut()), - stdout_handle: stdout_handle, + stdout_isatty, + stdout_handle: stdout_handle.unwrap_or(ptr::null_mut()), + color_mode, + ansi_colors_supported: false, } } @@ -252,71 +462,66 @@ impl Term for Console { self.stdin_isatty } + fn colors_enabled(&self) -> bool { + // TODO ANSI Colors & Windows <10 + match self.color_mode { + ColorMode::Enabled => self.stdout_isatty && self.ansi_colors_supported, + ColorMode::Forced => true, + ColorMode::Disabled => false, + } + } + // pub fn install_sigwinch_handler(&mut self) { // See ReadConsoleInputW && WINDOW_BUFFER_SIZE_EVENT // } - /// Try to get the number of columns in the current terminal, - /// or assume 80 if it fails. - fn get_columns(&self) -> usize { - let (cols, _) = get_win_size(self.stdout_handle); - cols - } - - /// Try to get the number of rows in the current terminal, - /// or assume 24 if it fails. - fn get_rows(&self) -> usize { - let (_, rows) = get_win_size(self.stdout_handle); - rows - } - /// Enable RAW mode for the terminal. - fn enable_raw_mode(&self) -> Result<Mode> { + fn enable_raw_mode(&mut self) -> Result<Mode> { if !self.stdin_isatty { - try!(Err(io::Error::new(io::ErrorKind::Other, - "no stdio handle available for this process"))); + try!(Err(io::Error::new( + io::ErrorKind::Other, + "no stdio handle available for this process", + ),)); } - let original_mode = try!(get_console_mode(self.stdin_handle)); + let original_stdin_mode = try!(get_console_mode(self.stdin_handle)); // Disable these modes - let raw = original_mode & - !(winapi::wincon::ENABLE_LINE_INPUT | winapi::wincon::ENABLE_ECHO_INPUT | - winapi::wincon::ENABLE_PROCESSED_INPUT); + let raw = original_stdin_mode & !(wincon::ENABLE_LINE_INPUT + | wincon::ENABLE_ECHO_INPUT + | wincon::ENABLE_PROCESSED_INPUT); // Enable these modes - let raw = raw | winapi::wincon::ENABLE_EXTENDED_FLAGS; - let raw = raw | winapi::wincon::ENABLE_INSERT_MODE; - let raw = raw | winapi::wincon::ENABLE_QUICK_EDIT_MODE; - let raw = raw | winapi::wincon::ENABLE_WINDOW_INPUT; - check!(kernel32::SetConsoleMode(self.stdin_handle, raw)); + let raw = raw | wincon::ENABLE_EXTENDED_FLAGS; + let raw = raw | wincon::ENABLE_INSERT_MODE; + let raw = raw | wincon::ENABLE_QUICK_EDIT_MODE; + let raw = raw | wincon::ENABLE_WINDOW_INPUT; + check!(consoleapi::SetConsoleMode(self.stdin_handle, raw)); + + let original_stdout_mode = if self.stdout_isatty { + let original_stdout_mode = try!(get_console_mode(self.stdout_handle)); + // To enable ANSI colors (Windows 10 only): + // https://docs.microsoft.com/en-us/windows/console/setconsolemode + if original_stdout_mode & wincon::ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0 { + let raw = original_stdout_mode | wincon::ENABLE_VIRTUAL_TERMINAL_PROCESSING; + self.ansi_colors_supported = + unsafe { consoleapi::SetConsoleMode(self.stdout_handle, raw) != 0 }; + } + Some(original_stdout_mode) + } else { + None + }; + Ok(Mode { - original_mode: original_mode, - stdin_handle: self.stdin_handle, - }) + original_stdin_mode, + stdin_handle: self.stdin_handle, + original_stdout_mode, + stdout_handle: self.stdout_handle, + }) } fn create_reader(&self, _: &Config) -> Result<ConsoleRawReader> { ConsoleRawReader::new() } - fn create_writer(&self) -> Stdout { - io::stdout() - } - - fn sigwinch(&self) -> bool { - SIGWINCH.compare_and_swap(true, false, atomic::Ordering::SeqCst) - } - - /// Clear the screen. Used to handle ctrl+l - fn clear_screen(&mut self, _: &mut Write) -> Result<()> { - let info = try!(self.get_console_screen_buffer_info()); - let coord = winapi::COORD { X: 0, Y: 0 }; - check!(kernel32::SetConsoleCursorPosition(self.stdout_handle, coord)); - let mut _count = 0; - let n = info.dwSize.X as winapi::DWORD * info.dwSize.Y as winapi::DWORD; - check!(kernel32::FillConsoleOutputCharacterA(self.stdout_handle, - ' ' as winapi::CHAR, - n, - coord, - &mut _count)); - Ok(()) + fn create_writer(&self) -> ConsoleRenderer { + ConsoleRenderer::new(self.stdout_handle) } } diff --git a/src/undo.rs b/src/undo.rs new file mode 100644 index 0000000000000000000000000000000000000000..7f7c9abddee56902daee306e2cf9ccd193094f5b --- /dev/null +++ b/src/undo.rs @@ -0,0 +1,471 @@ +//! Undo API +use std::fmt::Debug; + +use keymap::RepeatCount; +use line_buffer::{ChangeListener, DeleteListener, Direction, LineBuffer}; +use unicode_segmentation::UnicodeSegmentation; + +enum Change { + Begin, + End, + Insert { + idx: usize, + text: String, + }, // QuotedInsert, SelfInsert, Yank + Delete { + idx: usize, + text: String, + }, /* BackwardDeleteChar, BackwardKillWord, DeleteChar, + * KillLine, KillWholeLine, KillWord, + * UnixLikeDiscard, ViDeleteTo */ + Replace { + idx: usize, + old: String, + new: String, + }, /* CapitalizeWord, Complete, DowncaseWord, Replace, TransposeChars, TransposeWords, + * UpcaseWord, YankPop */ +} + +impl Change { + fn undo(&self, line: &mut LineBuffer) { + match *self { + Change::Begin | Change::End => { + unreachable!(); + } + Change::Insert { idx, ref text } => { + line.delete_range(idx..idx + text.len()); + } + Change::Delete { idx, ref text } => { + line.insert_str(idx, text); + line.set_pos(idx + text.len()); + } + Change::Replace { + idx, + ref old, + ref new, + } => { + line.replace(idx..idx + new.len(), old); + } + } + } + + #[cfg(test)] + fn redo(&self, line: &mut LineBuffer) { + match *self { + Change::Begin | Change::End => { + unreachable!(); + } + Change::Insert { idx, ref text } => { + line.insert_str(idx, text); + } + Change::Delete { idx, ref text } => { + line.delete_range(idx..idx + text.len()); + } + Change::Replace { + idx, + ref old, + ref new, + } => { + line.replace(idx..idx + old.len(), new); + } + } + } + + fn insert_seq(&self, indx: usize) -> bool { + if let Change::Insert { idx, ref text } = *self { + idx + text.len() == indx + } else { + false + } + } + + fn delete_seq(&self, indx: usize, len: usize) -> bool { + if let Change::Delete { idx, .. } = *self { + // delete or backspace + idx == indx || idx == indx + len + } else { + false + } + } + + fn replace_seq(&self, indx: usize) -> bool { + if let Change::Replace { idx, ref new, .. } = *self { + idx + new.len() == indx + } else { + false + } + } +} + +pub struct Changeset { + undo_group_level: u32, + undos: Vec<Change>, // undoable changes + redos: Vec<Change>, // undone changes, redoable +} + +impl Changeset { + pub fn new() -> Changeset { + Changeset { + undo_group_level: 0, + undos: Vec::new(), + redos: Vec::new(), + } + } + + pub fn begin(&mut self) -> usize { + debug!(target: "rustyline", "Changeset::begin"); + self.redos.clear(); + let mark = self.undos.len(); + self.undos.push(Change::Begin); + self.undo_group_level += 1; + mark + } + + pub fn end(&mut self) { + debug!(target: "rustyline", "Changeset::end"); + self.redos.clear(); + while self.undo_group_level > 0 { + self.undo_group_level -= 1; + if let Some(&Change::Begin) = self.undos.last() { + // emtpy Begin..End + self.undos.pop(); + } else { + self.undos.push(Change::End); + } + } + } + + fn insert_char(idx: usize, c: char) -> Change { + let mut text = String::new(); + text.push(c); + Change::Insert { idx, text } + } + + pub fn insert(&mut self, idx: usize, c: char) { + debug!(target: "rustyline", "Changeset::insert({}, {:?})", idx, c); + self.redos.clear(); + if !c.is_alphanumeric() || !self.undos.last().map_or(false, |lc| lc.insert_seq(idx)) { + self.undos.push(Self::insert_char(idx, c)); + return; + } + // merge consecutive char insertions when char is alphanumeric + let mut last_change = self.undos.pop().unwrap(); + if let Change::Insert { ref mut text, .. } = last_change { + text.push(c); + } else { + unreachable!(); + } + self.undos.push(last_change); + } + + pub fn insert_str<S: AsRef<str> + Into<String> + Debug>(&mut self, idx: usize, string: S) { + debug!(target: "rustyline", "Changeset::insert_str({}, {:?})", idx, string); + self.redos.clear(); + if string.as_ref().is_empty() { + return; + } + self.undos.push(Change::Insert { + idx, + text: string.into(), + }); + } + + pub fn delete<S: AsRef<str> + Into<String> + Debug>(&mut self, indx: usize, string: S) { + debug!(target: "rustyline", "Changeset::delete({}, {:?})", indx, string); + self.redos.clear(); + if string.as_ref().is_empty() { + return; + } + + if !Self::single_char(string.as_ref()) || !self + .undos + .last() + .map_or(false, |lc| lc.delete_seq(indx, string.as_ref().len())) + { + self.undos.push(Change::Delete { + idx: indx, + text: string.into(), + }); + return; + } + // merge consecutive char deletions when char is alphanumeric + let mut last_change = self.undos.pop().unwrap(); + if let Change::Delete { + ref mut idx, + ref mut text, + } = last_change + { + if *idx == indx { + text.push_str(string.as_ref()); + } else { + text.insert_str(0, string.as_ref()); + *idx = indx; + } + } else { + unreachable!(); + } + self.undos.push(last_change); + } + + fn single_char(s: &str) -> bool { + let mut graphemes = s.graphemes(true); + graphemes.next().map_or(false, |grapheme| { + grapheme.chars().all(|c| c.is_alphanumeric()) + }) && graphemes.next().is_none() + } + + pub fn replace<S: AsRef<str> + Into<String> + Debug>(&mut self, indx: usize, old_: S, new_: S) { + debug!(target: "rustyline", "Changeset::replace({}, {:?}, {:?})", indx, old_, new_); + self.redos.clear(); + + if !self.undos.last().map_or(false, |lc| lc.replace_seq(indx)) { + self.undos.push(Change::Replace { + idx: indx, + old: old_.into(), + new: new_.into(), + }); + return; + } + + // merge consecutive char replacements + let mut last_change = self.undos.pop().unwrap(); + if let Change::Replace { + ref mut old, + ref mut new, + .. + } = last_change + { + old.push_str(old_.as_ref()); + new.push_str(new_.as_ref()); + } else { + unreachable!(); + } + self.undos.push(last_change); + } + + pub fn undo(&mut self, line: &mut LineBuffer, n: RepeatCount) -> bool { + debug!(target: "rustyline", "Changeset::undo"); + let mut count = 0; + let mut waiting_for_begin = 0; + let mut undone = false; + loop { + if let Some(change) = self.undos.pop() { + match change { + Change::Begin => { + waiting_for_begin -= 1; + } + Change::End => { + waiting_for_begin += 1; + } + _ => { + change.undo(line); + undone = true; + } + }; + self.redos.push(change); + } else { + break; + } + if waiting_for_begin <= 0 { + count += 1; + if count >= n { + break; + } + } + } + undone + } + + pub fn truncate(&mut self, len: usize) { + debug!(target: "rustyline", "Changeset::truncate({})", len); + self.undos.truncate(len); + } + + #[cfg(test)] + pub fn redo(&mut self, line: &mut LineBuffer) -> bool { + let mut waiting_for_end = 0; + let mut redone = false; + loop { + if let Some(change) = self.redos.pop() { + match change { + Change::Begin => { + waiting_for_end += 1; + } + Change::End => { + waiting_for_end -= 1; + } + _ => { + change.redo(line); + redone = true; + } + }; + self.undos.push(change); + } else { + break; + } + if waiting_for_end <= 0 { + break; + } + } + redone + } + + pub fn last_insert(&self) -> Option<String> { + for change in self.undos.iter().rev() { + match change { + Change::Insert { ref text, .. } => return Some(text.to_owned()), + Change::Replace { ref new, .. } => return Some(new.to_owned()), + Change::End => { + continue; + } + _ => { + return None; + } + } + } + None + } +} + +impl DeleteListener for Changeset { + fn start_killing(&mut self) {} + + fn delete(&mut self, idx: usize, string: &str, _: Direction) { + self.delete(idx, string); + } + + fn stop_killing(&mut self) {} +} +impl ChangeListener for Changeset { + fn insert_char(&mut self, idx: usize, c: char) { + self.insert(idx, c); + } + + fn insert_str(&mut self, idx: usize, string: &str) { + self.insert_str(idx, string); + } + + fn replace(&mut self, idx: usize, old: &str, new: &str) { + self.replace(idx, old, new); + } +} + +#[cfg(test)] +mod tests { + use super::Changeset; + use line_buffer::LineBuffer; + + #[test] + fn test_insert_chars() { + let mut cs = Changeset::new(); + cs.insert(0, 'H'); + cs.insert(1, 'i'); + assert_eq!(1, cs.undos.len()); + assert_eq!(0, cs.redos.len()); + cs.insert(0, ' '); + assert_eq!(2, cs.undos.len()); + } + + #[test] + fn test_insert_strings() { + let mut cs = Changeset::new(); + cs.insert_str(0, "Hello"); + cs.insert_str(5, ", "); + assert_eq!(2, cs.undos.len()); + assert_eq!(0, cs.redos.len()); + } + + #[test] + fn test_undo_insert() { + let mut buf = LineBuffer::init("", 0, None); + buf.insert_str(0, "Hello"); + buf.insert_str(5, ", world!"); + let mut cs = Changeset::new(); + assert_eq!(buf.as_str(), "Hello, world!"); + + cs.insert_str(5, ", world!"); + + cs.undo(&mut buf, 1); + assert_eq!(0, cs.undos.len()); + assert_eq!(1, cs.redos.len()); + assert_eq!(buf.as_str(), "Hello"); + + cs.redo(&mut buf); + assert_eq!(1, cs.undos.len()); + assert_eq!(0, cs.redos.len()); + assert_eq!(buf.as_str(), "Hello, world!"); + } + + #[test] + fn test_undo_delete() { + let mut buf = LineBuffer::init("", 0, None); + buf.insert_str(0, "Hello"); + let mut cs = Changeset::new(); + assert_eq!(buf.as_str(), "Hello"); + + cs.delete(5, ", world!"); + + cs.undo(&mut buf, 1); + assert_eq!(buf.as_str(), "Hello, world!"); + + cs.redo(&mut buf); + assert_eq!(buf.as_str(), "Hello"); + } + + #[test] + fn test_delete_chars() { + let mut buf = LineBuffer::init("", 0, None); + buf.insert_str(0, "Hlo"); + + let mut cs = Changeset::new(); + cs.delete(1, "e"); + cs.delete(1, "l"); + assert_eq!(1, cs.undos.len()); + + cs.undo(&mut buf, 1); + assert_eq!(buf.as_str(), "Hello"); + } + + #[test] + fn test_backspace_chars() { + let mut buf = LineBuffer::init("", 0, None); + buf.insert_str(0, "Hlo"); + + let mut cs = Changeset::new(); + cs.delete(2, "l"); + cs.delete(1, "e"); + assert_eq!(1, cs.undos.len()); + + cs.undo(&mut buf, 1); + assert_eq!(buf.as_str(), "Hello"); + } + + #[test] + fn test_undo_replace() { + let mut buf = LineBuffer::init("", 0, None); + buf.insert_str(0, "Hello, world!"); + let mut cs = Changeset::new(); + assert_eq!(buf.as_str(), "Hello, world!"); + + buf.replace(1..5, "i"); + assert_eq!(buf.as_str(), "Hi, world!"); + cs.replace(1, "ello", "i"); + + cs.undo(&mut buf, 1); + assert_eq!(buf.as_str(), "Hello, world!"); + + cs.redo(&mut buf); + assert_eq!(buf.as_str(), "Hi, world!"); + } + + #[test] + fn test_last_insert() { + let mut cs = Changeset::new(); + cs.begin(); + cs.delete(0, "Hello"); + cs.insert_str(0, "Bye"); + cs.end(); + let insert = cs.last_insert(); + assert_eq!(Some("Bye".to_owned()), insert); + } +}