diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000000..fc23b2bb88 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,44 @@ +name: Rust + +on: + push: + branches: [master] + paths: + - '**.rs' + - '**.toml' + - '.github/workflows/rust.yml' + pull_request: + paths: + - '**.rs' + - '**.toml' + - '.github/workflows/rust.yml' + - '*.md' +env: + RUST_BACKTRACE: 1 +jobs: + build: + + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v2 + - uses: hecrj/setup-rust-action@v1 + with: + components: rustfmt, clippy + - name: Build + run: cargo build --workspace --all-targets + - name: Run tests + run: cargo test --workspace --all-targets + - name: Run doctests + run: cargo test --workspace --doc + - name: Test features + if: matrix.os != 'windows-latest' + run: cargo test --workspace --all-targets --all-features + - name: Clippy + run: cargo clippy --workspace -- -D warnings + - name: Format + run: cargo fmt --all -- --check diff --git a/.gitignore b/.gitignore index 188ef41619..56ab180f70 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,3 @@ -# Compiled files -*.o -*.so -*.rlib -*.dll - -# Executables -*.exe - # Generated by Cargo /target/ Cargo.lock diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 711713eb69..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,5 +0,0 @@ -sudo: false -language: rust -script: - - cargo build --verbose - - cargo test --verbose diff --git a/BUGS.md b/BUGS.md index 3a44c5bdcf..f6ac7d870f 100644 --- a/BUGS.md +++ b/BUGS.md @@ -4,7 +4,7 @@ Know issues We would like to introduce an incremental parsing phase (see `tree-sitter`). Because, when you have tokens (which may be as simple as words) or an AST, -completion / suggestion / highlting / validation become easy. +completion / suggestion / highlighting / validation become easy. So we need to send events to a lexer/parser, update `Document` accordingly. And fix `Completer` / `Hinter` / `Highlighter` API such as they have access to `Document`. @@ -16,6 +16,10 @@ Currently, performance is poor because, most of the time, we refresh the whole l We would like to transform events on prompt/line/hint into partial repaint. See `termwiz` design (`Surface`). +See `replxx` refresh delay (`_lastRefreshTime`) or `python-prompt-toolkit` max_render_postpone_time. +https://docs.rs/xi-unicode/0.3.0/xi_unicode/struct.LineBreakIterator.html +https://github.com/xi-editor/xi-editor/blob/master/rust/core-lib/src/linewrap.rs +[vt100](https://docs.rs/vt100/0.12.0/vt100/struct.Screen.html#method.contents_diff) ## Action / Command @@ -24,3 +28,19 @@ To do so, we need to refactor current key event dispatch. See `replxx` design (`ACTION_RESULT`, `action_trait_t`). +## Line wrapping (should be fixed with versions >= 6.1.2) + +On Unix platform, we assume that `auto_right_margin` (`am`) is enabled. +And on Windows, we activate `ENABLE_WRAP_AT_EOL_OUTPUT`. +But on Windows 10, `ENABLE_WRAP_AT_EOL_OUTPUT` and `ENABLE_VIRTUAL_TERMINAL_PROCESSING` seems to be incompatible. + +## Colors + +We assume that ANSI colors are supported. +Which is not the case on Windows (except on Windows 10)! + +## Emoji + +https://github.com/kkawakam/rustyline/issues/184 +https://docs.rs/xi-unicode/0.3.0/xi_unicode/trait.EmojiExt.html +https://docs.rs/termwiz/0.11.0/termwiz/cell/fn.grapheme_column_width.html \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index b5c361ba4d..da47a8ca08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustyline" -version = "5.0.5" +version = "9.1.1" authors = ["Katsu Kawakami "] edition = "2018" description = "Rustyline, a readline implementation based on Antirez's Linenoise" @@ -12,41 +12,55 @@ license = "MIT" categories = ["command-line-interface"] [badges] -travis-ci = { repository = "kkawakam/rustyline" } -appveyor = { repository = "kkawakam/rustyline" } maintenance = { status = "actively-developed" } [workspace] members = ["rustyline-derive"] [dependencies] -cfg-if = "0.1" -dirs = { version = "2.0", optional = true } +bitflags = "1.3" +cfg-if = "1.0" +# For file completion +# https://rustsec.org/advisories/RUSTSEC-2020-0053.html +dirs-next = { version = "2.0", optional = true } +# For History +fd-lock = "3.0.0" libc = "0.2" log = "0.4" unicode-width = "0.1" unicode-segmentation = "1.0" memchr = "2.0" +# For custom bindings +# https://rustsec.org/advisories/RUSTSEC-2021-0003.html +smallvec = "1.6.1" +radix_trie = "0.2" +regex = { version = "1.5.4", optional = true } [target.'cfg(unix)'.dependencies] -nix = "0.14" -utf8parse = "0.1" +nix = "0.23" +utf8parse = "0.2" +skim = { version = "0.9", optional = true } [target.'cfg(windows)'.dependencies] -winapi = { version = "0.3", features = ["consoleapi", "handleapi", "minwindef", "processenv", "winbase", "wincon", "winuser"] } +winapi = { version = "0.3", features = ["consoleapi", "handleapi", "minwindef", "processenv", "std", "winbase", "wincon", "winuser"] } +scopeguard = "1.1" +clipboard-win = "4.2.1" [dev-dependencies] -env_logger = "0.7" -tempdir = "0.3" +doc-comment = "0.3" +env_logger = { version = "0.9", default-features = false } +tempfile = "3.1.0" assert_matches = "1.2" -rustyline-derive = { version = "0.2.0", path = "rustyline-derive" } +rustyline-derive = { version = "0.6.0", path = "rustyline-derive" } [features] default = ["with-dirs"] -with-dirs = ["dirs"] +with-dirs = ["dirs-next"] +with-fuzzy = ["skim"] +case_insensitive_history_search = ["regex"] [package.metadata.docs.rs] -features = ["with-dirs"] +features = ["with-dirs", "with-fuzzy"] all-features = false no-default-features = true default-target = "x86_64-unknown-linux-gnu" diff --git a/CustomBinding.md b/CustomBinding.md new file mode 100644 index 0000000000..a35940c318 --- /dev/null +++ b/CustomBinding.md @@ -0,0 +1,68 @@ +## Related topics + +* [Multiple commands for a keybinding](https://github.com/kkawakam/rustyline/issues/306) and + [Conditional Bind Sequences](https://github.com/kkawakam/rustyline/issues/269) : original issues +* [Conditional Bind Sequences](https://github.com/kkawakam/rustyline/pull/293) : incomplete proposal +* [Add `Cmd::Yield` for complex custom bindings](https://github.com/kkawakam/rustyline/pull/342) : another proposal +* [Initial invoke trait and auto-indent example](https://github.com/kkawakam/rustyline/pull/466) : a validator is like a custom action triggered indirectly. + +And other issues that should be solved if our design is good: +* [Extend Meta-F,Alt+Right feature for hint partial completion](https://github.com/kkawakam/rustyline/pull/430) +* [Use Arrow-Up to search history with prefix](https://github.com/kkawakam/rustyline/issues/423) +* [Execute Arbitrary Command Via Keybinding](https://github.com/kkawakam/rustyline/issues/418) +* [Use Ctrl-E for hint completion](https://github.com/kkawakam/rustyline/pull/407) +* [Add History Search Behaviour](https://github.com/kkawakam/rustyline/pull/424) +* ... + +## Conditions / Filters +See https://python-prompt-toolkit.readthedocs.io/en/master/pages/advanced_topics/key_bindings.html?highlight=filter#attaching-a-filter-condition + +Some keys/commands may behave differently depending on: + * edit mode (emacs vs vi) + * vi input mode (insert vs replace vs command modes) + * empty line + * cursor position + * repeat count + * original key pressed (when same command is bound to different key) + * hint + * ... + +## More input +Some keys/commands may ask for more input. +I am not sure this point should be tackle here. + +## Multiple / complex actions +For one key/command, we may want to perform multiple actions. +We should ask the undo manager to start a "transaction" before first action and commit it after the last action. +Should we do something specific with the kill ring ? +We should refresh / repaint only when all actions are performed (or if ask explicitly?) depending on cumulated action impacts. +... + +## Misc + +```rust +/// Command / action result +#[derive(Debug, Clone, PartialEq, Copy)] +#[non_exhaustive] +pub enum ActionResult { + // Interrupt / reject user input + // => Err should be fine + //Bail, + /// + Continue, + /// Accept user input (except if `Validator` disagrees) + Return, +} +``` +```rust +bitflags::bitflags! { + #[doc = "Action invocation impacts"] + pub struct Impacts: u8 { + const PUSH_CHAR = 0b0000_0001; + const BEEP = 0b0000_0010; + const MOVE_CURSOR = 0b0000_0100; // State::move_cursor + const REFRESH = 0b0000_1000; // State::refresh_line + const CLEAR_SREEN = 0b0001_0000; // State::clear_screen + } +} +``` diff --git a/History.md b/History.md new file mode 100644 index 0000000000..bbb6efd4c5 --- /dev/null +++ b/History.md @@ -0,0 +1,35 @@ +# Config + max_history_size + +# Current session + +* we should remember (index) of the first line inserted by this session. + - if no line has been inserted => do nothing on save + - reset this index after saving successfully. +* we should remember (path and timestamp) of the file used to initialize/`load` history. + - if path used to save history is the same: + + if timestamp is still the same => we can append only new lines if history has not been truncated. + + update timestamp after saving successfully. +* we should remember (path and timestamp) of the file used to persist/`save` history. + - reset them if `load` is then called with a different path + - update them if `load` is then called with same path. + - update them after saving successfully + - if path used to save history is the same: + + if timestamp is still the same => we can append only new lines if history has not been truncated. + +``` +HistoryInfo + first_add_index: Option, // first line inserted by this session + truncated: bool // + path_info: Option, +``` +``` +PathInfo + path: Path, + modified: SystemTime, +``` + +--- +With `termwiz`, you can define your own `History` backend. +`termwiz` does not specify anything how the history is persisted. +Only how to access / search history entry. \ No newline at end of file diff --git a/Incremental.md b/Incremental.md new file mode 100644 index 0000000000..51e313002c --- /dev/null +++ b/Incremental.md @@ -0,0 +1,52 @@ +## Incremental computation + +We would like to avoid redrawing all row(s) when an event occurs. +Currently, we redraw all row(s) except when: + * a character is inserted at the end of input (and there is no hint and no `highlight_char`), + * only the cursor is moved (input is not touched and no `highlight_char`). + +Ideally, we would like to redraw only impacted row(s) / cell(s). + +### Observable values + +Currently, we assume that highlighting does not impact layout / rendered text size. +So only the following observables impact layout: + * prompt (interactive search, [input mode indicator](https://github.com/kkawakam/rustyline/pull/369)), + * [input mode](https://github.com/kkawakam/rustyline/pull/369), + * line(s) buffer, + * cursor position, + * hint / input validation message, + * screen size (line wrapping), + * [prompt continuation](https://github.com/kkawakam/rustyline/pull/372)s, + * row/wrap count. + +Some other values may impact layout but they are/should be constant: + * tab stop, + +### Line wrapping and highlighting + +Currently, there is no word wrapping (only grapheme wrapping). +But we highlight the whole input at once. +So there is no issue: for example, even if a keyword is wrapped, style is preserved. + +With [prompt continuation](https://github.com/kkawakam/rustyline/pull/372)s, +we (will) interleave user input with continuations. +So we need to preserve style. + +TODO How prompt_toolkit handle this problem ? +Maybe using ANSI sequence directly was a bad idea. If `Highlighter` returns style ranges, +applying style on input slice is easy (and also supporting styles on Windows < 10). + +### Impacts + +Current granularity: + * PUSH_CHAR at end of input + * BEEP + * MOVE_CURSOR + * REFRESH whole input / rows + * CLEAR_SCREEN (+REFRESH) + +Wanted additional granurality: + * PUSH_STRING at end of input + * REFRESH_DIRTY only rows / cells + diff --git a/README.md b/README.md index 6c263fb0e0..5be378b28e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ # RustyLine -[![Build Status](https://travis-ci.org/kkawakam/rustyline.svg?branch=master)](https://travis-ci.org/kkawakam/rustyline) -[![Build Status](https://ci.appveyor.com/api/projects/status/github/kkawakam/rustyline?branch=master&svg=true)](https://ci.appveyor.com/project/kkawakam/rustyline/branch/master) +[![Build Status](https://github.com/kkawakam/rustyline/workflows/Rust/badge.svg)](https://github.com/kkawakam/rustyline/actions) [![dependency status](https://deps.rs/repo/github/kkawakam/rustyline/status.svg)](https://deps.rs/repo/github/kkawakam/rustyline) -[![](http://meritbadge.herokuapp.com/rustyline)](https://crates.io/crates/rustyline) +[![](https://img.shields.io/crates/v/rustyline.svg)](https://crates.io/crates/rustyline) [![Docs](https://docs.rs/rustyline/badge.svg)](https://docs.rs/rustyline) Readline implementation in Rust that is based on [Antirez' Linenoise](https://github.com/antirez/linenoise) @@ -16,11 +15,10 @@ Readline implementation in Rust that is based on [Antirez' Linenoise](https://gi **Note**: * Powershell ISE is not supported, check [issue #56](https://github.com/kkawakam/rustyline/issues/56) * Mintty (Cygwin/MinGW) is not supported +* Highlighting / Colors are not supported on Windows < Windows 10 except with ConEmu and `ColorMode::Forced`. ## Example ```rust -extern crate rustyline; - use rustyline::error::ReadlineError; use rustyline::Editor; @@ -61,7 +59,7 @@ to your `Cargo.toml`: ```toml [dependencies] -rustyline = "5.0.5" +rustyline = "9.1.0" ``` ## Features @@ -201,6 +199,8 @@ $ # current settings of all terminal attributes: $ stty -a $ # key bindings: $ bind -p +$ # print out a terminfo description: +$ infocmp ``` ## Similar projects @@ -215,6 +215,7 @@ Library | Lang | OS | Term | Unicode | History | Comple [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 | +[reedline][] | Rust | Ux/Win | ANSI | Yes | Yes | any | Emacs/vi/bind | No | Yes | Ux/Win | Yes | [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 | [termwiz][] | Rust | Ux/Win | Any | ? | Yes | any | Emacs | No | No | Ux/Win | No | @@ -227,6 +228,7 @@ Rustyline | Rust | Ux/Win | ANSI | Yes | Yes | any [Liner]: https://github.com/redox-os/liner [prompt_toolkit]: https://github.com/jonathanslenders/python-prompt-toolkit [rb-readline]: https://github.com/ConnorAtherton/rb-readline +[reedline]: https://github.com/nushell/reedline [replxx]: https://github.com/AmokHuginnsson/replxx [termwiz]: https://github.com/wez/wezterm/tree/master/termwiz @@ -238,6 +240,9 @@ line instead of horizontally scrolling as more characters are typed. Currently this feature is always enabled and there is no configuration option to disable it. -_This feature does not allow the end user to hit a special key +This feature does not allow the end user to hit a special key sequence and enter a mode where hitting the return key will cause a -literal newline to be added to the input buffer_. +literal newline to be added to the input buffer. + +The way to achieve multi-line editing is to implement the `Validator` +trait. diff --git a/TODO.md b/TODO.md index 7189a31e23..c56198d509 100644 --- a/TODO.md +++ b/TODO.md @@ -16,7 +16,7 @@ Color Completion - [X] Quoted path -- [ ] Windows escape/unescape space in path +- [X] Windows escape/unescape space in path - [ ] file completion & escape/unescape (#106) - [ ] file completion & tilde (#62) - [X] display versus replacement @@ -40,15 +40,16 @@ 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 +- [X] append_history - [ ] history_truncate_file +- [ ] custom persistent storage Input - [ ] Password input (#58) (https://github.com/conradkdotcom/rpassword) (https://github.com/antirez/linenoise/issues/125) - [X] quoted insert (#65) - [ ] Overwrite mode (em-toggle-overwrite, vi-replace-mode, rl_insert_mode) - [ ] Encoding -- [ ] [Ctrl-][Alt-][Shift-] (#121) +- [X] [Ctrl-][Alt-][Shift-] (#121) Layout - [ ] Redraw perf (https://crates.io/crates/cassowary) diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 4506f3af83..0000000000 --- a/appveyor.yml +++ /dev/null @@ -1,16 +0,0 @@ -environment: - TARGET: x86_64-pc-windows-msvc -install: - - 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 - -build: false - -test_script: - - cargo test --verbose - -cache: - - C:\Users\appveyor\.cargo diff --git a/examples/custom_key_bindings.rs b/examples/custom_key_bindings.rs new file mode 100644 index 0000000000..bcca95b165 --- /dev/null +++ b/examples/custom_key_bindings.rs @@ -0,0 +1,117 @@ +use smallvec::smallvec; +use std::borrow::Cow::{self, Borrowed, Owned}; + +use rustyline::highlight::{Highlighter, PromptInfo}; +use rustyline::hint::{Hinter, HistoryHinter}; +use rustyline::{ + Cmd, ConditionalEventHandler, Context, Editor, Event, EventContext, EventHandler, KeyEvent, + RepeatCount, Result, +}; +use rustyline_derive::{Completer, Helper, Validator}; + +#[derive(Completer, Helper, Validator)] +struct MyHelper(HistoryHinter); + +impl Hinter for MyHelper { + type Hint = String; + + fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option { + self.0.hint(line, pos, ctx) + } +} + +impl Highlighter for MyHelper { + fn highlight_prompt<'b, 's: 'b, 'p: 'b>( + &'s self, + prompt: &'p str, + info: PromptInfo<'_>, + ) -> Cow<'b, str> { + if info.is_default() { + Owned(format!("\x1b[1;32m{}\x1b[m", prompt)) + } else { + Borrowed(prompt) + } + } + + fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { + Owned(format!("\x1b[1m{}\x1b[m", hint)) + } +} + +#[derive(Clone)] +struct CompleteHintHandler; +impl ConditionalEventHandler for CompleteHintHandler { + fn handle(&self, evt: &Event, _: RepeatCount, _: bool, ctx: &EventContext) -> Option { + if !ctx.has_hint() { + return None; // default + } + if let Some(k) = evt.get(0) { + #[allow(clippy::if_same_then_else)] + if *k == KeyEvent::ctrl('E') { + Some(Cmd::CompleteHint) + } else if *k == KeyEvent::alt('f') && ctx.line().len() == ctx.pos() { + let text = ctx.hint_text()?; + let mut start = 0; + if let Some(first) = text.chars().next() { + if !first.is_alphanumeric() { + start = text.find(|c: char| c.is_alphanumeric()).unwrap_or_default(); + } + } + + let text = text + .chars() + .enumerate() + .take_while(|(i, c)| *i <= start || c.is_alphanumeric()) + .map(|(_, c)| c) + .collect::(); + + Some(Cmd::Insert(1, text)) + } else { + None + } + } else { + unreachable!() + } + } +} + +struct TabEventHandler; +impl ConditionalEventHandler for TabEventHandler { + fn handle(&self, evt: &Event, n: RepeatCount, _: bool, ctx: &EventContext) -> Option { + debug_assert_eq!(*evt, Event::from(KeyEvent::from('\t'))); + if ctx.line()[..ctx.pos()] + .chars() + .rev() + .next() + .filter(|c| c.is_whitespace()) + .is_some() + { + Some(Cmd::SelfInsert(n, '\t')) + } else { + None // default complete + } + } +} + +fn main() -> Result<()> { + let mut rl = Editor::::new(); + rl.set_helper(Some(MyHelper(HistoryHinter {}))); + + let ceh = Box::new(CompleteHintHandler); + rl.bind_sequence(KeyEvent::ctrl('E'), EventHandler::Conditional(ceh.clone())); + rl.bind_sequence(KeyEvent::alt('f'), EventHandler::Conditional(ceh)); + rl.bind_sequence( + KeyEvent::from('\t'), + EventHandler::Conditional(Box::new(TabEventHandler)), + ); + rl.bind_sequence( + Event::KeySeq(smallvec![KeyEvent::ctrl('X'), KeyEvent::ctrl('E')]), + EventHandler::Simple(Cmd::Suspend), // TODO external editor + ); + + loop { + let line = rl.readline("> ")?; + rl.add_history_entry(line.as_str()); + println!("Line: {}", line); + } +} diff --git a/examples/diy_hints.rs b/examples/diy_hints.rs new file mode 100644 index 0000000000..fb3130be4b --- /dev/null +++ b/examples/diy_hints.rs @@ -0,0 +1,95 @@ +use std::collections::HashSet; + +use rustyline::hint::{Hint, Hinter}; +use rustyline::Context; +use rustyline::{Editor, Result}; +use rustyline_derive::{Completer, Helper, Highlighter, Validator}; + +#[derive(Completer, Helper, Validator, Highlighter)] +struct DIYHinter { + // It's simple example of rustyline, for more efficient, please use ** radix trie ** + hints: HashSet, +} + +#[derive(Hash, Debug, PartialEq, Eq)] +struct CommandHint { + display: String, + complete_up_to: usize, +} + +impl Hint for CommandHint { + fn display(&self) -> &str { + &self.display + } + + fn completion(&self) -> Option<&str> { + if self.complete_up_to > 0 { + Some(&self.display[..self.complete_up_to]) + } else { + None + } + } +} + +impl CommandHint { + fn new(text: &str, complete_up_to: &str) -> CommandHint { + assert!(text.starts_with(complete_up_to)); + CommandHint { + display: text.into(), + complete_up_to: complete_up_to.len(), + } + } + + fn suffix(&self, strip_chars: usize) -> CommandHint { + CommandHint { + display: self.display[strip_chars..].to_owned(), + complete_up_to: self.complete_up_to.saturating_sub(strip_chars), + } + } +} + +impl Hinter for DIYHinter { + type Hint = CommandHint; + + fn hint(&self, line: &str, pos: usize, _ctx: &Context<'_>) -> Option { + if line.is_empty() || pos < line.len() { + return None; + } + + self.hints + .iter() + .filter_map(|hint| { + // expect hint after word complete, like redis cli, add condition: + // line.ends_with(" ") + if hint.display.starts_with(line) { + Some(hint.suffix(pos)) + } else { + None + } + }) + .next() + } +} + +fn diy_hints() -> HashSet { + let mut set = HashSet::new(); + set.insert(CommandHint::new("help", "help")); + set.insert(CommandHint::new("get key", "get ")); + set.insert(CommandHint::new("set key value", "set ")); + set.insert(CommandHint::new("hget key field", "hget ")); + set.insert(CommandHint::new("hset key field value", "hset ")); + set +} + +fn main() -> Result<()> { + println!("This is a DIY hint hack of rustyline"); + let h = DIYHinter { hints: diy_hints() }; + + let mut rl: Editor = Editor::new(); + rl.set_helper(Some(h)); + + loop { + let input = rl.readline("> ")?; + println!("input: {}", input); + } +} diff --git a/examples/example.rs b/examples/example.rs index b9969d55db..49202b0016 100644 --- a/examples/example.rs +++ b/examples/example.rs @@ -1,20 +1,23 @@ -use env_logger; use std::borrow::Cow::{self, Borrowed, Owned}; use rustyline::completion::{Completer, FilenameCompleter, Pair}; use rustyline::config::OutputStreamType; use rustyline::error::ReadlineError; use rustyline::highlight::{Highlighter, MatchingBracketHighlighter}; +use rustyline::highlight::{PromptInfo}; use rustyline::hint::{Hinter, HistoryHinter}; -use rustyline::{Cmd, CompletionType, Config, Context, EditMode, Editor, KeyPress}; +use rustyline::validate::{self, MatchingBracketValidator, Validator}; +use rustyline::{Cmd, CompletionType, Config, Context, EditMode, Editor, KeyEvent}; use rustyline_derive::Helper; #[derive(Helper)] struct MyHelper { completer: FilenameCompleter, highlighter: MatchingBracketHighlighter, + validator: MatchingBracketValidator, hinter: HistoryHinter, colored_prompt: String, + continuation_prompt: String, } impl Completer for MyHelper { @@ -31,6 +34,8 @@ impl Completer for MyHelper { } impl Hinter for MyHelper { + type Hint = String; + fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option { self.hinter.hint(line, pos, ctx) } @@ -40,15 +45,23 @@ impl Highlighter for MyHelper { fn highlight_prompt<'b, 's: 'b, 'p: 'b>( &'s self, prompt: &'p str, - default: bool, + info: PromptInfo<'_>, ) -> Cow<'b, str> { - if default { - Borrowed(&self.colored_prompt) + if info.is_default() { + if info.line_no() > 0 { + Borrowed(&self.continuation_prompt) + } else { + Borrowed(&self.colored_prompt) + } } else { Borrowed(prompt) } } + fn has_continuation_prompt(&self) -> bool { + return true; + } + fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { Owned("\x1b[1m".to_owned() + hint + "\x1b[m") } @@ -62,6 +75,19 @@ impl Highlighter for MyHelper { } } +impl Validator for MyHelper { + fn validate( + &self, + ctx: &mut validate::ValidationContext, + ) -> rustyline::Result { + self.validator.validate(ctx) + } + + fn validate_while_typing(&self) -> bool { + self.validator.validate_while_typing() + } +} + // To debug rustyline: // RUST_LOG=rustyline=debug cargo run --example example 2> debug.log fn main() -> rustyline::Result<()> { @@ -76,18 +102,20 @@ fn main() -> rustyline::Result<()> { completer: FilenameCompleter::new(), highlighter: MatchingBracketHighlighter::new(), hinter: HistoryHinter {}, - colored_prompt: "".to_owned(), + colored_prompt: " 0> ".to_owned(), + continuation_prompt: "\x1b[1;32m...> \x1b[0m".to_owned(), + validator: MatchingBracketValidator::new(), }; let mut rl = Editor::with_config(config); rl.set_helper(Some(h)); - rl.bind_sequence(KeyPress::Meta('N'), Cmd::HistorySearchForward); - rl.bind_sequence(KeyPress::Meta('P'), Cmd::HistorySearchBackward); + rl.bind_sequence(KeyEvent::alt('n'), Cmd::HistorySearchForward); + rl.bind_sequence(KeyEvent::alt('p'), Cmd::HistorySearchBackward); if rl.load_history("history.txt").is_err() { println!("No previous history."); } let mut count = 1; loop { - let p = format!("{}> ", count); + let p = format!("{:>3}> ", count); rl.helper_mut().expect("No helper").colored_prompt = format!("\x1b[1;32m{}\x1b[0m", p); let readline = rl.readline(&p); match readline { @@ -96,11 +124,11 @@ fn main() -> rustyline::Result<()> { println!("Line: {}", line); } Err(ReadlineError::Interrupted) => { - println!("CTRL-C"); + println!("Interrupted"); break; } Err(ReadlineError::Eof) => { - println!("CTRL-D"); + println!("Encountered Eof"); break; } Err(err) => { @@ -110,5 +138,5 @@ fn main() -> rustyline::Result<()> { } count += 1; } - rl.save_history("history.txt") + rl.append_history("history.txt") } diff --git a/examples/input_multiline.rs b/examples/input_multiline.rs new file mode 100644 index 0000000000..70a9cc8e35 --- /dev/null +++ b/examples/input_multiline.rs @@ -0,0 +1,29 @@ +use rustyline::validate::{ + MatchingBracketValidator, ValidationContext, ValidationResult, Validator, +}; +use rustyline::{Editor, Result}; +use rustyline_derive::{Completer, Helper, Highlighter, Hinter}; + +#[derive(Completer, Helper, Highlighter, Hinter)] +struct InputValidator { + brackets: MatchingBracketValidator, +} + +impl Validator for InputValidator { + fn validate(&self, ctx: &mut ValidationContext) -> Result { + self.brackets.validate(ctx) + } +} + +fn main() -> Result<()> { + let h = InputValidator { + brackets: MatchingBracketValidator::new(), + }; + let mut rl = Editor::new(); + rl.set_helper(Some(h)); + + let input = rl.readline("> ")?; + println!("Input: {}", input); + + Ok(()) +} diff --git a/examples/input_validation.rs b/examples/input_validation.rs new file mode 100644 index 0000000000..c80a34dade --- /dev/null +++ b/examples/input_validation.rs @@ -0,0 +1,31 @@ +use rustyline::validate::{ValidationContext, ValidationResult, Validator}; +use rustyline::{Editor, Result}; +use rustyline_derive::{Completer, Helper, Highlighter, Hinter}; + +#[derive(Completer, Helper, Highlighter, Hinter)] +struct InputValidator {} + +impl Validator for InputValidator { + fn validate(&self, ctx: &mut ValidationContext) -> Result { + use ValidationResult::{Incomplete, Invalid, Valid}; + let input = ctx.input(); + let result = if !input.starts_with("SELECT") { + Invalid(Some(" --< Expect: SELECT stmt".to_owned())) + } else if !input.ends_with(';') { + Incomplete + } else { + Valid(None) + }; + Ok(result) + } +} + +fn main() -> Result<()> { + let h = InputValidator {}; + let mut rl = Editor::new(); + rl.set_helper(Some(h)); + + let input = rl.readline("> ")?; + println!("Input: {}", input); + Ok(()) +} diff --git a/examples/minimal.rs b/examples/minimal.rs new file mode 100644 index 0000000000..de84a593d6 --- /dev/null +++ b/examples/minimal.rs @@ -0,0 +1,10 @@ +use rustyline::{Editor, Result}; + +/// Minimal REPL +fn main() -> Result<()> { + let mut rl = Editor::<()>::new(); + loop { + let line = rl.readline("> ")?; // read + println!("Line: {}", line); // eval / print + } // loop +} diff --git a/examples/numeric_input.rs b/examples/numeric_input.rs new file mode 100644 index 0000000000..bdb7625f73 --- /dev/null +++ b/examples/numeric_input.rs @@ -0,0 +1,33 @@ +use rustyline::{ + Cmd, ConditionalEventHandler, Editor, Event, EventContext, EventHandler, KeyCode, KeyEvent, + Modifiers, RepeatCount, Result, +}; + +struct FilteringEventHandler; +impl ConditionalEventHandler for FilteringEventHandler { + fn handle(&self, evt: &Event, _: RepeatCount, _: bool, _: &EventContext) -> Option { + if let Some(KeyEvent(KeyCode::Char(c), m)) = evt.get(0) { + if m.contains(Modifiers::CTRL) || m.contains(Modifiers::ALT) || c.is_ascii_digit() { + None + } else { + Some(Cmd::Noop) // filter out invalid input + } + } else { + None + } + } +} + +fn main() -> Result<()> { + let mut rl = Editor::<()>::new(); + + rl.bind_sequence( + Event::Any, + EventHandler::Conditional(Box::new(FilteringEventHandler)), + ); + + loop { + let line = rl.readline("> ")?; + println!("Num: {}", line); + } +} diff --git a/examples/read_password.rs b/examples/read_password.rs index 54448e130b..435e6e3474 100644 --- a/examples/read_password.rs +++ b/examples/read_password.rs @@ -2,10 +2,10 @@ use std::borrow::Cow::{self, Borrowed, Owned}; use rustyline::config::Configurer; use rustyline::highlight::Highlighter; -use rustyline::{ColorMode, Editor}; -use rustyline_derive::{Completer, Helper, Hinter}; +use rustyline::{ColorMode, Editor, Result}; +use rustyline_derive::{Completer, Helper, Hinter, Validator}; -#[derive(Completer, Helper, Hinter)] +#[derive(Completer, Helper, Hinter, Validator)] struct MaskingHighlighter { masking: bool, } @@ -25,7 +25,7 @@ impl Highlighter for MaskingHighlighter { } } -fn main() -> rustyline::Result<()> { +fn main() -> Result<()> { println!("This is just a hack. Reading passwords securely requires more than that."); let h = MaskingHighlighter { masking: false }; let mut rl = Editor::new(); diff --git a/rustfmt.toml b/rustfmt.toml index 83697e3d73..404622967c 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -2,3 +2,12 @@ wrap_comments = true format_strings = true error_on_unformatted = false reorder_impl_items = true + +condense_wildcard_suffixes = true +format_code_in_doc_comments = true +format_macro_matchers = true +format_macro_bodies = true +#merge_imports = true +normalize_doc_attributes = true +use_field_init_shorthand = true +use_try_shorthand = true \ No newline at end of file diff --git a/rustyline-derive/Cargo.toml b/rustyline-derive/Cargo.toml index 9aa723a464..f882db0526 100644 --- a/rustyline-derive/Cargo.toml +++ b/rustyline-derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustyline-derive" -version = "0.2.0" +version = "0.6.0" authors = ["gwenn"] edition = "2018" description = "Rustyline macros implementation of #[derive(Completer, Helper, Hinter, Highlighter)]" @@ -19,5 +19,5 @@ maintenance = { status = "actively-developed" } proc-macro = true [dependencies] -syn = "1.0" -quote = "1.0" +syn = { version = "1.0", default-features = false, features = ["derive", "parsing", "printing", "proc-macro"] } +quote = { version = "1.0", default-features = false } diff --git a/rustyline-derive/src/lib.rs b/rustyline-derive/src/lib.rs index fd1af8a040..bcf6451d35 100644 --- a/rustyline-derive/src/lib.rs +++ b/rustyline-derive/src/lib.rs @@ -1,5 +1,3 @@ -extern crate proc_macro; - use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, DeriveInput}; @@ -11,8 +9,9 @@ pub fn completer_macro_derive(input: TokenStream) -> TokenStream { let generics = input.generics; let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let expanded = quote! { - impl #impl_generics rustyline::completion::Completer for #name #ty_generics #where_clause { - type Candidate = std::string::String; + #[automatically_derived] + impl #impl_generics ::rustyline::completion::Completer for #name #ty_generics #where_clause { + type Candidate = ::std::string::String; } }; TokenStream::from(expanded) @@ -25,7 +24,8 @@ pub fn helper_macro_derive(input: TokenStream) -> TokenStream { let generics = input.generics; let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let expanded = quote! { - impl #impl_generics rustyline::Helper for #name #ty_generics #where_clause { + #[automatically_derived] + impl #impl_generics ::rustyline::Helper for #name #ty_generics #where_clause { } }; TokenStream::from(expanded) @@ -38,7 +38,8 @@ pub fn highlighter_macro_derive(input: TokenStream) -> TokenStream { let generics = input.generics; let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let expanded = quote! { - impl #impl_generics rustyline::highlight::Highlighter for #name #ty_generics #where_clause { + #[automatically_derived] + impl #impl_generics ::rustyline::highlight::Highlighter for #name #ty_generics #where_clause { } }; TokenStream::from(expanded) @@ -51,7 +52,23 @@ pub fn hinter_macro_derive(input: TokenStream) -> TokenStream { let generics = input.generics; let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let expanded = quote! { - impl #impl_generics rustyline::hint::Hinter for #name #ty_generics #where_clause { + #[automatically_derived] + impl #impl_generics ::rustyline::hint::Hinter for #name #ty_generics #where_clause { + type Hint = ::std::string::String; + } + }; + TokenStream::from(expanded) +} + +#[proc_macro_derive(Validator)] +pub fn validator_macro_derive(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = &input.ident; + let generics = input.generics; + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + let expanded = quote! { + #[automatically_derived] + impl #impl_generics ::rustyline::validate::Validator for #name #ty_generics #where_clause { } }; TokenStream::from(expanded) diff --git a/src/binding.rs b/src/binding.rs new file mode 100644 index 0000000000..9ce395fd80 --- /dev/null +++ b/src/binding.rs @@ -0,0 +1,244 @@ +/// Custom event handlers +use crate::{ + Cmd, EditMode, InputMode, InputState, KeyCode, KeyEvent, Modifiers, Refresher, RepeatCount, +}; + +use radix_trie::TrieKey; +use smallvec::{smallvec, SmallVec}; + +/// Input event +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Event { + /// Wildcard. + /// Useful if you want to filter out some keys. + Any, + /// Key sequence + // TODO Validate 2 ? + KeySeq(SmallVec<[KeyEvent; 2]>), + /// TODO Mouse event + Mouse(), +} + +impl Event { + /// See [`KeyEvent::normalize`] + pub(crate) fn normalize(mut self) -> Self { + if let Event::KeySeq(ref mut keys) = self { + for key in keys.iter_mut() { + *key = KeyEvent::normalize(*key); + } + } + self + } + + /// Return `i`th key event + pub fn get(&self, i: usize) -> Option<&KeyEvent> { + if let Event::KeySeq(ref ks) = self { + ks.get(i) + } else { + None + } + } +} + +impl From for Event { + fn from(k: KeyEvent) -> Event { + Event::KeySeq(smallvec![k]) + } +} + +const BASE: u32 = 0x0010ffff + 1; +const BASE_CONTROL: u32 = 0x02000000; +const BASE_META: u32 = 0x04000000; +const BASE_SHIFT: u32 = 0x01000000; +const ESCAPE: u32 = 27; +const PAGE_UP: u32 = BASE + 1; +const PAGE_DOWN: u32 = PAGE_UP + 1; +const DOWN: u32 = PAGE_DOWN + 1; +const UP: u32 = DOWN + 1; +const LEFT: u32 = UP + 1; +const RIGHT: u32 = LEFT + 1; +const HOME: u32 = RIGHT + 1; +const END: u32 = HOME + 1; +const DELETE: u32 = END + 1; +const INSERT: u32 = DELETE + 1; +//const F1: u32 = INSERT + 1; +const MOUSE: u32 = /* F24 + 1 */ INSERT + 25; +const PASTE_START: u32 = MOUSE + 1; +const PASTE_FINISH: u32 = PASTE_START + 1; +const ANY: u32 = PASTE_FINISH + 1; + +impl KeyEvent { + fn encode(&self) -> u32 { + let mut u = match self.0 { + KeyCode::UnknownEscSeq => 0, + KeyCode::Backspace => u32::from('\x7f'), + KeyCode::BackTab => u32::from('\t') | BASE_SHIFT, + KeyCode::BracketedPasteStart => PASTE_START, + KeyCode::BracketedPasteEnd => PASTE_FINISH, + KeyCode::Char(c) => u32::from(c), + KeyCode::Delete => DELETE, + KeyCode::Down => DOWN, + KeyCode::End => END, + KeyCode::Enter => u32::from('\r'), + KeyCode::F(i) => INSERT + i as u32, + KeyCode::Esc => ESCAPE, + KeyCode::Home => HOME, + KeyCode::Insert => INSERT, + KeyCode::Left => LEFT, + KeyCode::Null => 0, + KeyCode::PageDown => PAGE_DOWN, + KeyCode::PageUp => PAGE_UP, + KeyCode::Right => RIGHT, + KeyCode::Tab => u32::from('\t'), + KeyCode::Up => UP, + }; + if self.1.contains(Modifiers::CTRL) { + u |= BASE_CONTROL; + } + if self.1.contains(Modifiers::ALT) { + u |= BASE_META; + } + if self.1.contains(Modifiers::SHIFT) { + u |= BASE_SHIFT; + } + u + } +} + +impl TrieKey for Event { + fn encode_bytes(&self) -> Vec { + match self { + Event::Any => ANY.to_be_bytes().to_vec(), + Event::KeySeq(keys) => { + let mut dst = Vec::with_capacity(keys.len() * 4); + for key in keys { + dst.extend_from_slice(&key.encode().to_be_bytes()); + } + dst + } + Event::Mouse() => MOUSE.to_be_bytes().to_vec(), + } + } +} + +/// Event handler +pub enum EventHandler { + /// unconditional command + Simple(Cmd), + /// handler behaviour depends on input state + Conditional(Box), + /* invoke multiple actions + * TODO Macro(), */ +} + +impl From for EventHandler { + fn from(c: Cmd) -> EventHandler { + EventHandler::Simple(c) + } +} + +/// Give access to user input. +pub struct EventContext<'r> { + mode: EditMode, + input_mode: InputMode, + wrt: &'r dyn Refresher, +} + +impl<'r> EventContext<'r> { + pub(crate) fn new(is: &InputState, wrt: &'r dyn Refresher) -> Self { + EventContext { + mode: is.mode, + input_mode: is.input_mode, + wrt, + } + } + + /// emacs or vi mode + pub fn mode(&self) -> EditMode { + self.mode + } + + /// vi input mode + pub fn input_mode(&self) -> InputMode { + self.input_mode + } + + /// Returns `true` if there is a hint displayed. + pub fn has_hint(&self) -> bool { + self.wrt.has_hint() + } + + /// Returns the hint text that is shown after the current cursor position. + pub fn hint_text(&self) -> Option<&str> { + self.wrt.hint_text() + } + + /// currently edited line + pub fn line(&self) -> &str { + self.wrt.line() + } + + /// Current cursor position (byte position) + pub fn pos(&self) -> usize { + self.wrt.pos() + } +} + +/// May behave differently depending on: +/// * edit mode (emacs vs vi) +/// * vi input mode (insert vs replace vs command modes) +/// * empty line +/// * cursor position +/// * repeat count +/// * original key pressed (when same command is bound to different key) +/// * hint +/// * ... +pub trait ConditionalEventHandler: Send + Sync { + /// Takes the current input state and + /// returns the command to be performed or `None` to perform the default + /// one. + fn handle( + &self, + evt: &Event, + n: RepeatCount, + positive: bool, + ctx: &EventContext, + ) -> Option; +} + +#[cfg(test)] +mod test { + use super::{Event, EventHandler}; + use crate::{Cmd, KeyCode, KeyEvent, Modifiers}; + use radix_trie::Trie; + use smallvec::smallvec; + + #[test] + fn encode() { + let mut trie = Trie::new(); + let evt = Event::KeySeq(smallvec![KeyEvent::ctrl('X'), KeyEvent::ctrl('E')]); + trie.insert(evt.clone(), EventHandler::from(Cmd::Noop)); + let prefix = Event::from(KeyEvent::ctrl('X')); + let subtrie = trie.get_raw_descendant(&prefix); + assert!(subtrie.is_some()); + let subtrie = subtrie.unwrap(); + let sub_result = subtrie.get(&evt); + assert!(sub_result.is_ok()); + assert!(sub_result.unwrap().is_some()); + let prefix = Event::from(KeyEvent::ctrl('O')); + let subtrie = trie.get_raw_descendant(&prefix); + assert!(subtrie.is_none()) + } + + #[test] + fn no_collision() { + use {Event as E, EventHandler as H, KeyCode as C, KeyEvent as K, Modifiers as M}; + let mut trie = Trie::new(); + trie.insert(E::from(K(C::Backspace, M::NONE)), H::from(Cmd::Noop)); + trie.insert(E::from(K(C::Enter, M::NONE)), H::from(Cmd::Noop)); + trie.insert(E::from(K(C::Tab, M::NONE)), H::from(Cmd::Noop)); + trie.insert(E::from(K(C::Backspace, M::CTRL)), H::from(Cmd::Noop)); + trie.insert(E::from(K(C::Enter, M::CTRL)), H::from(Cmd::Noop)); + trie.insert(E::from(K(C::Tab, M::CTRL)), H::from(Cmd::Noop)); + } +} diff --git a/src/command.rs b/src/command.rs new file mode 100644 index 0000000000..89a0b9bd3f --- /dev/null +++ b/src/command.rs @@ -0,0 +1,233 @@ +use std::sync::{Arc, Mutex}; + +use crate::complete_hint_line; +use crate::config::Config; +use crate::edit::State; +use crate::error; +use crate::history::SearchDirection; +use crate::keymap::{Anchor, At, Cmd, Movement, Word}; +use crate::keymap::{InputState, Refresher}; +use crate::kill_ring::{KillRing, Mode}; +use crate::line_buffer::WordAction; +use crate::{Helper, Result}; + +pub enum Status { + Proceed, + Submit, +} + +pub fn execute( + cmd: Cmd, + s: &mut State<'_, '_, H>, + input_state: &InputState, + kill_ring: &Arc>, + config: &Config, +) -> Result { + use Status::*; + + match cmd { + Cmd::CompleteHint => { + complete_hint_line(s)?; + } + Cmd::SelfInsert(n, c) => { + s.edit_insert(c, n)?; + } + Cmd::Insert(n, text) => { + s.edit_yank(input_state, &text, Anchor::Before, n)?; + } + Cmd::Move(Movement::BeginningOfLine) => { + // Move to the beginning of line. + s.edit_move_home()? + } + Cmd::Move(Movement::ViFirstPrint) => { + s.edit_move_home()?; + s.edit_move_to_next_word(At::Start, Word::Big, 1)? + } + Cmd::Move(Movement::BackwardChar(n)) => { + // Move back a character. + s.edit_move_backward(n)? + } + Cmd::ReplaceChar(n, c) => s.edit_replace_char(c, n)?, + Cmd::Replace(mvt, text) => { + s.edit_kill(&mvt)?; + if let Some(text) = text { + s.edit_insert_text(&text)? + } + } + Cmd::Overwrite(c) => { + s.edit_overwrite_char(c)?; + } + Cmd::EndOfFile => { + if s.has_hint() || !s.is_default_prompt() { + // Force a refresh without hints to leave the previous + // line as the user typed it after a newline. + s.refresh_line_with_msg(None)?; + } + if s.line.is_empty() { + return Err(error::ReadlineError::Eof); + } else if !input_state.is_emacs_mode() { + return Ok(Submit); + } + } + Cmd::Move(Movement::EndOfLine) => { + // Move to the end of line. + s.edit_move_end()? + } + Cmd::Move(Movement::ForwardChar(n)) => { + // Move forward a character. + s.edit_move_forward(n)? + } + Cmd::ClearScreen => { + // Clear the screen leaving the current line at the top of the screen. + s.clear_screen()?; + s.refresh_line()? + } + Cmd::NextHistory => { + // Fetch the next command from the history list. + s.edit_history_next(false)? + } + Cmd::PreviousHistory => { + // Fetch the previous command from the history list. + s.edit_history_next(true)? + } + Cmd::LineUpOrPreviousHistory(n) => { + if !s.edit_move_line_up(n)? { + s.edit_history_next(true)? + } + } + Cmd::LineDownOrNextHistory(n) => { + if !s.edit_move_line_down(n)? { + s.edit_history_next(false)? + } + } + Cmd::HistorySearchBackward => s.edit_history_search(SearchDirection::Reverse)?, + Cmd::HistorySearchForward => s.edit_history_search(SearchDirection::Forward)?, + Cmd::TransposeChars => { + // Exchange the char before cursor with the character at cursor. + s.edit_transpose_chars()? + } + Cmd::Yank(n, anchor) => { + // retrieve (yank) last item killed + let mut kill_ring = kill_ring.lock().unwrap(); + if let Some(text) = kill_ring.yank() { + s.edit_yank(input_state, text, anchor, n)? + } + } + Cmd::ViYankTo(ref mvt) => { + if let Some(text) = s.line.copy(mvt) { + let mut kill_ring = kill_ring.lock().unwrap(); + kill_ring.kill(&text, Mode::Append) + } + } + Cmd::AcceptLine | Cmd::AcceptOrInsertLine { .. } | Cmd::Newline => { + if s.has_hint() || !s.is_default_prompt() { + // Force a refresh without hints to leave the previous + // line as the user typed it after a newline. + s.refresh_line_with_msg(None)?; + } + let validation_result = s.validate()?; + let valid = validation_result.is_valid(); + let end = s.line.is_end_of_input(); + match (cmd, valid, end) { + (Cmd::AcceptLine, ..) + | (Cmd::AcceptOrInsertLine { .. }, true, true) + | ( + Cmd::AcceptOrInsertLine { + accept_in_the_middle: true, + }, + true, + _, + ) => { + return Ok(Submit); + } + (Cmd::Newline, ..) + | (Cmd::AcceptOrInsertLine { .. }, false, _) + | (Cmd::AcceptOrInsertLine { .. }, true, false) => { + if valid || !validation_result.has_message() { + s.edit_insert('\n', 1)?; + } + } + _ => unreachable!(), + } + } + Cmd::BeginningOfHistory => { + // move to first entry in history + s.edit_history(true)? + } + Cmd::EndOfHistory => { + // move to last entry in history + s.edit_history(false)? + } + Cmd::Move(Movement::BackwardWord(n, word_def)) => { + // move backwards one word + s.edit_move_to_prev_word(word_def, n)? + } + Cmd::CapitalizeWord => { + // capitalize word after point + s.edit_word(WordAction::Capitalize)? + } + Cmd::Kill(ref mvt) => { + s.edit_kill(mvt)?; + } + Cmd::Move(Movement::ForwardWord(n, at, word_def)) => { + // move forwards one word + s.edit_move_to_next_word(at, word_def, n)? + } + Cmd::Move(Movement::LineUp(n)) => { + s.edit_move_line_up(n)?; + } + Cmd::Move(Movement::LineDown(n)) => { + s.edit_move_line_down(n)?; + } + Cmd::Move(Movement::BeginningOfBuffer) => { + // Move to the start of the buffer. + s.edit_move_buffer_start()? + } + Cmd::Move(Movement::EndOfBuffer) => { + // Move to the end of the buffer. + s.edit_move_buffer_end()? + } + Cmd::DowncaseWord => { + // lowercase word after point + s.edit_word(WordAction::Lowercase)? + } + Cmd::TransposeWords(n) => { + // transpose words + s.edit_transpose_words(n)? + } + Cmd::UpcaseWord => { + // uppercase word after point + s.edit_word(WordAction::Uppercase)? + } + Cmd::YankPop => { + // yank-pop + let mut kill_ring = kill_ring.lock().unwrap(); + if let Some((yank_size, text)) = kill_ring.yank_pop() { + s.edit_yank_pop(yank_size, text)? + } + } + Cmd::Move(Movement::ViCharSearch(n, cs)) => s.edit_move_to(cs, n)?, + Cmd::Undo(n) => { + if s.changes.borrow_mut().undo(&mut s.line, n) { + s.refresh_line()?; + } + } + Cmd::Dedent(mvt) => { + s.edit_indent(&mvt, config.indent_size(), true)?; + } + Cmd::Indent(mvt) => { + s.edit_indent(&mvt, config.indent_size(), false)?; + } + Cmd::Interrupt => { + // Move to end, in case cursor was in the middle of the + // line, so that next thing application prints goes after + // the input + s.move_cursor_to_end()?; + return Err(error::ReadlineError::Interrupted); + } + _ => { + // Ignore the character typed. + } + } + Ok(Proceed) +} diff --git a/src/completion.rs b/src/completion.rs index eadae06538..c31d5a94c3 100644 --- a/src/completion.rs +++ b/src/completion.rs @@ -7,11 +7,6 @@ use crate::line_buffer::LineBuffer; use crate::{Context, Result}; use memchr::memchr; -// TODO: let the implementers choose/find word boundaries ??? -// (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. @@ -30,8 +25,32 @@ impl Candidate for String { } } +/// #[deprecated = "Unusable"] +impl Candidate for str { + fn display(&self) -> &str { + self + } + + fn replacement(&self) -> &str { + self + } +} + +impl Candidate for &'_ str { + fn display(&self) -> &str { + self + } + + fn replacement(&self) -> &str { + self + } +} + +/// Completion candidate pair pub struct Pair { + /// Text to display when listing alternatives. pub display: String, + /// Text to insert in line. pub replacement: String, } @@ -45,17 +64,23 @@ impl Candidate for Pair { } } +// TODO: let the implementers customize how the candidate(s) are displayed +// https://github.com/kkawakam/rustyline/issues/302 + /// To be called for tab-completion. pub trait Completer { + /// Specific completion candidate. type Candidate: Candidate; + // TODO: let the implementers choose/find word boundaries ??? => Lexer + /// 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", 11) => Ok((3, vec!["/usr/local/"])) fn complete( - &self, + &self, // FIXME should be `&mut self` line: &str, pos: usize, ctx: &Context<'_>, @@ -149,14 +174,19 @@ cfg_if::cfg_if! { } } +/// Kind of quote. #[derive(Clone, Copy, Debug, PartialEq)] pub enum Quote { + /// Double quote: `"` Double, + /// Single quote: `'` Single, + /// No quote None, } impl FilenameCompleter { + /// Constructor pub fn new() -> Self { Self { break_chars: &DEFAULT_BREAK_CHARS, @@ -164,6 +194,9 @@ impl FilenameCompleter { } } + /// Takes the currently edited `line` with the cursor `pos`ition and + /// returns the start position and the completion candidates for the + /// partial path to be completed. pub fn complete_path(&self, line: &str, pos: usize) -> Result<(usize, Vec)> { let (start, path, esc_char, break_chars, quote) = if let Some((idx, quote)) = find_unclosed_quote(&line[..pos]) { @@ -186,11 +219,13 @@ impl FilenameCompleter { ) } } else { - let (start, path) = extract_word(line, pos, ESCAPE_CHAR, &self.break_chars); + 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 = filename_complete(&path, esc_char, break_chars, quote)?; + let mut matches = filename_complete(&path, esc_char, break_chars, quote); + #[allow(clippy::unnecessary_sort_by)] + matches.sort_by(|a, b| a.display().cmp(b.display())); Ok((start, matches)) } } @@ -283,9 +318,9 @@ fn filename_complete( esc_char: Option, break_chars: &[u8], quote: Quote, -) -> Result> { +) -> Vec { #[cfg(feature = "with-dirs")] - use dirs::home_dir; + use dirs_next::home_dir; use std::env::current_dir; let sep = path::MAIN_SEPARATOR; @@ -327,31 +362,42 @@ fn filename_complete( // if dir doesn't exist, then don't offer any completions if !dir.exists() { - return Ok(entries); + return entries; } // if any of the below IO operations have errors, just ignore them if let Ok(read_dir) = dir.read_dir() { - for entry in read_dir { - if let Ok(entry) = entry { - if let Some(s) = entry.file_name().to_str() { - if s.starts_with(file_name) { - 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 - } + let file_name = normalize(file_name); + for entry in read_dir.flatten() { + if let Some(s) = entry.file_name().to_str() { + let ns = normalize(s); + if ns.starts_with(file_name.as_ref()) { + 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 } } } } - Ok(entries) + entries +} + +#[cfg(any(windows, target_os = "macos"))] +fn normalize(s: &str) -> Cow { + // case insensitive + Cow::Owned(s.to_lowercase()) +} + +#[cfg(not(any(windows, target_os = "macos")))] +fn normalize(s: &str) -> Cow { + Cow::Borrowed(s) } /// Given a `line` and a cursor `pos`ition, @@ -393,11 +439,12 @@ pub fn extract_word<'l>( } } +/// Returns the longest common prefix among all `Candidate::replacement()`s. pub fn longest_common_prefix(candidates: &[C]) -> Option<&str> { if candidates.is_empty() { return None; } else if candidates.len() == 1 { - return Some(&candidates[0].replacement()); + return Some(candidates[0].replacement()); } let mut longest_common_prefix = 0; 'o: loop { @@ -489,12 +536,12 @@ mod tests { let line = "ls '/usr/local/b"; assert_eq!( (4, "/usr/local/b"), - super::extract_word(line, line.len(), Some('\\'), &break_chars) + 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) + super::extract_word(line, line.len(), Some('\\'), break_chars) ); } @@ -520,13 +567,13 @@ mod tests { let input = String::from("/usr/local/b"); assert_eq!( input.clone(), - super::escape(input, Some('\\'), &break_chars, super::Quote::None) + 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, super::Quote::None) + super::escape(input, Some('\\'), break_chars, super::Quote::None) ); } @@ -540,21 +587,21 @@ mod tests { let s = "User"; let c1 = String::from(s); - candidates.push(c1.clone()); + candidates.push(c1); { let lcp = super::longest_common_prefix(&candidates); assert_eq!(Some(s), lcp); } let c2 = String::from("Users"); - candidates.push(c2.clone()); + candidates.push(c2); { let lcp = super::longest_common_prefix(&candidates); assert_eq!(Some(s), lcp); } let c3 = String::from(""); - candidates.push(c3.clone()); + candidates.push(c3); { let lcp = super::longest_common_prefix(&candidates); assert!(lcp.is_none()); @@ -581,4 +628,10 @@ mod tests { super::find_unclosed_quote("\"c:\\users\\All Users\\") ) } + + #[cfg(windows)] + #[test] + pub fn normalize() { + assert_eq!(super::normalize("Windows"), "windows") + } } diff --git a/src/config.rs b/src/config.rs index 0f5783e943..2be6c3264c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -28,9 +28,16 @@ pub struct Config { output_stream: OutputStreamType, /// Horizontal space taken by a tab. tab_stop: usize, + /// Indentation size for indent/dedent commands + indent_size: usize, + /// Check if cursor position is at leftmost before displaying prompt + check_cursor_position: bool, + /// Bracketed paste on unix platform + enable_bracketed_paste: bool, } impl Config { + /// Returns a `Config` builder. pub fn builder() -> Builder { Builder::new() } @@ -72,18 +79,30 @@ impl Config { self.history_ignore_space = yes; } + /// Completion behaviour. + /// + /// By default, `CompletionType::Circular`. pub fn completion_type(&self) -> CompletionType { self.completion_type } + /// When listing completion alternatives, only display + /// one screen of possibilities at a time (used for `CompletionType::List` + /// mode). pub fn completion_prompt_limit(&self) -> usize { self.completion_prompt_limit } + /// Duration (milliseconds) Rustyline will wait for a character when + /// reading an ambiguous key sequence (used for `EditMode::Vi` mode on unix + /// platform). + /// + /// By default, no timeout (-1) or 500ms if `EditMode::Vi` is activated. pub fn keyseq_timeout(&self) -> i32 { self.keyseq_timeout } + /// Emacs or Vi mode pub fn edit_mode(&self) -> EditMode { self.edit_mode } @@ -111,6 +130,9 @@ impl Config { self.color_mode = color_mode; } + /// Tell which output stream should be used: stdout or stderr. + /// + /// By default, stdout is used. pub fn output_stream(&self) -> OutputStreamType { self.output_stream } @@ -120,6 +142,8 @@ impl Config { } /// Horizontal space taken by a tab. + /// + /// By default, 8. pub fn tab_stop(&self) -> usize { self.tab_stop } @@ -127,6 +151,31 @@ impl Config { pub(crate) fn set_tab_stop(&mut self, tab_stop: usize) { self.tab_stop = tab_stop; } + + /// Check if cursor position is at leftmost before displaying prompt. + /// + /// By default, we don't check. + pub fn check_cursor_position(&self) -> bool { + self.check_cursor_position + } + + /// Indentation size used by indentation commands + /// + /// By default, 2. + pub fn indent_size(&self) -> usize { + self.indent_size + } + + pub(crate) fn set_indent_size(&mut self, indent_size: usize) { + self.indent_size = indent_size; + } + + /// Bracketed paste on unix platform + /// + /// By default, it's enabled. + pub fn enable_bracketed_paste(&self) -> bool { + self.enable_bracketed_paste + } } impl Default for Config { @@ -144,6 +193,9 @@ impl Default for Config { color_mode: ColorMode::Enabled, output_stream: OutputStreamType::Stdout, tab_stop: 8, + indent_size: 2, + check_cursor_position: false, + enable_bracketed_paste: true, } } } @@ -159,7 +211,7 @@ pub enum BellStyle { Visible, } -/// `Audible` by default on unix (overriden by current Terminal settings). +/// `Audible` by default on unix (overridden by current Terminal settings). /// `None` on windows. impl Default for BellStyle { #[cfg(any(windows, target_arch = "wasm32"))] @@ -173,14 +225,18 @@ impl Default for BellStyle { } } +/// History filter #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum HistoryDuplicates { + /// No filter AlwaysAdd, /// a line will not be added to the history if it matches the previous entry IgnoreConsecutive, } +/// Tab completion style #[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] pub enum CompletionType { /// Complete the next full match (like in Vim by default) Circular, @@ -188,38 +244,56 @@ pub enum CompletionType { /// When more than one match, list all matches /// (like in Bash/Readline). List, + + /// Complete the match using fuzzy search and selection + /// (like fzf and plugins) + /// Currently only available for unix platforms as dependency on + /// skim->tuikit Compile with `--features=fuzzy` to enable + #[cfg(all(unix, feature = "with-fuzzy"))] + Fuzzy, } /// Style of editing / Standard keymaps #[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] pub enum EditMode { + /// Emacs keymap Emacs, + /// Vi keymap Vi, } /// Colorization mode #[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] pub enum ColorMode { + /// Activate highlighting if platform/terminal is supported. Enabled, + /// Activate highlighting even if platform is not supported (windows < 10). Forced, + /// Deactivate highlighting even if platform/terminal is supported. Disabled, } /// Should the editor use stdout or stderr // TODO console term::TermTarget #[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] pub enum OutputStreamType { + /// Use stderr Stderr, + /// Use stdout Stdout, } /// Configuration builder -#[derive(Debug, Default)] +#[derive(Clone, Debug, Default)] pub struct Builder { p: Config, } impl Builder { + /// Returns a `Config` builder. pub fn new() -> Self { Self { p: Config::default(), @@ -317,6 +391,31 @@ impl Builder { self } + /// Check if cursor position is at leftmost before displaying prompt. + /// + /// By default, we don't check. + pub fn check_cursor_position(mut self, yes: bool) -> Self { + self.set_check_cursor_position(yes); + self + } + + /// Indentation size + /// + /// By default, `2` + pub fn indent_size(mut self, indent_size: usize) -> Self { + self.set_indent_size(indent_size); + self + } + + /// Enable or disable bracketed paste on unix platform + /// + /// By default, it's enabled. + pub fn bracketed_paste(mut self, enabled: bool) -> Self { + self.enable_bracketed_paste(enabled); + self + } + + /// Builds a `Config` with the settings specified so far. pub fn build(self) -> Config { self.p } @@ -328,7 +427,9 @@ impl Configurer for Builder { } } +/// Trait for component that holds a `Config`. pub trait Configurer { + /// `Config` accessor. fn config_mut(&mut self) -> &mut Config; /// Set the maximum length for the history. @@ -408,4 +509,24 @@ pub trait Configurer { fn set_tab_stop(&mut self, tab_stop: usize) { self.config_mut().set_tab_stop(tab_stop); } + + /// Check if cursor position is at leftmost before displaying prompt. + /// + /// By default, we don't check. + fn set_check_cursor_position(&mut self, yes: bool) { + self.config_mut().check_cursor_position = yes; + } + /// Indentation size for indent/dedent commands + /// + /// By default, `2` + fn set_indent_size(&mut self, size: usize) { + self.config_mut().set_indent_size(size); + } + + /// Enable or disable bracketed paste on unix platform + /// + /// By default, it's enabled. + fn enable_bracketed_paste(&mut self, enabled: bool) { + self.config_mut().enable_bracketed_paste = enabled; + } } diff --git a/src/edit.rs b/src/edit.rs index 8bd1878792..3e123713f6 100644 --- a/src/edit.rs +++ b/src/edit.rs @@ -1,5 +1,6 @@ //! Command processor +use log::debug; use std::cell::RefCell; use std::fmt; use std::rc::Rc; @@ -8,29 +9,42 @@ use unicode_width::UnicodeWidthChar; use super::{Context, Helper, Result}; use crate::highlight::Highlighter; -use crate::history::Direction; +use crate::hint::Hint; +use crate::history::SearchDirection; use crate::keymap::{Anchor, At, CharSearch, Cmd, Movement, RepeatCount, Word}; -use crate::keymap::{InputState, Refresher}; +use crate::keymap::{InputState, Invoke, Refresher}; use crate::layout::{Layout, Position}; use crate::line_buffer::{LineBuffer, WordAction, MAX_LINE}; use crate::tty::{Renderer, Term, Terminal}; use crate::undo::Changeset; +use crate::validate::{ValidationContext, ValidationResult}; + +#[derive(Debug)] +pub struct Prompt<'prompt> { + /// Prompt to display (rl_prompt) + pub text: &'prompt str, + /// Prompt Unicode/visible width and height + pub size: Position, + /// Is this a default (user-defined) prompt, or temporary like `(arg: 0)`? + pub is_default: bool, + /// Is prompt rectangular or single line + pub has_continuation: bool, +} /// Represent the state during line editing. /// Implement rendering. pub struct State<'out, 'prompt, H: Helper> { pub out: &'out mut ::Writer, - prompt: &'prompt str, // Prompt to display (rl_prompt) - prompt_size: Position, // Prompt Unicode/visible width and height - pub line: LineBuffer, // Edited line buffer + prompt: Prompt<'prompt>, + pub line: LineBuffer, // Edited line buffer pub layout: Layout, saved_line_for_history: LineBuffer, // Current edited line before history browsing byte_buffer: [u8; 4], pub changes: Rc>, // changes to line, for undo/redo pub helper: Option<&'out H>, - pub ctx: Context<'out>, // Give access to history for `hinter` - pub hint: Option, // last hint displayed - highlight_char: bool, // `true` if a char has been highlighted + pub ctx: Context<'out>, // Give access to history for `hinter` + pub hint: Option>, // last hint displayed + highlight_char: bool, // `true` if a char has been highlighted } enum Info<'m> { @@ -46,11 +60,16 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { helper: Option<&'out H>, ctx: Context<'out>, ) -> State<'out, 'prompt, H> { - let prompt_size = out.calculate_position(prompt, Position::default()); + let prompt_size = out.calculate_position(prompt, Position::default(), 0); + let has_continuation = helper.map(|h| h.has_continuation_prompt()).unwrap_or(false); State { out, - prompt, - prompt_size, + prompt: Prompt { + text: prompt, + size: prompt_size, + is_default: true, + has_continuation, + }, line: LineBuffer::with_capacity(MAX_LINE).can_growth(true), layout: Layout::default(), saved_line_for_history: LineBuffer::with_capacity(MAX_LINE).can_growth(true), @@ -81,13 +100,13 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { let rc = input_state.next_cmd(rdr, self, single_esc_abort); if rc.is_err() && self.out.sigwinch() { self.out.update_size(); - self.prompt_size = self - .out - .calculate_position(self.prompt, Position::default()); + self.prompt.size = + self.out + .calculate_position(self.prompt.text, Position::default(), 0); self.refresh_line()?; continue; } - if let Ok(Cmd::Replace(_, _)) = rc { + if let Ok(Cmd::Replace(..)) = rc { self.changes.borrow_mut().begin(); } return rc; @@ -108,39 +127,50 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { 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.layout.cursor == cursor { + let new_layout = self.out.compute_layout(&self.prompt, &self.line, None); + if new_layout.cursor == self.layout.cursor { return Ok(()); } if self.highlight_char() { - let prompt_size = self.prompt_size; - self.refresh(self.prompt, prompt_size, true, Info::NoHint)?; + self.refresh_default(Info::NoHint)?; } else { - self.out.move_cursor(self.layout.cursor, cursor)?; - self.layout.prompt_size = self.prompt_size; - self.layout.cursor = cursor; - debug_assert!(self.layout.prompt_size <= self.layout.cursor); + self.out + .move_cursor(self.layout.cursor, new_layout.cursor)?; + self.layout.prompt_size = self.prompt.size; + self.layout.cursor = new_layout.cursor; debug_assert!(self.layout.cursor <= self.layout.end); } Ok(()) } + pub fn move_cursor_to_end(&mut self) -> Result<()> { + if self.layout.cursor == self.layout.end { + return Ok(()); + } + self.out.move_cursor(self.layout.cursor, self.layout.end)?; + self.layout.cursor = self.layout.end; + Ok(()) + } + pub fn move_cursor_at_leftmost(&mut self, rdr: &mut ::Reader) -> Result<()> { self.out.move_cursor_at_leftmost(rdr) } - fn refresh( - &mut self, - prompt: &str, - prompt_size: Position, - default_prompt: bool, - info: Info<'_>, - ) -> Result<()> { + fn refresh(&mut self, prompt: &Prompt<'_>, info: Info<'_>) -> Result<()> { + self._refresh(Some(prompt), info) + } + + fn refresh_default(&mut self, info: Info<'_>) -> Result<()> { + // We pass None, because we can't pass `&self.prompt` + // to the method having `&mut self` as a receiver + self._refresh(None, info) + } + + fn _refresh(&mut self, non_default_prompt: Option<&Prompt<'_>>, info: Info<'_>) -> Result<()> { + let prompt = non_default_prompt.unwrap_or(&self.prompt); let info = match info { Info::NoHint => None, - Info::Hint => self.hint.as_ref().map(String::as_str), + Info::Hint => self.hint.as_ref().map(|h| h.display()), Info::Msg(msg) => msg, }; let highlighter = if self.out.colors_enabled() { @@ -149,28 +179,9 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { None }; - // calculate the desired position of the cursor - let pos = self.line.pos(); - let cursor = self.out.calculate_position(&self.line[..pos], prompt_size); - // calculate the position of the end of the input line - let mut end = if pos == self.line.len() { - cursor - } else { - self.out.calculate_position(&self.line[pos..], cursor) - }; - if let Some(info) = info { - end = self.out.calculate_position(&info, end); - } - - let new_layout = Layout { - prompt_size, - default_prompt, - cursor, - end, - }; - debug_assert!(new_layout.prompt_size <= new_layout.cursor); - debug_assert!(new_layout.cursor <= new_layout.end); - + let new_layout = self.out.compute_layout(prompt, &self.line, info); + debug!(target: "rustyline", "old layout: {:?}", self.layout); + debug!(target: "rustyline", "new layout: {:?}", new_layout); self.out.refresh_line( prompt, &self.line, @@ -187,9 +198,12 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { pub fn hint(&mut self) { if let Some(hinter) = self.helper { let hint = hinter.hint(self.line.as_str(), self.line.pos(), &self.ctx); - self.hint = hint; + self.hint = match hint { + Some(val) if !val.display().is_empty() => Some(Box::new(val) as Box), + _ => None, + }; } else { - self.hint = None + self.hint = None; } } @@ -214,33 +228,64 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { pub fn is_default_prompt(&self) -> bool { self.layout.default_prompt } + + pub fn validate(&mut self) -> Result { + if let Some(validator) = self.helper { + self.changes.borrow_mut().begin(); + let result = validator.validate(&mut ValidationContext::new(self))?; + let corrected = self.changes.borrow_mut().end(); + match result { + ValidationResult::Incomplete => {} + ValidationResult::Valid(ref msg) => { + // Accept the line regardless of where the cursor is. + if corrected || self.has_hint() || msg.is_some() { + // Force a refresh without hints to leave the previous + // line as the user typed it after a newline. + self.refresh_line_with_msg(msg.as_deref())?; + } + } + ValidationResult::Invalid(ref msg) => { + if corrected || self.has_hint() || msg.is_some() { + self.refresh_line_with_msg(msg.as_deref())?; + } + } + } + Ok(result) + } else { + Ok(ValidationResult::Valid(None)) + } + } +} + +impl<'out, 'prompt, H: Helper> Invoke for State<'out, 'prompt, H> { + fn input(&self) -> &str { + self.line.as_str() + } } impl<'out, 'prompt, H: Helper> Refresher for State<'out, 'prompt, H> { fn refresh_line(&mut self) -> Result<()> { - let prompt_size = self.prompt_size; self.hint(); self.highlight_char(); - self.refresh(self.prompt, prompt_size, true, Info::Hint) + self.refresh_default(Info::Hint) } - fn refresh_line_with_msg(&mut self, msg: Option) -> Result<()> { - let prompt_size = self.prompt_size; + fn refresh_line_with_msg(&mut self, msg: Option<&str>) -> Result<()> { self.hint = None; self.highlight_char(); - self.refresh( - self.prompt, - prompt_size, - true, - Info::Msg(msg.as_ref().map(String::as_str)), - ) + self.refresh_default(Info::Msg(msg.as_deref())) } fn refresh_prompt_and_line(&mut self, prompt: &str) -> Result<()> { - let prompt_size = self.out.calculate_position(prompt, Position::default()); + let prompt = Prompt { + text: prompt, + size: self.out.calculate_position(prompt, Position::default(), 0), + is_default: false, + has_continuation: false, + }; self.hint(); self.highlight_char(); - self.refresh(prompt, prompt_size, false, Info::Hint) + self.refresh(&prompt, Info::Hint) } fn doing_insert(&mut self) { @@ -262,13 +307,24 @@ impl<'out, 'prompt, H: Helper> Refresher for State<'out, 'prompt, H> { fn has_hint(&self) -> bool { self.hint.is_some() } + + fn hint_text(&self) -> Option<&str> { + self.hint.as_ref().and_then(|hint| hint.completion()) + } + + fn line(&self) -> &str { + self.line.as_str() + } + + fn pos(&self) -> usize { + self.line.pos() + } } impl<'out, 'prompt, H: Helper> fmt::Debug for State<'out, 'prompt, H> { 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("cols", &self.out.get_columns()) .field("layout", &self.layout) @@ -289,7 +345,6 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { 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 no_previous_hint = self.hint.is_none(); self.hint(); let width = ch.width().unwrap_or(0); @@ -302,13 +357,12 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { // Avoid a full update of the line in the trivial case. self.layout.cursor.col += width; self.layout.end.col += width; - debug_assert!(self.layout.prompt_size <= self.layout.cursor); debug_assert!(self.layout.cursor <= self.layout.end); 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, true, Info::Hint) + self.refresh_default(Info::Hint) } } else { self.refresh_line() @@ -420,6 +474,24 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { } } + /// Move cursor to the start of the buffer. + pub fn edit_move_buffer_start(&mut self) -> Result<()> { + if self.line.move_buffer_start() { + self.move_cursor() + } else { + Ok(()) + } + } + + /// Move cursor to the end of the buffer. + pub fn edit_move_buffer_end(&mut self) -> Result<()> { + if self.line.move_buffer_end() { + self.move_cursor() + } else { + Ok(()) + } + } + pub fn edit_kill(&mut self, mvt: &Movement) -> Result<()> { if self.line.kill(mvt) { self.refresh_line() @@ -437,14 +509,6 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { self.refresh_line() } - 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(); @@ -473,6 +537,26 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { } } + /// Moves the cursor to the same column in the line above + pub fn edit_move_line_up(&mut self, n: RepeatCount) -> Result { + if self.line.move_to_line_up(n) { + self.move_cursor()?; + Ok(true) + } else { + Ok(false) + } + } + + /// Moves the cursor to the same column in the line above + pub fn edit_move_line_down(&mut self, n: RepeatCount) -> Result { + if self.line.move_to_line_down(n) { + self.move_cursor()?; + Ok(true) + } else { + Ok(false) + } + } + pub fn edit_move_to(&mut self, cs: CharSearch, n: RepeatCount) -> Result<()> { if self.line.move_to(cs, n) { self.move_cursor() @@ -538,30 +622,29 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { } // Non-incremental, anchored search - pub fn edit_history_search(&mut self, dir: Direction) -> Result<()> { + pub fn edit_history_search(&mut self, dir: SearchDirection) -> Result<()> { let history = self.ctx.history; if history.is_empty() { return self.out.beep(); } - if self.ctx.history_index == history.len() && dir == Direction::Forward - || self.ctx.history_index == 0 && dir == Direction::Reverse + if self.ctx.history_index == history.len() && dir == SearchDirection::Forward + || self.ctx.history_index == 0 && dir == SearchDirection::Reverse { return self.out.beep(); } - if dir == Direction::Reverse { + if dir == SearchDirection::Reverse { self.ctx.history_index -= 1; } else { self.ctx.history_index += 1; } - if let Some(history_index) = history.starts_with( + if let Some(sr) = history.starts_with( &self.line.as_str()[..self.line.pos()], self.ctx.history_index, dir, ) { - self.ctx.history_index = history_index; - let buf = history.get(history_index).unwrap(); + self.ctx.history_index = sr.idx; self.changes.borrow_mut().begin(); - self.line.update(buf, buf.len()); + self.line.update(sr.entry, sr.pos); self.changes.borrow_mut().end(); self.refresh_line() } else { @@ -598,6 +681,15 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { } self.refresh_line() } + + /// Change the indentation of the lines covered by movement + pub fn edit_indent(&mut self, mvt: &Movement, amount: usize, dedent: bool) -> Result<()> { + if self.line.indent(mvt, amount, dedent) { + self.refresh_line() + } else { + Ok(()) + } + } } #[cfg(test)] @@ -610,8 +702,12 @@ pub fn init_state<'out, H: Helper>( ) -> State<'out, 'static, H> { State { out, - prompt: "", - prompt_size: Position::default(), + prompt: Prompt { + text: "", + size: Position::default(), + is_default: true, + has_continuation: false, + }, line: LineBuffer::init(line, pos, None), layout: Layout::default(), saved_line_for_history: LineBuffer::with_capacity(100), @@ -619,7 +715,7 @@ pub fn init_state<'out, H: Helper>( changes: Rc::new(RefCell::new(Changeset::new())), helper, ctx: Context::new(history), - hint: Some("hint".to_owned()), + hint: Some(Box::new("hint".to_owned())), highlight_char: false, } } diff --git a/src/error.rs b/src/error.rs index fb2d4d6432..3ef771a438 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,24 +1,22 @@ //! Contains error type for handling I/O and Errno errors -#[cfg(unix)] -use nix; #[cfg(windows)] use std::char; use std::error; use std::fmt; 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 // #[non_exhaustive] #[allow(clippy::module_name_repetitions)] #[derive(Debug)] +#[non_exhaustive] pub enum ReadlineError { /// I/O Error Io(io::Error), - /// EOF (Ctrl-D) + /// EOF (VEOF / Ctrl-D) Eof, - /// Ctrl-C + /// Interrupt signal (VINTR / VQUIT / Ctrl-C) Interrupted, /// Chars Error #[cfg(unix)] @@ -26,10 +24,16 @@ pub enum ReadlineError { /// Unix Error from syscall #[cfg(unix)] Errno(nix::Error), + /// Error generated on WINDOW_BUFFER_SIZE_EVENT to mimic unix SIGWINCH + /// signal #[cfg(windows)] WindowResize, + /// Like Utf8Error on unix #[cfg(windows)] Decode(char::DecodeUtf16Error), + /// Something went wrong calling a Windows API + #[cfg(windows)] + SystemError(clipboard_win::SystemError), } impl fmt::Display for ReadlineError { @@ -46,28 +50,14 @@ impl fmt::Display for ReadlineError { ReadlineError::WindowResize => write!(f, "WindowResize"), #[cfg(windows)] ReadlineError::Decode(ref err) => err.fmt(f), - } - } -} - -impl error::Error for ReadlineError { - fn description(&self) -> &str { - match *self { - ReadlineError::Io(ref err) => err.description(), - ReadlineError::Eof => "EOF", - ReadlineError::Interrupted => "Interrupted", - #[cfg(unix)] - ReadlineError::Utf8Error => "invalid utf-8: corrupt contents", - #[cfg(unix)] - ReadlineError::Errno(ref err) => err.description(), - #[cfg(windows)] - ReadlineError::WindowResize => "WindowResize", #[cfg(windows)] - ReadlineError::Decode(ref err) => err.description(), + ReadlineError::SystemError(ref err) => err.fmt(f), } } } +impl error::Error for ReadlineError {} + impl From for ReadlineError { fn from(err: io::Error) -> Self { ReadlineError::Io(err) @@ -93,3 +83,10 @@ impl From for ReadlineError { ReadlineError::Decode(err) } } + +#[cfg(windows)] +impl From for ReadlineError { + fn from(err: clipboard_win::SystemError) -> Self { + ReadlineError::SystemError(err) + } +} diff --git a/src/highlight.rs b/src/highlight.rs index 5349a07e43..f181229843 100644 --- a/src/highlight.rs +++ b/src/highlight.rs @@ -5,6 +5,15 @@ use memchr::memchr; use std::borrow::Cow::{self, Borrowed, Owned}; use std::cell::Cell; +pub struct PromptInfo<'a> { + pub(crate) is_default: bool, + pub(crate) offset: usize, + pub(crate) cursor: Option, + pub(crate) input: &'a str, + pub(crate) line: &'a str, + pub(crate) line_no: usize, +} + /// 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). @@ -26,11 +35,18 @@ pub trait Highlighter { fn highlight_prompt<'b, 's: 'b, 'p: 'b>( &'s self, prompt: &'p str, - default: bool, + info: PromptInfo<'_>, ) -> Cow<'b, str> { - let _ = default; + let _ = info; Borrowed(prompt) } + + /// Returns `true` if prompt is rectangular rather than being present only + /// on the first line of input + fn has_continuation_prompt(&self) -> bool { + false + } + /// Takes the `hint` and /// returns the highlighted version (with ANSI color). fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { @@ -42,7 +58,7 @@ pub trait Highlighter { /// Currently, used only with `CompletionType::List`. fn highlight_candidate<'c>( &self, - candidate: &'c str, + candidate: &'c str, // FIXME should be Completer::Candidate completion: CompletionType, ) -> Cow<'c, str> { let _ = completion; @@ -69,9 +85,9 @@ impl<'r, H: ?Sized + Highlighter> Highlighter for &'r H { fn highlight_prompt<'b, 's: 'b, 'p: 'b>( &'s self, prompt: &'p str, - default: bool, + info: PromptInfo<'_>, ) -> Cow<'b, str> { - (**self).highlight_prompt(prompt, default) + (**self).highlight_prompt(prompt, info) } fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { @@ -94,12 +110,16 @@ impl<'r, H: ?Sized + Highlighter> Highlighter for &'r H { const OPENS: &[u8; 3] = b"{[("; const CLOSES: &[u8; 3] = b"}])"; +/// TODO versus https://python-prompt-toolkit.readthedocs.io/en/master/pages/reference.html?highlight=HighlightMatchingBracketProcessor#prompt_toolkit.layout.processors.HighlightMatchingBracketProcessor + +/// Highlight matching bracket when typed or cursor moved on. #[derive(Default)] pub struct MatchingBracketHighlighter { bracket: Cell>, // memorize the character to search... } impl MatchingBracketHighlighter { + /// Constructor pub fn new() -> Self { Self { bracket: Cell::new(None), @@ -191,17 +211,13 @@ fn check_bracket(line: &str, pos: usize) -> Option<(u8, usize)> { loop { let b = line.as_bytes()[pos]; if is_close_bracket(b) { - if pos == 0 { - return None; - } else { - return Some((b, pos)); - } + return if pos == 0 { None } else { Some((b, pos)) }; } else if is_open_bracket(b) { - if pos + 1 == line.len() { - return None; + return if pos + 1 == line.len() { + None } else { - return Some((b, pos)); - } + Some((b, pos)) + }; } else if under_cursor && pos > 0 { under_cursor = false; pos -= 1; // or before cursor @@ -230,6 +246,88 @@ fn is_close_bracket(bracket: u8) -> bool { memchr(bracket, CLOSES).is_some() } +pub(crate) fn split_highlight<'a>(src: &'a str, offset: usize) + -> (Cow<'a, str>, Cow<'a, str>) +{ + let mut style_buffer = String::with_capacity(32); + let mut iter = src.char_indices(); + let mut non_escape_idx = 0; + while let Some((idx, c)) = iter.next() { + if c == '\x1b' { + match iter.next() { + Some((_, '[')) => {} + _ => continue, // unknown escape, skip + } + while let Some((end_idx, c)) = iter.next() { + match c { + 'm' => { + let slice = &src[idx..end_idx+1]; + if slice == "\x1b[0m" { + style_buffer.clear(); + } else { + style_buffer.push_str(slice); + } + break; + } + ';' | '0'..='9' => continue, + _ => break, // unknown escape, skip + } + } + continue; + } + if non_escape_idx >= offset { + if style_buffer.is_empty() { + return (src[..idx].into(), src[idx..].into()); + } else { + let mut left = String::with_capacity(idx + 4); + left.push_str(&src[..idx]); + left.push_str("\x1b[0m"); + let mut right = String::with_capacity( + src.len() - idx + style_buffer.len()); + right.push_str(&style_buffer); + right.push_str(&src[idx..]); + return (left.into(), right.into()); + } + } + non_escape_idx += c.len_utf8(); + } + return (src.into(), "".into()); +} + +impl PromptInfo<'_> { + /// Returns true if this is the default prompt + pub fn is_default(&self) -> bool { + self.is_default + } + /// Returns the byte offset where prompt is shown in the initial text + /// + /// This is a position right after the newline of the previous line + pub fn line_offset(&self) -> usize { + self.offset + } + + /// Returns the byte position of the cursor relative to `line_offset` if + /// the cursor is in the current line + pub fn cursor(&self) -> Option { + self.cursor + } + + /// Returns the zero-based line number of the current prompt line + pub fn line_no(&self) -> usize { + self.line_no + } + + /// Returns the line contents shown after the prompt + pub fn line(&self) -> &str { + self.line + } + + /// Returns the whole input (equal to `line` if input is the single line) + pub fn input(&self) -> &str { + self.input + } +} + #[cfg(test)] mod tests { #[test] diff --git a/src/hint.rs b/src/hint.rs index 8a89f9bf2f..71b162a23b 100644 --- a/src/hint.rs +++ b/src/hint.rs @@ -1,33 +1,62 @@ //! Hints (suggestions at the right of the prompt as you type). -use crate::history::Direction; +use crate::history::SearchDirection; use crate::Context; +/// A hint returned by Hinter +pub trait Hint { + /// Text to display when hint is active + fn display(&self) -> &str; + /// Text to insert in line when right arrow is pressed + fn completion(&self) -> Option<&str>; +} + +impl Hint for String { + fn display(&self) -> &str { + self.as_str() + } + + fn completion(&self) -> Option<&str> { + Some(self.as_str()) + } +} + /// Hints provider pub trait Hinter { + /// Specific hint type + type Hint: Hint + 'static; + /// 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. // TODO Validate: called while editing line but not while moving cursor. - fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option { + fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option { let _ = (line, pos, ctx); None } } -impl Hinter for () {} +impl Hinter for () { + type Hint = String; +} impl<'r, H: ?Sized + Hinter> Hinter for &'r H { - fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option { + type Hint = H::Hint; + + fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option { (**self).hint(line, pos, ctx) } } +/// Add suggestion based on previous history entries matching current user +/// input. pub struct HistoryHinter {} impl Hinter for HistoryHinter { + type Hint = String; + fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option { - if pos < line.len() { + if line.is_empty() || pos < line.len() { return None; } let start = if ctx.history_index() == ctx.history().len() { @@ -35,17 +64,14 @@ impl Hinter for HistoryHinter { } else { ctx.history_index() }; - if let Some(history_index) = - ctx.history - .starts_with(&line[..pos], start, Direction::Reverse) + if let Some(sr) = ctx + .history + .starts_with(line, start, SearchDirection::Reverse) { - let entry = ctx.history.get(history_index); - if let Some(entry) = entry { - if entry == line || entry == &line[..pos] { - return None; - } + if sr.entry == line { + return None; } - return entry.map(|s| s[pos..].to_owned()); + return Some(sr.entry[pos..].to_owned()); } None } diff --git a/src/history.rs b/src/history.rs index b12a0bd9ae..1bde437516 100644 --- a/src/history.rs +++ b/src/history.rs @@ -1,24 +1,45 @@ //! History API -#[cfg(unix)] -use libc; +use fd_lock::RwLock; +use log::{debug, warn}; use std::collections::vec_deque; use std::collections::VecDeque; -use std::fs::File; +use std::fs::{File, OpenOptions}; +use std::io::SeekFrom; use std::iter::DoubleEndedIterator; use std::ops::Index; -use std::path::Path; +use std::path::{Path, PathBuf}; +use std::time::SystemTime; use super::Result; use crate::config::{Config, HistoryDuplicates}; /// Search direction #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Direction { +pub enum SearchDirection { + /// Search history forward Forward, + /// Search history backward Reverse, } +/// History search result +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct SearchResult<'a> { + /// history entry + pub entry: &'a str, + /// history index + pub idx: usize, + /// match position in `entry` + pub pos: usize, +} + +/// HistoryEntry: text + timestamp +/// TODO Make possible to customize how history is stored / loaded. +/// https://github.com/kkawakam/rustyline/issues/442 +/// https://github.com/kkawakam/rustyline/issues/127 +/// See https://python-prompt-toolkit.readthedocs.io/en/master/pages/reference.html#prompt_toolkit.history.History abstract methods + /// Current state of the history. #[derive(Default)] pub struct History { @@ -26,19 +47,37 @@ pub struct History { max_len: usize, pub(crate) ignore_space: bool, pub(crate) ignore_dups: bool, + /// Number of entries inputed by user and not saved yet + new_entries: usize, + /// last path used by either `load` or `save` + path_info: Option, } +/// Last histo path, modified timestamp and size +struct PathInfo(PathBuf, SystemTime, usize); + impl History { + // New multiline-aware history files start with `#V2\n` and have newlines + // and backslashes escaped in them. + const FILE_VERSION_V2: &'static str = "#V2"; + + /// Default constructor pub fn new() -> Self { Self::with_config(Config::default()) } + /// Customized constructor with: + /// - `Config::max_history_size()`, + /// - `Config::history_ignore_space()`, + /// - `Config::history_duplicates()`. pub fn with_config(config: Config) -> Self { Self { entries: VecDeque::new(), max_len: config.max_history_size(), ignore_space: config.history_ignore_space(), ignore_dups: config.history_duplicates() == HistoryDuplicates::IgnoreConsecutive, + new_entries: 0, + path_info: None, } } @@ -78,6 +117,7 @@ impl History { self.entries.pop_front(); } self.entries.push_back(line.into()); + self.new_entries = self.new_entries.saturating_add(1).min(self.len()); true } @@ -96,41 +136,59 @@ impl 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). + /// Like [stifle_history](http://tiswww.case.edu/php/chet/readline/history.html#IDX11). pub fn set_max_len(&mut self, len: usize) { self.max_len = len; - if len == 0 { - self.entries.clear(); - return; - } - loop { - if self.entries.len() <= len { - break; - } - self.entries.pop_front(); + if self.len() > len { + self.entries.drain(..self.len() - len); + self.new_entries = self.new_entries.min(len); } } /// 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 + ?Sized>(&self, path: &P) -> Result<()> { - use std::io::{BufWriter, Write}; - - if self.is_empty() { + // https://tiswww.case.edu/php/chet/readline/history.html#IDX31 + pub fn save + ?Sized>(&mut self, path: &P) -> Result<()> { + if self.is_empty() || self.new_entries == 0 { return Ok(()); } + let path = path.as_ref(); let old_umask = umask(); let f = File::create(path); restore_umask(old_umask); let file = f?; - fix_perm(&file); + let mut lock = RwLock::new(file); + let lock_guard = lock.write()?; + self.save_to(&lock_guard, false)?; + self.new_entries = 0; + self.update_path(path, &lock_guard, self.len()) + } + + fn save_to(&mut self, file: &File, append: bool) -> Result<()> { + use std::io::{BufWriter, Write}; + + fix_perm(file); let mut wtr = BufWriter::new(file); - for entry in &self.entries { - wtr.write_all(entry.as_bytes())?; + let first_new_entry = if append { + self.entries.len().saturating_sub(self.new_entries) + } else { + wtr.write_all(Self::FILE_VERSION_V2.as_bytes())?; + wtr.write_all(b"\n")?; + 0 + }; + for entry in self.entries.iter().skip(first_new_entry) { + let mut bytes = entry.as_bytes(); + while let Some(i) = memchr::memchr2(b'\\', b'\n', bytes) { + wtr.write_all(&bytes[..i])?; + if bytes[i] == b'\n' { + wtr.write_all(b"\\n")?; // escaped line feed + } else { + debug_assert_eq!(bytes[i], b'\\'); + wtr.write_all(b"\\\\")?; // escaped backslash + } + bytes = &bytes[i + 1..]; + } + wtr.write_all(bytes)?; // remaining bytes with no \n or \ wtr.write_all(b"\n")?; } // https://github.com/rust-lang/rust/issues/32677#issuecomment-204833485 @@ -138,24 +196,184 @@ impl History { Ok(()) } + /// Append new entries in the specified file. + // Like [append_history](http://tiswww.case.edu/php/chet/readline/history.html#IDX30). + pub fn append + ?Sized>(&mut self, path: &P) -> Result<()> { + use std::io::Seek; + + if self.is_empty() || self.new_entries == 0 { + return Ok(()); + } + let path = path.as_ref(); + if !path.exists() || self.new_entries == self.max_len { + return self.save(path); + } + let file = OpenOptions::new().write(true).read(true).open(path)?; + let mut lock = RwLock::new(file); + let mut lock_guard = lock.write()?; + if self.can_just_append(path, &lock_guard)? { + lock_guard.seek(SeekFrom::End(0))?; + self.save_to(&lock_guard, true)?; + let size = self + .path_info + .as_ref() + .unwrap() + .2 + .saturating_add(self.new_entries); + self.new_entries = 0; + return self.update_path(path, &lock_guard, size); + } + // we may need to truncate file before appending new entries + let mut other = Self { + entries: VecDeque::new(), + max_len: self.max_len, + ignore_space: self.ignore_space, + ignore_dups: self.ignore_dups, + new_entries: 0, + path_info: None, + }; + other.load_from(&lock_guard)?; + let first_new_entry = self.entries.len().saturating_sub(self.new_entries); + for entry in self.entries.iter().skip(first_new_entry) { + other.add(entry); + } + lock_guard.seek(SeekFrom::Start(0))?; + lock_guard.set_len(0)?; // if new size < old size + other.save_to(&lock_guard, false)?; + self.update_path(path, &lock_guard, other.len())?; + self.new_entries = 0; + Ok(()) + } + /// Load the history from the specified file. /// /// # Errors /// Will return `Err` if path does not already exist or could not be read. pub fn load + ?Sized>(&mut self, path: &P) -> Result<()> { + let path = path.as_ref(); + let file = File::open(path)?; + let lock = RwLock::new(file); + let lock_guard = lock.read()?; + let len = self.len(); + if self.load_from(&lock_guard)? { + self.update_path(path, &lock_guard, self.len() - len) + } else { + // discard old version on next save + self.path_info = None; + Ok(()) + } + } + + fn load_from(&mut self, file: &File) -> Result { use std::io::{BufRead, BufReader}; - let file = File::open(&path)?; let rdr = BufReader::new(file); - for line in rdr.lines() { - self.add(line?); // TODO truncate to MAX_LINE + let mut lines = rdr.lines(); + let mut v2 = false; + if let Some(first) = lines.next() { + let line = first?; + if line == Self::FILE_VERSION_V2 { + v2 = true; + } else { + self.add(line); + } } + let mut appendable = v2; + for line in lines { + let mut line = line?; + if line.is_empty() { + continue; + } + if v2 { + let mut copy = None; // lazily copy line if unescaping is needed + let mut str = line.as_str(); + while let Some(i) = str.find('\\') { + if copy.is_none() { + copy = Some(String::with_capacity(line.len())); + } + let s = copy.as_mut().unwrap(); + s.push_str(&str[..i]); + let j = i + 1; // escaped char idx + let b = if j < str.len() { + str.as_bytes()[j] + } else { + 0 // unexpected if History::save works properly + }; + match b { + b'n' => { + s.push('\n'); // unescaped line feed + } + b'\\' => { + s.push('\\'); // unescaped back slash + } + _ => { + // only line feed and back slash should have been escaped + warn!(target: "rustyline", "bad escaped line: {}", line); + copy = None; + break; + } + } + str = &str[j + 1..]; + } + if let Some(mut s) = copy { + s.push_str(str); // remaining bytes with no escaped char + line = s; + } + } + appendable &= self.add(line); // TODO truncate to MAX_LINE + } + self.new_entries = 0; // TODO we may lost new entries if loaded lines < max_len + Ok(appendable) + } + + fn update_path(&mut self, path: &Path, file: &File, size: usize) -> Result<()> { + let modified = file.metadata()?.modified()?; + if let Some(PathInfo( + ref mut previous_path, + ref mut previous_modified, + ref mut previous_size, + )) = self.path_info + { + if previous_path.as_path() != path { + *previous_path = path.to_owned(); + } + *previous_modified = modified; + *previous_size = size; + } else { + self.path_info = Some(PathInfo(path.to_owned(), modified, size)); + } + debug!(target: "rustyline", "PathInfo({:?}, {:?}, {})", path, modified, size); Ok(()) } + fn can_just_append(&self, path: &Path, file: &File) -> Result { + if let Some(PathInfo(ref previous_path, ref previous_modified, ref previous_size)) = + self.path_info + { + if previous_path.as_path() != path { + debug!(target: "rustyline", "cannot append: {:?} <> {:?}", previous_path, path); + return Ok(false); + } + let modified = file.metadata()?.modified()?; + if *previous_modified != modified + || self.max_len <= *previous_size + || self.max_len < (*previous_size).saturating_add(self.new_entries) + { + debug!(target: "rustyline", "cannot append: {:?} < {:?} or {} < {} + {}", + previous_modified, modified, self.max_len, previous_size, self.new_entries); + Ok(false) + } else { + Ok(true) + } + } else { + Ok(false) + } + } + /// Clear history pub fn clear(&mut self) { - self.entries.clear() + self.entries.clear(); + self.new_entries = 0; } /// Search history (start position inclusive [0, len-1]). @@ -166,37 +384,107 @@ impl History { /// 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 { - let test = |entry: &String| entry.contains(term); - self.search_match(term, start, dir, test) + pub fn search(&self, term: &str, start: usize, dir: SearchDirection) -> Option { + #[cfg(not(feature = "case_insensitive_history_search"))] + { + let test = |entry: &str| entry.find(term); + self.search_match(term, start, dir, test) + } + #[cfg(feature = "case_insensitive_history_search")] + { + use regex::{escape, RegexBuilder}; + if let Ok(re) = RegexBuilder::new(&escape(term)) + .case_insensitive(true) + .build() + { + let test = |entry: &str| re.find(entry).map(|m| m.start()); + self.search_match(term, start, dir, test) + } else { + None + } + } } /// Anchored search - pub fn starts_with(&self, term: &str, start: usize, dir: Direction) -> Option { - let test = |entry: &String| entry.starts_with(term); - self.search_match(term, start, dir, test) + pub fn starts_with( + &self, + term: &str, + start: usize, + dir: SearchDirection, + ) -> Option { + #[cfg(not(feature = "case_insensitive_history_search"))] + { + let test = |entry: &str| { + if entry.starts_with(term) { + Some(term.len()) + } else { + None + } + }; + self.search_match(term, start, dir, test) + } + #[cfg(feature = "case_insensitive_history_search")] + { + use regex::{escape, RegexBuilder}; + if let Ok(re) = RegexBuilder::new(&escape(term)) + .case_insensitive(true) + .build() + { + let test = |entry: &str| { + re.find(entry) + .and_then(|m| if m.start() == 0 { Some(m) } else { None }) + .map(|m| m.end()) + }; + self.search_match(term, start, dir, test) + } else { + None + } + } } - fn search_match(&self, term: &str, start: usize, dir: Direction, test: F) -> Option + fn search_match( + &self, + term: &str, + start: usize, + dir: SearchDirection, + test: F, + ) -> Option where - F: Fn(&String) -> bool, + F: Fn(&str) -> Option, { if term.is_empty() || start >= self.len() { return None; } match dir { - Direction::Reverse => { - let index = self + SearchDirection::Reverse => { + for (idx, entry) in self .entries .iter() .rev() .skip(self.entries.len() - 1 - start) - .position(test); - index.map(|index| start - index) + .enumerate() + { + if let Some(cursor) = test(entry) { + return Some(SearchResult { + idx: start - idx, + entry, + pos: cursor, + }); + } + } + None } - Direction::Forward => { - let index = self.entries.iter().skip(start).position(test); - index.map(|index| index + start) + SearchDirection::Forward => { + for (idx, entry) in self.entries.iter().skip(start).enumerate() { + if let Some(cursor) = test(entry) { + return Some(SearchResult { + idx: idx + start, + entry, + pos: cursor, + }); + } + } + None } } } @@ -276,10 +564,9 @@ cfg_if::cfg_if! { #[cfg(test)] mod tests { - use super::{Direction, History}; + use super::{History, SearchDirection, SearchResult}; use crate::config::Config; - use std::path::Path; - use tempdir; + use crate::Result; fn init() -> History { let mut history = History::new(); @@ -316,37 +603,189 @@ mod tests { } #[test] - fn save() { + #[cfg_attr(miri, ignore)] // unsupported operation: `getcwd` not available when isolation is enabled + fn save() -> Result<()> { + check_save("line\nfour \\ abc") + } + + #[test] + #[cfg_attr(miri, ignore)] // unsupported operation: `open` not available when isolation is enabled + fn save_windows_path() -> Result<()> { + let path = "cd source\\repos\\forks\\nushell\\"; + check_save(path) + } + + fn check_save(line: &str) -> Result<()> { + let mut history = init(); + assert!(history.add(line)); + let tf = tempfile::NamedTempFile::new()?; + + history.save(tf.path())?; + let mut history2 = History::new(); + history2.load(tf.path())?; + for (a, b) in history.entries.iter().zip(history2.entries.iter()) { + assert_eq!(a, b); + } + tf.close()?; + Ok(()) + } + + #[test] + #[cfg_attr(miri, ignore)] // unsupported operation: `getcwd` not available when isolation is enabled + fn load_legacy() -> Result<()> { + use std::io::Write; + let tf = tempfile::NamedTempFile::new()?; + { + let mut legacy = std::fs::File::create(tf.path())?; + // Some data we'd accidentally corrupt if we got the version wrong + let data = b"\ + test\\n \\abc \\123\n\ + 123\\n\\\\n\n\ + abcde + "; + legacy.write_all(data)?; + legacy.flush()?; + } + let mut history = History::new(); + history.load(tf.path())?; + assert_eq!(history.entries[0], "test\\n \\abc \\123"); + assert_eq!(history.entries[1], "123\\n\\\\n"); + assert_eq!(history.entries[2], "abcde"); + + tf.close()?; + Ok(()) + } + + #[test] + #[cfg_attr(miri, ignore)] // unsupported operation: `getcwd` not available when isolation is enabled + fn append() -> Result<()> { let mut history = init(); - let td = tempdir::TempDir::new_in(&Path::new("."), "histo").unwrap(); - let history_path = td.path().join(".history"); + let tf = tempfile::NamedTempFile::new()?; + + history.append(tf.path())?; + + let mut history2 = History::new(); + history2.load(tf.path())?; + history2.add("line4"); + history2.append(tf.path())?; - history.save(&history_path).unwrap(); - history.load(&history_path).unwrap(); - td.close().unwrap(); + history.add("line5"); + history.append(tf.path())?; + + let mut history3 = History::new(); + history3.load(tf.path())?; + assert_eq!(history3.len(), 5); + + tf.close()?; + Ok(()) + } + + #[test] + #[cfg_attr(miri, ignore)] // unsupported operation: `getcwd` not available when isolation is enabled + fn truncate() -> Result<()> { + let tf = tempfile::NamedTempFile::new()?; + + let config = Config::builder().history_ignore_dups(false).build(); + let mut history = History::with_config(config); + history.add("line1"); + history.add("line1"); + history.append(tf.path())?; + + let mut history = History::new(); + history.load(tf.path())?; + history.add("l"); + history.append(tf.path())?; + + let mut history = History::new(); + history.load(tf.path())?; + assert_eq!(history.len(), 2); + assert_eq!(history.entries[1], "l"); + + tf.close()?; + Ok(()) } #[test] fn search() { let history = init(); - assert_eq!(None, history.search("", 0, Direction::Forward)); - assert_eq!(None, history.search("none", 0, Direction::Forward)); - assert_eq!(None, history.search("line", 3, Direction::Forward)); - - assert_eq!(Some(0), history.search("line", 0, Direction::Forward)); - assert_eq!(Some(1), history.search("line", 1, Direction::Forward)); - assert_eq!(Some(2), history.search("line3", 1, Direction::Forward)); + assert_eq!(None, history.search("", 0, SearchDirection::Forward)); + assert_eq!(None, history.search("none", 0, SearchDirection::Forward)); + assert_eq!(None, history.search("line", 3, SearchDirection::Forward)); + + assert_eq!( + Some(SearchResult { + idx: 0, + entry: history.get(0).unwrap(), + pos: 0 + }), + history.search("line", 0, SearchDirection::Forward) + ); + assert_eq!( + Some(SearchResult { + idx: 1, + entry: history.get(1).unwrap(), + pos: 0 + }), + history.search("line", 1, SearchDirection::Forward) + ); + assert_eq!( + Some(SearchResult { + idx: 2, + entry: history.get(2).unwrap(), + pos: 0 + }), + history.search("line3", 1, SearchDirection::Forward) + ); } #[test] fn reverse_search() { let history = init(); - assert_eq!(None, history.search("", 2, Direction::Reverse)); - assert_eq!(None, history.search("none", 2, Direction::Reverse)); - assert_eq!(None, history.search("line", 3, Direction::Reverse)); + assert_eq!(None, history.search("", 2, SearchDirection::Reverse)); + assert_eq!(None, history.search("none", 2, SearchDirection::Reverse)); + assert_eq!(None, history.search("line", 3, SearchDirection::Reverse)); + + assert_eq!( + Some(SearchResult { + idx: 2, + entry: history.get(2).unwrap(), + pos: 0 + }), + history.search("line", 2, SearchDirection::Reverse) + ); + assert_eq!( + Some(SearchResult { + idx: 1, + entry: history.get(1).unwrap(), + pos: 0 + }), + history.search("line", 1, SearchDirection::Reverse) + ); + assert_eq!( + Some(SearchResult { + idx: 0, + entry: history.get(0).unwrap(), + pos: 0 + }), + history.search("line1", 1, SearchDirection::Reverse) + ); + } - assert_eq!(Some(2), history.search("line", 2, Direction::Reverse)); - assert_eq!(Some(1), history.search("line", 1, Direction::Reverse)); - assert_eq!(Some(0), history.search("line1", 1, Direction::Reverse)); + #[test] + #[cfg(feature = "case_insensitive_history_search")] + fn anchored_search() { + let history = init(); + assert_eq!( + Some(SearchResult { + idx: 2, + entry: history.get(2).unwrap(), + pos: 4 + }), + history.starts_with("LiNe", 2, SearchDirection::Reverse) + ); + assert_eq!( + None, + history.starts_with("iNe", 2, SearchDirection::Reverse) + ); } } diff --git a/src/keymap.rs b/src/keymap.rs index 2b34765755..8fafb8b39c 100644 --- a/src/keymap.rs +++ b/src/keymap.rs @@ -1,25 +1,26 @@ //! Bindings from keys to command for Emacs and Vi modes -use std::collections::HashMap; use std::sync::{Arc, RwLock}; use log::debug; +use radix_trie::Trie; use super::Result; -use crate::config::Config; -use crate::config::EditMode; -use crate::keys::KeyPress; +use crate::keys::{KeyCode as K, KeyEvent, KeyEvent as E, Modifiers as M}; use crate::tty::{RawReader, Term, Terminal}; +use crate::{Config, EditMode, Event, EventContext, EventHandler}; /// The number of times one command should be repeated. pub type RepeatCount = usize; /// Commands -// #[non_exhaustive] #[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] pub enum Cmd { /// abort Abort, // Miscellaneous Command /// accept-line + /// + /// See also AcceptOrInsertLine AcceptLine, /// beginning-of-history BeginningOfHistory, @@ -27,25 +28,34 @@ pub enum Cmd { CapitalizeWord, /// clear-screen ClearScreen, + /// Paste from the clipboard + #[cfg(windows)] + PasteFromClipboard, /// complete Complete, /// complete-backward CompleteBackward, /// complete-hint CompleteHint, + /// Dedent current line + Dedent(Movement), /// downcase-word DowncaseWord, /// vi-eof-maybe EndOfFile, /// end-of-history EndOfHistory, - /// forward-search-history + /// forward-search-history (incremental search) ForwardSearchHistory, - /// history-search-backward + /// history-search-backward (common prefix search) HistorySearchBackward, - /// history-search-forward + /// history-search-forward (common prefix search) HistorySearchForward, + /// Indent current line + Indent(Movement), + /// Insert text Insert(RepeatCount, String), + /// Interrupt signal (Ctrl-C) Interrupt, /// backward-delete-char, backward-kill-line, backward-kill-word /// delete-char, kill-line, kill-word, unix-line-discard, unix-word-rubout, @@ -57,6 +67,7 @@ pub enum Cmd { Move(Movement), /// next-history NextHistory, + /// No action Noop, /// vi-replace Overwrite(char), @@ -68,10 +79,11 @@ pub enum Cmd { ReplaceChar(RepeatCount, char), /// vi-change-to, vi-substitute Replace(Movement, Option), - /// reverse-search-history + /// reverse-search-history (incremental search) ReverseSearchHistory, /// self-insert SelfInsert(RepeatCount, char), + /// Suspend signal (Ctrl-Z on unix platform) Suspend, /// transpose-chars TransposeChars, @@ -79,6 +91,7 @@ pub enum Cmd { TransposeWords(RepeatCount), /// undo Undo(RepeatCount), + /// Unsupported / unexpected Unknown, /// upcase-word UpcaseWord, @@ -88,36 +101,61 @@ pub enum Cmd { Yank(RepeatCount, Anchor), /// yank-pop YankPop, + /// moves cursor to the line above or switches to prev history entry if + /// the cursor is already on the first line + LineUpOrPreviousHistory(RepeatCount), + /// moves cursor to the line below or switches to next history entry if + /// the cursor is already on the last line + LineDownOrNextHistory(RepeatCount), + /// Inserts a newline + Newline, + /// Either accepts or inserts a newline + /// + /// Always inserts newline if input is non-valid. Can also insert newline + /// if cursor is in the middle of the text + /// + /// If you support multi-line input: + /// * Use `accept_in_the_middle: true` for mostly single-line cases, for + /// example command-line. + /// * Use `accept_in_the_middle: false` for mostly multi-line cases, for + /// example SQL or JSON input. + AcceptOrInsertLine { + /// Whether this commands accepts input if the cursor not at the end + /// of the current input + accept_in_the_middle: bool, + }, } impl Cmd { + /// Tells if current command should reset kill ring. pub fn should_reset_kill_ring(&self) -> bool { #[allow(clippy::match_same_arms)] match *self { Cmd::Kill(Movement::BackwardChar(_)) | Cmd::Kill(Movement::ForwardChar(_)) => true, Cmd::ClearScreen | Cmd::Kill(_) - | Cmd::Replace(_, _) + | Cmd::Replace(..) | Cmd::Noop | Cmd::Suspend - | Cmd::Yank(_, _) + | Cmd::Yank(..) | Cmd::YankPop => false, _ => true, } } fn is_repeatable_change(&self) -> bool { - match *self { - Cmd::Insert(_, _) - | Cmd::Kill(_) - | Cmd::ReplaceChar(_, _) - | Cmd::Replace(_, _) - | Cmd::SelfInsert(_, _) - | Cmd::ViYankTo(_) - | Cmd::Yank(_, _) => true, - Cmd::TransposeChars // TODO Validate - | _ => false, - } + matches!( + *self, + Cmd::Dedent(..) + | Cmd::Indent(..) + | Cmd::Insert(..) + | Cmd::Kill(_) + | Cmd::ReplaceChar(..) + | Cmd::Replace(..) + | Cmd::SelfInsert(..) + | Cmd::ViYankTo(_) + | Cmd::Yank(..) // Cmd::TransposeChars | TODO Validate + ) } fn is_repeatable(&self) -> bool { @@ -130,6 +168,8 @@ impl Cmd { // Replay this command with a possible different `RepeatCount`. fn redo(&self, new: Option, wrt: &dyn Refresher) -> Self { match *self { + Cmd::Dedent(ref mvt) => Cmd::Dedent(mvt.redo(new)), + Cmd::Indent(ref mvt) => Cmd::Indent(mvt.redo(new)), Cmd::Insert(previous, ref text) => { Cmd::Insert(repeat_count(previous, new), text.clone()) } @@ -188,26 +228,33 @@ pub enum Word { /// Where to move with respect to word boundary #[derive(Debug, Clone, PartialEq, Copy)] pub enum At { + /// Start of word. Start, + /// Before end of word. BeforeEnd, + /// After end of word. AfterEnd, } /// Where to paste (relative to cursor position) #[derive(Debug, Clone, PartialEq, Copy)] pub enum Anchor { + /// After cursor After, + /// Before cursor Before, } -/// Vi character search +/// character search #[derive(Debug, Clone, PartialEq, Copy)] pub enum CharSearch { + /// Forward search Forward(char), - // until + /// Forward search until ForwardBefore(char), + /// Backward search Backward(char), - // until + /// Backward search until BackwardAfter(char), } @@ -224,8 +271,10 @@ impl CharSearch { /// Where to move #[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] pub enum Movement { - WholeLine, // not really a movement + /// Whole current line (not really a movement but a range) + WholeLine, /// beginning-of-line BeginningOfLine, /// end-of-line @@ -234,7 +283,7 @@ pub enum Movement { 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 + /// character-search, character-search-backward, vi-char-search ViCharSearch(RepeatCount, CharSearch), /// vi-first-print ViFirstPrint, @@ -242,6 +291,16 @@ pub enum Movement { BackwardChar(RepeatCount), /// forward-char ForwardChar(RepeatCount), + /// move to the same column on the previous line + LineUp(RepeatCount), + /// move to the same column on the next line + LineDown(RepeatCount), + /// Whole user input (not really a movement but a range) + WholeBuffer, + /// beginning-of-buffer + BeginningOfBuffer, + /// end-of-buffer + EndOfBuffer, } impl Movement { @@ -263,12 +322,18 @@ impl Movement { } Movement::BackwardChar(previous) => Movement::BackwardChar(repeat_count(previous, new)), Movement::ForwardChar(previous) => Movement::ForwardChar(repeat_count(previous, new)), + Movement::LineUp(previous) => Movement::LineUp(repeat_count(previous, new)), + Movement::LineDown(previous) => Movement::LineDown(repeat_count(previous, new)), + Movement::WholeBuffer => Movement::WholeBuffer, + Movement::BeginningOfBuffer => Movement::BeginningOfBuffer, + Movement::EndOfBuffer => Movement::EndOfBuffer, } } } -#[derive(PartialEq)] -enum InputMode { +/// Vi input modes +#[derive(Clone, Copy, PartialEq)] +pub enum InputMode { /// Vi Command/Alternate Command, /// Insert/Input mode @@ -279,21 +344,35 @@ enum InputMode { /// Transform key(s) to commands based on current input mode pub struct InputState { - mode: EditMode, - custom_bindings: Arc>>, - input_mode: InputMode, // vi only ? + pub(crate) mode: EditMode, + custom_bindings: Arc>>, + pub(crate) input_mode: InputMode, // vi only ? // numeric arguments: http://web.mit.edu/gnu/doc/html/rlman_1.html#SEC7 num_args: i16, last_cmd: Cmd, // vi only last_char_search: Option, // vi only } +/// Provide indirect mutation to user input. +pub trait Invoke { + /// currently edited line + fn input(&self) -> &str; + // TODO + //fn invoke(&mut self, cmd: Cmd) -> Result; +} + +impl Invoke for &str { + fn input(&self) -> &str { + self + } +} + 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`] with a specific message instead of hint - fn refresh_line_with_msg(&mut self, msg: Option) -> Result<()>; + fn refresh_line_with_msg(&mut self, msg: Option<&str>) -> 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. @@ -306,10 +385,16 @@ pub trait Refresher { fn is_cursor_at_end(&self) -> bool; /// Returns `true` if there is a hint displayed. fn has_hint(&self) -> bool; + /// Returns the hint text that is shown after the current cursor position. + fn hint_text(&self) -> Option<&str>; + /// currently edited line + fn line(&self) -> &str; + /// Current cursor position (byte position) + fn pos(&self) -> usize; } impl InputState { - pub fn new(config: &Config, custom_bindings: Arc>>) -> Self { + pub fn new(config: &Config, custom_bindings: Arc>>) -> Self { Self { mode: config.edit_mode(), custom_bindings, @@ -334,19 +419,97 @@ impl InputState { single_esc_abort: bool, ) -> Result { match self.mode { - EditMode::Emacs => self.emacs(rdr, wrt, single_esc_abort), - EditMode::Vi if self.input_mode != InputMode::Command => self.vi_insert(rdr, wrt), - EditMode::Vi => self.vi_command(rdr, wrt), + EditMode::Emacs => { + let key = rdr.next_key(single_esc_abort)?; + self.emacs(rdr, wrt, key) + } + EditMode::Vi if self.input_mode != InputMode::Command => { + let key = rdr.next_key(false)?; + self.vi_insert(rdr, wrt, key) + } + EditMode::Vi => { + let key = rdr.next_key(false)?; + self.vi_command(rdr, wrt, key) + } + } + } + + /// Application customized binding + fn custom_binding( + &self, + wrt: &mut dyn Refresher, + evt: &Event, + n: RepeatCount, + positive: bool, + ) -> Option { + let bindings = self.custom_bindings.read().unwrap(); + let handler = bindings.get(evt).or_else(|| bindings.get(&Event::Any)); + if let Some(handler) = handler { + match handler { + EventHandler::Simple(cmd) => Some(cmd.clone()), + EventHandler::Conditional(handler) => { + let ctx = EventContext::new(self, wrt); + handler.handle(evt, n, positive, &ctx) + } + } + } else { + None } } - // TODO dynamic prompt (arg: ?) + /// Terminal peculiar binding + fn term_binding( + rdr: &mut R, + wrt: &mut dyn Refresher, + key: &KeyEvent, + ) -> Option { + let cmd = rdr.find_binding(key); + if cmd == Some(Cmd::EndOfFile) && !wrt.line().is_empty() { + None // ReadlineError::Eof only if line is empty + } else { + cmd + } + } + + fn custom_seq_binding( + &self, + rdr: &mut R, + wrt: &mut dyn Refresher, + evt: &mut Event, + n: RepeatCount, + positive: bool, + ) -> Result> { + let bindings = self.custom_bindings.read().unwrap(); + while let Some(subtrie) = bindings.get_raw_descendant(evt) { + let snd_key = rdr.next_key(true)?; + if let Event::KeySeq(ref mut key_seq) = evt { + key_seq.push(snd_key); + } else { + break; + } + let handler = subtrie.get(evt).unwrap(); + if let Some(handler) = handler { + let cmd = match handler { + EventHandler::Simple(cmd) => Some(cmd.clone()), + EventHandler::Conditional(handler) => { + let ctx = EventContext::new(self, wrt); + handler.handle(evt, n, positive, &ctx) + } + }; + if cmd.is_some() { + return Ok(cmd); + } + } + } + Ok(None) + } + fn emacs_digit_argument( &mut self, rdr: &mut R, wrt: &mut dyn Refresher, digit: char, - ) -> Result { + ) -> Result { #[allow(clippy::cast_possible_truncation)] match digit { '0'..='9' => { @@ -362,7 +525,7 @@ impl InputState { let key = rdr.next_key(true)?; #[allow(clippy::cast_possible_truncation)] match key { - KeyPress::Char(digit @ '0'..='9') | KeyPress::Meta(digit @ '0'..='9') => { + E(K::Char(digit @ '0'..='9'), m) if m == M::NONE || m == M::ALT => { if self.num_args == -1 { self.num_args *= digit.to_digit(10).unwrap() as i16; } else if self.num_args.abs() < 1000 { @@ -373,7 +536,7 @@ impl InputState { .saturating_add(digit.to_digit(10).unwrap() as i16); } } - KeyPress::Char('-') | KeyPress::Meta('-') => {} + E(K::Char('-'), m) if m == M::NONE || m == M::ALT => {} _ => { wrt.refresh_line()?; return Ok(key); @@ -386,60 +549,53 @@ impl InputState { &mut self, rdr: &mut R, wrt: &mut dyn Refresher, - single_esc_abort: bool, + mut key: KeyEvent, ) -> Result { - let mut key = rdr.next_key(single_esc_abort)?; - if let KeyPress::Meta(digit @ '-') = key { + if let E(K::Char(digit @ '-'), M::ALT) = key { key = self.emacs_digit_argument(rdr, wrt, digit)?; - } else if let KeyPress::Meta(digit @ '0'..='9') = key { + } else if let E(K::Char(digit @ '0'..='9'), M::ALT) = key { key = self.emacs_digit_argument(rdr, wrt, digit)?; } let (n, positive) = self.emacs_num_args(); // consume them in all cases - { - let bindings = self.custom_bindings.read().unwrap(); - if let Some(cmd) = bindings.get(&key) { - debug!(target: "rustyline", "Custom command: {:?}", cmd); - return Ok(if cmd.is_repeatable() { - cmd.redo(Some(n), wrt) - } else { - cmd.clone() - }); - } + + let mut evt = key.into(); + if let Some(cmd) = self.custom_binding(wrt, &evt, n, positive) { + return Ok(if cmd.is_repeatable() { + cmd.redo(Some(n), wrt) + } else { + cmd + }); + } else if let Some(cmd) = InputState::term_binding(rdr, wrt, &key) { + return Ok(cmd); } let cmd = match key { - KeyPress::Char(c) => { + E(K::Char(c), M::NONE) => { if positive { Cmd::SelfInsert(n, c) } else { Cmd::Unknown } } - KeyPress::Ctrl('A') => Cmd::Move(Movement::BeginningOfLine), - KeyPress::Ctrl('B') => { - if positive { - Cmd::Move(Movement::BackwardChar(n)) - } else { - Cmd::Move(Movement::ForwardChar(n)) - } - } - KeyPress::Ctrl('E') => Cmd::Move(Movement::EndOfLine), - KeyPress::Ctrl('F') => { - if positive { - Cmd::Move(Movement::ForwardChar(n)) - } else { - Cmd::Move(Movement::BackwardChar(n)) - } - } - KeyPress::Ctrl('G') | KeyPress::Esc | KeyPress::Meta('\x07') => Cmd::Abort, - KeyPress::Ctrl('H') | KeyPress::Backspace => { - if positive { - Cmd::Kill(Movement::BackwardChar(n)) - } else { - Cmd::Kill(Movement::ForwardChar(n)) - } - } - KeyPress::BackTab => Cmd::CompleteBackward, - KeyPress::Tab => { + E(K::Char('A'), M::CTRL) => Cmd::Move(Movement::BeginningOfLine), + E(K::Char('B'), M::CTRL) => Cmd::Move(if positive { + Movement::BackwardChar(n) + } else { + Movement::ForwardChar(n) + }), + E(K::Char('E'), M::CTRL) => Cmd::Move(Movement::EndOfLine), + E(K::Char('F'), M::CTRL) => Cmd::Move(if positive { + Movement::ForwardChar(n) + } else { + Movement::BackwardChar(n) + }), + E(K::Char('G'), M::CTRL) | E::ESC | E(K::Char('G'), M::CTRL_ALT) => Cmd::Abort, + E(K::Char('H'), M::CTRL) | E::BACKSPACE => Cmd::Kill(if positive { + Movement::BackwardChar(n) + } else { + Movement::ForwardChar(n) + }), + E(K::BackTab, M::NONE) => Cmd::CompleteBackward, + E(K::Char('I'), M::CTRL) | E(K::Tab, M::NONE) => { if positive { Cmd::Complete } else { @@ -447,61 +603,92 @@ impl InputState { } } // Don't complete hints when the cursor is not at the end of a line - KeyPress::Right if wrt.has_hint() && wrt.is_cursor_at_end() => Cmd::CompleteHint, - KeyPress::Ctrl('K') => { - if positive { - Cmd::Kill(Movement::EndOfLine) + E(K::Right, M::NONE) if wrt.has_hint() && wrt.is_cursor_at_end() => Cmd::CompleteHint, + E(K::Char('K'), M::CTRL) => Cmd::Kill(if positive { + Movement::EndOfLine + } else { + Movement::BeginningOfLine + }), + E(K::Char('L'), M::CTRL) => Cmd::ClearScreen, + E(K::Char('N'), M::CTRL) => Cmd::NextHistory, + E(K::Char('P'), M::CTRL) => Cmd::PreviousHistory, + E(K::Char('X'), M::CTRL) => { + if let Some(cmd) = self.custom_seq_binding(rdr, wrt, &mut evt, n, positive)? { + cmd } else { - Cmd::Kill(Movement::BeginningOfLine) + let snd_key = match evt { + // we may have already read the second key in custom_seq_binding + Event::KeySeq(ref key_seq) if key_seq.len() > 1 => key_seq[1], + _ => rdr.next_key(true)?, + }; + match snd_key { + E(K::Char('G'), M::CTRL) | E::ESC => Cmd::Abort, + E(K::Char('U'), M::CTRL) => Cmd::Undo(n), + E(K::Backspace, M::NONE) => Cmd::Kill(if positive { + Movement::BeginningOfLine + } else { + Movement::EndOfLine + }), + _ => Cmd::Unknown, + } } } - KeyPress::Ctrl('L') => Cmd::ClearScreen, - KeyPress::Ctrl('N') => Cmd::NextHistory, - KeyPress::Ctrl('P') => Cmd::PreviousHistory, - KeyPress::Ctrl('X') => { - let snd_key = rdr.next_key(true)?; - match snd_key { - KeyPress::Ctrl('G') | KeyPress::Esc => Cmd::Abort, - KeyPress::Ctrl('U') => Cmd::Undo(n), + // character-search, character-search-backward + E(K::Char(']'), m @ M::CTRL) | E(K::Char(']'), m @ M::CTRL_ALT) => { + let ch = rdr.next_key(false)?; + match ch { + E(K::Char(ch), M::NONE) => Cmd::Move(Movement::ViCharSearch( + n, + if positive { + if m.contains(M::ALT) { + CharSearch::Backward(ch) + } else { + CharSearch::ForwardBefore(ch) + } + } else if m.contains(M::ALT) { + CharSearch::ForwardBefore(ch) + } else { + CharSearch::Backward(ch) + }, + )), _ => Cmd::Unknown, } } - KeyPress::Meta('\x08') | KeyPress::Meta('\x7f') => { - if positive { - Cmd::Kill(Movement::BackwardWord(n, Word::Emacs)) - } else { - Cmd::Kill(Movement::ForwardWord(n, At::AfterEnd, Word::Emacs)) - } - } - KeyPress::Meta('<') => Cmd::BeginningOfHistory, - KeyPress::Meta('>') => Cmd::EndOfHistory, - KeyPress::Meta('B') | KeyPress::Meta('b') => { - if positive { - Cmd::Move(Movement::BackwardWord(n, Word::Emacs)) - } else { - Cmd::Move(Movement::ForwardWord(n, At::AfterEnd, Word::Emacs)) - } - } - KeyPress::Meta('C') | KeyPress::Meta('c') => Cmd::CapitalizeWord, - KeyPress::Meta('D') | KeyPress::Meta('d') => { - if positive { - Cmd::Kill(Movement::ForwardWord(n, At::AfterEnd, Word::Emacs)) - } else { - Cmd::Kill(Movement::BackwardWord(n, Word::Emacs)) - } - } - KeyPress::Meta('F') | KeyPress::Meta('f') => { - if positive { - Cmd::Move(Movement::ForwardWord(n, At::AfterEnd, Word::Emacs)) - } else { - Cmd::Move(Movement::BackwardWord(n, Word::Emacs)) - } - } - KeyPress::Meta('L') | KeyPress::Meta('l') => Cmd::DowncaseWord, - KeyPress::Meta('T') | KeyPress::Meta('t') => Cmd::TransposeWords(n), - KeyPress::Meta('U') | KeyPress::Meta('u') => Cmd::UpcaseWord, - KeyPress::Meta('Y') | KeyPress::Meta('y') => Cmd::YankPop, - _ => self.common(rdr, key, n, positive)?, + E(K::Backspace, M::ALT) => Cmd::Kill(if positive { + Movement::BackwardWord(n, Word::Emacs) + } else { + Movement::ForwardWord(n, At::AfterEnd, Word::Emacs) + }), + E(K::Char('<'), M::ALT) => Cmd::BeginningOfHistory, + E(K::Char('>'), M::ALT) => Cmd::EndOfHistory, + E(K::Char('B'), M::ALT) + | E(K::Char('b'), M::ALT) + | E(K::Left, M::CTRL) + | E(K::Left, M::ALT) => Cmd::Move(if positive { + Movement::BackwardWord(n, Word::Emacs) + } else { + Movement::ForwardWord(n, At::AfterEnd, Word::Emacs) + }), + E(K::Char('C'), M::ALT) | E(K::Char('c'), M::ALT) => Cmd::CapitalizeWord, + E(K::Char('D'), M::ALT) | E(K::Char('d'), M::ALT) => Cmd::Kill(if positive { + Movement::ForwardWord(n, At::AfterEnd, Word::Emacs) + } else { + Movement::BackwardWord(n, Word::Emacs) + }), + E(K::Char('F'), M::ALT) + | E(K::Char('f'), M::ALT) + | E(K::Right, M::CTRL) + | E(K::Right, M::ALT) => Cmd::Move(if positive { + Movement::ForwardWord(n, At::AfterEnd, Word::Emacs) + } else { + Movement::BackwardWord(n, Word::Emacs) + }), + E(K::Char('L'), M::ALT) | E(K::Char('l'), M::ALT) => Cmd::DowncaseWord, + E(K::Char('T'), M::ALT) | E(K::Char('t'), M::ALT) => Cmd::TransposeWords(n), + // TODO ESC-R (r): Undo all changes made to this line. + E(K::Char('U'), M::ALT) | E(K::Char('u'), M::ALT) => Cmd::UpcaseWord, + E(K::Char('Y'), M::ALT) | E(K::Char('y'), M::ALT) => Cmd::YankPop, + _ => self.common(rdr, wrt, evt, key, n, positive)?, }; debug!(target: "rustyline", "Emacs command: {:?}", cmd); Ok(cmd) @@ -513,12 +700,12 @@ impl InputState { rdr: &mut R, wrt: &mut dyn Refresher, digit: char, - ) -> Result { + ) -> Result { self.num_args = digit.to_digit(10).unwrap() as i16; loop { wrt.refresh_prompt_and_line(&format!("(arg: {}) ", self.num_args))?; let key = rdr.next_key(false)?; - if let KeyPress::Char(digit @ '0'..='9') = key { + if let E(K::Char(digit @ '0'..='9'), M::NONE) = key { if self.num_args.abs() < 1000 { // shouldn't ever need more than 4 digits self.num_args = self @@ -533,89 +720,94 @@ impl InputState { } } - fn vi_command(&mut self, rdr: &mut R, wrt: &mut dyn Refresher) -> Result { - let mut key = rdr.next_key(false)?; - if let KeyPress::Char(digit @ '1'..='9') = key { + fn vi_command( + &mut self, + rdr: &mut R, + wrt: &mut dyn Refresher, + mut key: KeyEvent, + ) -> Result { + if let E(K::Char(digit @ '1'..='9'), M::NONE) = key { key = self.vi_arg_digit(rdr, wrt, digit)?; } let no_num_args = self.num_args == 0; let n = self.vi_num_args(); // consume them in all cases - { - let bindings = self.custom_bindings.read().unwrap(); - if let Some(cmd) = bindings.get(&key) { - debug!(target: "rustyline", "Custom command: {:?}", cmd); - return Ok(if cmd.is_repeatable() { - if no_num_args { - cmd.redo(None, wrt) - } else { - cmd.redo(Some(n), wrt) - } + let evt = key.into(); + if let Some(cmd) = self.custom_binding(wrt, &evt, n, true) { + return Ok(if cmd.is_repeatable() { + if no_num_args { + cmd.redo(None, wrt) } else { - cmd.clone() - }); - } + cmd.redo(Some(n), wrt) + } + } else { + cmd + }); + } else if let Some(cmd) = InputState::term_binding(rdr, wrt, &key) { + return Ok(cmd); } let cmd = match key { - KeyPress::Char('$') | - KeyPress::End => Cmd::Move(Movement::EndOfLine), - KeyPress::Char('.') => { // vi-redo (repeat last command) + E(K::Char('$'), M::NONE) | E(K::End, M::NONE) => Cmd::Move(Movement::EndOfLine), + E(K::Char('.'), M::NONE) => { + // vi-redo (repeat last command) if no_num_args { self.last_cmd.redo(None, wrt) } else { self.last_cmd.redo(Some(n), wrt) } - }, - // TODO KeyPress::Char('%') => Cmd::???, Move to the corresponding opening/closing bracket - KeyPress::Char('0') => Cmd::Move(Movement::BeginningOfLine), - KeyPress::Char('^') => Cmd::Move(Movement::ViFirstPrint), - KeyPress::Char('a') => { + } + // TODO E(K::Char('%'), M::NONE) => Cmd::???, Move to the corresponding opening/closing + // bracket + E(K::Char('0'), M::NONE) => Cmd::Move(Movement::BeginningOfLine), + E(K::Char('^'), M::NONE) => Cmd::Move(Movement::ViFirstPrint), + E(K::Char('a'), M::NONE) => { // vi-append-mode self.input_mode = InputMode::Insert; wrt.doing_insert(); Cmd::Move(Movement::ForwardChar(n)) } - KeyPress::Char('A') => { + E(K::Char('A'), M::NONE) => { // 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') => { + E(K::Char('b'), M::NONE) => Cmd::Move(Movement::BackwardWord(n, Word::Vi)), /* vi-prev-word */ + E(K::Char('B'), M::NONE) => Cmd::Move(Movement::BackwardWord(n, Word::Big)), + E(K::Char('c'), M::NONE) => { self.input_mode = InputMode::Insert; match self.vi_cmd_motion(rdr, wrt, key, n)? { Some(mvt) => Cmd::Replace(mvt, None), None => Cmd::Unknown, } } - KeyPress::Char('C') => { + E(K::Char('C'), M::NONE) => { self.input_mode = InputMode::Insert; Cmd::Replace(Movement::EndOfLine, None) } - KeyPress::Char('d') => { - match self.vi_cmd_motion(rdr, wrt, key, n)? { - Some(mvt) => Cmd::Kill(mvt), - None => Cmd::Unknown, - } + E(K::Char('d'), M::NONE) => match self.vi_cmd_motion(rdr, wrt, key, n)? { + Some(mvt) => Cmd::Kill(mvt), + None => Cmd::Unknown, + }, + E(K::Char('D'), M::NONE) | E(K::Char('K'), M::CTRL) => Cmd::Kill(Movement::EndOfLine), + E(K::Char('e'), M::NONE) => { + Cmd::Move(Movement::ForwardWord(n, At::BeforeEnd, Word::Vi)) } - KeyPress::Char('D') | - KeyPress::Ctrl('K') => Cmd::Kill(Movement::EndOfLine), - KeyPress::Char('e') => Cmd::Move(Movement::ForwardWord(n, At::BeforeEnd, Word::Vi)), - KeyPress::Char('E') => Cmd::Move(Movement::ForwardWord(n, At::BeforeEnd, Word::Big)), - KeyPress::Char('i') => { + E(K::Char('E'), M::NONE) => { + Cmd::Move(Movement::ForwardWord(n, At::BeforeEnd, Word::Big)) + } + E(K::Char('i'), M::NONE) => { // vi-insertion-mode self.input_mode = InputMode::Insert; wrt.doing_insert(); Cmd::Noop } - KeyPress::Char('I') => { + E(K::Char('I'), M::NONE) => { // vi-insert-beg self.input_mode = InputMode::Insert; wrt.doing_insert(); Cmd::Move(Movement::BeginningOfLine) } - KeyPress::Char(c) if c == 'f' || c == 'F' || c == 't' || c == 'T' => { + E(K::Char(c), M::NONE) if c == 'f' || c == 'F' || c == 't' || c == 'T' => { // vi-char-search let cs = self.vi_char_search(rdr, c)?; match cs { @@ -623,81 +815,85 @@ impl InputState { None => Cmd::Unknown, } } - KeyPress::Char(';') => { - match self.last_char_search { - Some(cs) => Cmd::Move(Movement::ViCharSearch(n, cs)), - None => Cmd::Noop, - } - } - KeyPress::Char(',') => { - match self.last_char_search { - Some(ref cs) => Cmd::Move(Movement::ViCharSearch(n, cs.opposite())), - None => Cmd::Noop, - } - } - // TODO KeyPress::Char('G') => Cmd::???, Move to the history line n - KeyPress::Char('p') => Cmd::Yank(n, Anchor::After), // vi-put - KeyPress::Char('P') => Cmd::Yank(n, Anchor::Before), // vi-put - KeyPress::Char('r') => { + E(K::Char(';'), M::NONE) => match self.last_char_search { + Some(cs) => Cmd::Move(Movement::ViCharSearch(n, cs)), + None => Cmd::Noop, + }, + E(K::Char(','), M::NONE) => match self.last_char_search { + Some(ref cs) => Cmd::Move(Movement::ViCharSearch(n, cs.opposite())), + None => Cmd::Noop, + }, + // TODO E(K::Char('G'), M::NONE) => Cmd::???, Move to the history line n + E(K::Char('p'), M::NONE) => Cmd::Yank(n, Anchor::After), // vi-put + E(K::Char('P'), M::NONE) => Cmd::Yank(n, Anchor::Before), // vi-put + E(K::Char('r'), M::NONE) => { // vi-replace-char: let ch = rdr.next_key(false)?; match ch { - KeyPress::Char(c) => Cmd::ReplaceChar(n, c), - KeyPress::Esc => Cmd::Noop, + E(K::Char(c), M::NONE) => Cmd::ReplaceChar(n, c), + E::ESC => Cmd::Noop, _ => Cmd::Unknown, } } - KeyPress::Char('R') => { + E(K::Char('R'), M::NONE) => { // vi-replace-mode (overwrite-mode) self.input_mode = InputMode::Replace; Cmd::Replace(Movement::ForwardChar(0), None) } - KeyPress::Char('s') => { + E(K::Char('s'), M::NONE) => { // vi-substitute-char: self.input_mode = InputMode::Insert; Cmd::Replace(Movement::ForwardChar(n), None) } - KeyPress::Char('S') => { + E(K::Char('S'), M::NONE) => { // 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 self.vi_cmd_motion(rdr, wrt, key, n)? { - Some(mvt) => Cmd::ViYankTo(mvt), - None => Cmd::Unknown, - } + E(K::Char('u'), M::NONE) => Cmd::Undo(n), + // E(K::Char('U'), M::NONE) => Cmd::???, // revert-line + E(K::Char('w'), M::NONE) => Cmd::Move(Movement::ForwardWord(n, At::Start, Word::Vi)), /* vi-next-word */ + E(K::Char('W'), M::NONE) => Cmd::Move(Movement::ForwardWord(n, At::Start, Word::Big)), /* vi-next-word */ + // TODO move backward if eol + E(K::Char('x'), M::NONE) => Cmd::Kill(Movement::ForwardChar(n)), // vi-delete + E(K::Char('X'), M::NONE) => Cmd::Kill(Movement::BackwardChar(n)), // vi-rubout + E(K::Char('y'), M::NONE) => match self.vi_cmd_motion(rdr, wrt, key, n)? { + Some(mvt) => Cmd::ViYankTo(mvt), + None => Cmd::Unknown, + }, + // E(K::Char('Y'), M::NONE) => Cmd::???, // vi-yank-to + E(K::Char('h'), M::NONE) | E(K::Char('H'), M::CTRL) | E::BACKSPACE => { + Cmd::Move(Movement::BackwardChar(n)) } - // KeyPress::Char('Y') => Cmd::???, // vi-yank-to - KeyPress::Char('h') | - KeyPress::Ctrl('H') | - KeyPress::Backspace => Cmd::Move(Movement::BackwardChar(n)), - KeyPress::Ctrl('G') => Cmd::Abort, - KeyPress::Char('l') | - KeyPress::Char(' ') => Cmd::Move(Movement::ForwardChar(n)), - KeyPress::Ctrl('L') => Cmd::ClearScreen, - KeyPress::Char('+') | - KeyPress::Char('j') | // TODO: move to the start of the line. - KeyPress::Ctrl('N') => Cmd::NextHistory, - KeyPress::Char('-') | - KeyPress::Char('k') | // TODO: move to the start of the line. - KeyPress::Ctrl('P') => Cmd::PreviousHistory, - KeyPress::Ctrl('R') => { + E(K::Char('G'), M::CTRL) => Cmd::Abort, + E(K::Char('l'), M::NONE) | E(K::Char(' '), M::NONE) => { + Cmd::Move(Movement::ForwardChar(n)) + } + E(K::Char('L'), M::CTRL) => Cmd::ClearScreen, + E(K::Char('+'), M::NONE) | E(K::Char('j'), M::NONE) => Cmd::LineDownOrNextHistory(n), + // TODO: move to the start of the line. + E(K::Char('N'), M::CTRL) => Cmd::NextHistory, + E(K::Char('-'), M::NONE) | E(K::Char('k'), M::NONE) => Cmd::LineUpOrPreviousHistory(n), + // TODO: move to the start of the line. + E(K::Char('P'), M::CTRL) => Cmd::PreviousHistory, + E(K::Char('R'), M::CTRL) => { self.input_mode = InputMode::Insert; // TODO Validate Cmd::ReverseSearchHistory } - KeyPress::Ctrl('S') => { + E(K::Char('S'), M::CTRL) => { self.input_mode = InputMode::Insert; // TODO Validate Cmd::ForwardSearchHistory } - KeyPress::Esc => Cmd::Noop, - _ => self.common(rdr, key, n, true)?, + E(K::Char('<'), M::NONE) => match self.vi_cmd_motion(rdr, wrt, key, n)? { + Some(mvt) => Cmd::Dedent(mvt), + None => Cmd::Unknown, + }, + E(K::Char('>'), M::NONE) => match self.vi_cmd_motion(rdr, wrt, key, n)? { + Some(mvt) => Cmd::Indent(mvt), + None => Cmd::Unknown, + }, + E::ESC => Cmd::Noop, + _ => self.common(rdr, wrt, evt, key, n, true)?, }; debug!(target: "rustyline", "Vi command: {:?}", cmd); if cmd.is_repeatable_change() { @@ -706,45 +902,56 @@ impl InputState { Ok(cmd) } - fn vi_insert(&mut self, rdr: &mut R, wrt: &mut dyn Refresher) -> Result { - let key = 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() - }); - } + fn vi_insert( + &mut self, + rdr: &mut R, + wrt: &mut dyn Refresher, + key: KeyEvent, + ) -> Result { + let evt = key.into(); + if let Some(cmd) = self.custom_binding(wrt, &evt, 0, true) { + return Ok(if cmd.is_repeatable() { + cmd.redo(None, wrt) + } else { + cmd + }); + } else if let Some(cmd) = InputState::term_binding(rdr, wrt, &key) { + return Ok(cmd); } let cmd = match key { - KeyPress::Char(c) => { + E(K::Char(c), M::NONE) => { 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::BackTab => Cmd::CompleteBackward, - KeyPress::Tab => Cmd::Complete, + E(K::Char('H'), M::CTRL) | E::BACKSPACE => Cmd::Kill(Movement::BackwardChar(1)), + E(K::BackTab, M::NONE) => Cmd::CompleteBackward, + E(K::Char('I'), M::CTRL) | E(K::Tab, M::NONE) => Cmd::Complete, // Don't complete hints when the cursor is not at the end of a line - KeyPress::Right if wrt.has_hint() && wrt.is_cursor_at_end() => Cmd::CompleteHint, - KeyPress::Esc => { + E(K::Right, M::NONE) if wrt.has_hint() && wrt.is_cursor_at_end() => Cmd::CompleteHint, + E(K::Char(k), M::ALT) => { + debug!(target: "rustyline", "Vi fast command mode: {}", k); + self.input_mode = InputMode::Command; + wrt.done_inserting(); + + self.vi_command(rdr, wrt, E(K::Char(k), M::NONE))? + } + E::ESC => { // vi-movement-mode/vi-command-mode self.input_mode = InputMode::Command; wrt.done_inserting(); Cmd::Move(Movement::BackwardChar(1)) } - _ => self.common(rdr, key, 1, true)?, + _ => self.common(rdr, wrt, evt, key, 1, true)?, }; debug!(target: "rustyline", "Vi insert: {:?}", cmd); if cmd.is_repeatable_change() { - if let (Cmd::Replace(_, _), Cmd::SelfInsert(_, _)) = (&self.last_cmd, &cmd) { + #[allow(clippy::if_same_then_else)] + if let (Cmd::Replace(..), Cmd::SelfInsert(..)) = (&self.last_cmd, &cmd) { // replacing... - } else if let (Cmd::SelfInsert(_, _), Cmd::SelfInsert(_, _)) = (&self.last_cmd, &cmd) { + } else if let (Cmd::SelfInsert(..), Cmd::SelfInsert(..)) = (&self.last_cmd, &cmd) { // inserting... } else { self.last_cmd = cmd.clone(); @@ -757,7 +964,7 @@ impl InputState { &mut self, rdr: &mut R, wrt: &mut dyn Refresher, - key: KeyPress, + key: KeyEvent, n: RepeatCount, ) -> Result> { let mut mvt = rdr.next_key(false)?; @@ -765,49 +972,46 @@ impl InputState { return Ok(Some(Movement::WholeLine)); } let mut n = n; - if let KeyPress::Char(digit @ '1'..='9') = mvt { + if let E(K::Char(digit @ '1'..='9'), M::NONE) = mvt { // vi-arg-digit mvt = self.vi_arg_digit(rdr, wrt, digit)?; n = self.vi_num_args().saturating_mul(n); } Ok(match mvt { - KeyPress::Char('$') => Some(Movement::EndOfLine), - KeyPress::Char('0') => Some(Movement::BeginningOfLine), - KeyPress::Char('^') => Some(Movement::ViFirstPrint), - KeyPress::Char('b') => Some(Movement::BackwardWord(n, Word::Vi)), - KeyPress::Char('B') => Some(Movement::BackwardWord(n, Word::Big)), - KeyPress::Char('e') => Some(Movement::ForwardWord(n, At::AfterEnd, Word::Vi)), - KeyPress::Char('E') => Some(Movement::ForwardWord(n, At::AfterEnd, Word::Big)), - KeyPress::Char(c) if c == 'f' || c == 'F' || c == 't' || c == 'T' => { + E(K::Char('$'), M::NONE) => Some(Movement::EndOfLine), + E(K::Char('0'), M::NONE) => Some(Movement::BeginningOfLine), + E(K::Char('^'), M::NONE) => Some(Movement::ViFirstPrint), + E(K::Char('b'), M::NONE) => Some(Movement::BackwardWord(n, Word::Vi)), + E(K::Char('B'), M::NONE) => Some(Movement::BackwardWord(n, Word::Big)), + E(K::Char('e'), M::NONE) => Some(Movement::ForwardWord(n, At::AfterEnd, Word::Vi)), + E(K::Char('E'), M::NONE) => Some(Movement::ForwardWord(n, At::AfterEnd, Word::Big)), + E(K::Char(c), M::NONE) if c == 'f' || c == 'F' || c == 't' || c == 'T' => { let cs = self.vi_char_search(rdr, c)?; - match cs { - Some(cs) => Some(Movement::ViCharSearch(n, cs)), - None => None, - } + cs.map(|cs| Movement::ViCharSearch(n, cs)) } - 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 => { + E(K::Char(';'), M::NONE) => self + .last_char_search + .map(|cs| Movement::ViCharSearch(n, cs)), + E(K::Char(','), M::NONE) => self + .last_char_search + .map(|cs| Movement::ViCharSearch(n, cs.opposite())), + E(K::Char('h'), M::NONE) | E(K::Char('H'), M::CTRL) | E::BACKSPACE => { Some(Movement::BackwardChar(n)) } - KeyPress::Char('l') | KeyPress::Char(' ') => Some(Movement::ForwardChar(n)), - KeyPress::Char('w') => { + E(K::Char('l'), M::NONE) | E(K::Char(' '), M::NONE) => Some(Movement::ForwardChar(n)), + E(K::Char('j'), M::NONE) | E(K::Char('+'), M::NONE) => Some(Movement::LineDown(n)), + E(K::Char('k'), M::NONE) | E(K::Char('-'), M::NONE) => Some(Movement::LineUp(n)), + E(K::Char('w'), M::NONE) => { // 'cw' is 'ce' - if key == KeyPress::Char('c') { + if key == E(K::Char('c'), M::NONE) { Some(Movement::ForwardWord(n, At::AfterEnd, Word::Vi)) } else { Some(Movement::ForwardWord(n, At::Start, Word::Vi)) } } - KeyPress::Char('W') => { + E(K::Char('W'), M::NONE) => { // 'cW' is 'cE' - if key == KeyPress::Char('c') { + if key == E(K::Char('c'), M::NONE) { Some(Movement::ForwardWord(n, At::AfterEnd, Word::Big)) } else { Some(Movement::ForwardWord(n, At::Start, Word::Big)) @@ -824,7 +1028,7 @@ impl InputState { ) -> Result> { let ch = rdr.next_key(false)?; Ok(match ch { - KeyPress::Char(ch) => { + E(K::Char(ch), M::NONE) => { let cs = match cmd { 'f' => CharSearch::Forward(ch), 't' => CharSearch::ForwardBefore(ch), @@ -842,74 +1046,88 @@ impl InputState { fn common( &mut self, rdr: &mut R, - key: KeyPress, + wrt: &mut dyn Refresher, + mut evt: Event, + key: KeyEvent, n: RepeatCount, positive: bool, ) -> Result { Ok(match key { - KeyPress::Home => Cmd::Move(Movement::BeginningOfLine), - KeyPress::Left => { - if positive { - Cmd::Move(Movement::BackwardChar(n)) - } else { - Cmd::Move(Movement::ForwardChar(n)) - } - } - KeyPress::Ctrl('C') => Cmd::Interrupt, - KeyPress::Ctrl('D') => Cmd::EndOfFile, - KeyPress::Delete => { - if positive { - Cmd::Kill(Movement::ForwardChar(n)) - } else { - Cmd::Kill(Movement::BackwardChar(n)) - } - } - KeyPress::End => Cmd::Move(Movement::EndOfLine), - KeyPress::Right => { - if positive { - Cmd::Move(Movement::ForwardChar(n)) + E(K::Home, M::NONE) => Cmd::Move(Movement::BeginningOfLine), + E(K::Left, M::NONE) => Cmd::Move(if positive { + Movement::BackwardChar(n) + } else { + Movement::ForwardChar(n) + }), + #[cfg(any(windows, test))] + E(K::Char('C'), M::CTRL) => Cmd::Interrupt, + E(K::Char('D'), M::CTRL) => { + if self.is_emacs_mode() && !wrt.line().is_empty() { + Cmd::Kill(if positive { + Movement::ForwardChar(n) + } else { + Movement::BackwardChar(n) + }) + } else if cfg!(window) || cfg!(test) || !wrt.line().is_empty() { + Cmd::EndOfFile } else { - Cmd::Move(Movement::BackwardChar(n)) + Cmd::Unknown } } - KeyPress::Ctrl('J') | - KeyPress::Enter => Cmd::AcceptLine, - KeyPress::Down => Cmd::NextHistory, - KeyPress::Up => Cmd::PreviousHistory, - KeyPress::Ctrl('R') => Cmd::ReverseSearchHistory, - KeyPress::Ctrl('S') => Cmd::ForwardSearchHistory, // most terminals override Ctrl+S to suspend execution - KeyPress::Ctrl('T') => Cmd::TransposeChars, - KeyPress::Ctrl('U') => { - if positive { - Cmd::Kill(Movement::BeginningOfLine) - } else { - Cmd::Kill(Movement::EndOfLine) - } - }, - KeyPress::Ctrl('Q') | // most terminals override Ctrl+Q to resume execution - KeyPress::Ctrl('V') => Cmd::QuotedInsert, - KeyPress::Ctrl('W') => { - if positive { - Cmd::Kill(Movement::BackwardWord(n, Word::Big)) - } else { - Cmd::Kill(Movement::ForwardWord(n, At::AfterEnd, Word::Big)) + E(K::Delete, M::NONE) => Cmd::Kill(if positive { + Movement::ForwardChar(n) + } else { + Movement::BackwardChar(n) + }), + E(K::End, M::NONE) => Cmd::Move(Movement::EndOfLine), + E(K::Right, M::NONE) => Cmd::Move(if positive { + Movement::ForwardChar(n) + } else { + Movement::BackwardChar(n) + }), + E(K::Char('J'), M::CTRL) | E(K::Char('M'), M::CTRL) | E::ENTER => { + Cmd::AcceptOrInsertLine { + accept_in_the_middle: true, } } - KeyPress::Ctrl('Y') => { + E(K::Down, M::NONE) => Cmd::LineDownOrNextHistory(1), + E(K::Up, M::NONE) => Cmd::LineUpOrPreviousHistory(1), + E(K::Char('R'), M::CTRL) => Cmd::ReverseSearchHistory, + // most terminals override Ctrl+S to suspend execution + E(K::Char('S'), M::CTRL) => Cmd::ForwardSearchHistory, + E(K::Char('T'), M::CTRL) => Cmd::TransposeChars, + E(K::Char('U'), M::CTRL) => Cmd::Kill(if positive { + Movement::BeginningOfLine + } else { + Movement::EndOfLine + }), + // most terminals override Ctrl+Q to resume execution + E(K::Char('Q'), M::CTRL) => Cmd::QuotedInsert, + #[cfg(not(windows))] + E(K::Char('V'), M::CTRL) => Cmd::QuotedInsert, + #[cfg(windows)] + E(K::Char('V'), M::CTRL) => Cmd::PasteFromClipboard, + E(K::Char('W'), M::CTRL) => Cmd::Kill(if positive { + Movement::BackwardWord(n, Word::Big) + } else { + Movement::ForwardWord(n, At::AfterEnd, Word::Big) + }), + E(K::Char('Y'), M::CTRL) => { if positive { Cmd::Yank(n, Anchor::Before) } else { Cmd::Unknown // TODO Validate } } - KeyPress::Ctrl('Z') => Cmd::Suspend, - KeyPress::Ctrl('_') => Cmd::Undo(n), - KeyPress::UnknownEscSeq => Cmd::Noop, - KeyPress::BracketedPasteStart => { + E(K::Char('_'), M::CTRL) => Cmd::Undo(n), + E(K::UnknownEscSeq, M::NONE) => Cmd::Noop, + E(K::BracketedPasteStart, M::NONE) => { let paste = rdr.read_pasted_text()?; Cmd::Insert(1, paste) - }, - _ => Cmd::Unknown, + } + _ => self + .custom_seq_binding(rdr, wrt, &mut evt, n, positive)? + .unwrap_or(Cmd::Unknown), }) } @@ -929,7 +1147,7 @@ impl InputState { if let (n, false) = num_args.overflowing_abs() { (n as RepeatCount, false) } else { - (RepeatCount::max_value(), false) + (RepeatCount::MAX, false) } } else { (num_args as RepeatCount, true) diff --git a/src/keys.rs b/src/keys.rs index 72876d8f6a..eb21a5e73e 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -1,90 +1,218 @@ //! Key constants -// #[non_exhaustive] +/// Input key pressed and modifiers #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum KeyPress { +pub struct KeyEvent(pub KeyCode, pub Modifiers); + +impl KeyEvent { + /// Constant value representing an unmodified press of `KeyCode::Backspace`. + pub(crate) const BACKSPACE: Self = Self(KeyCode::Backspace, Modifiers::NONE); + /// Constant value representing an unmodified press of `KeyCode::Enter`. + pub(crate) const ENTER: Self = Self(KeyCode::Enter, Modifiers::NONE); + /// Constant value representing an unmodified press of `KeyCode::Esc`. + pub(crate) const ESC: Self = Self(KeyCode::Esc, Modifiers::NONE); + + /// Constructor from `char` and modifiers + pub fn new(c: char, mut mods: Modifiers) -> Self { + use {KeyCode as K, KeyEvent as E, Modifiers as M}; + + if !c.is_control() { + if !mods.is_empty() { + mods.remove(M::SHIFT); // TODO Validate: no SHIFT even if + // `c` is uppercase + } + return E(K::Char(c), mods); + } + #[allow(clippy::match_same_arms)] + match c { + '\x00' => E(K::Char('@'), mods | M::CTRL), // '\0' + '\x01' => E(K::Char('A'), mods | M::CTRL), + '\x02' => E(K::Char('B'), mods | M::CTRL), + '\x03' => E(K::Char('C'), mods | M::CTRL), + '\x04' => E(K::Char('D'), mods | M::CTRL), + '\x05' => E(K::Char('E'), mods | M::CTRL), + '\x06' => E(K::Char('F'), mods | M::CTRL), + '\x07' => E(K::Char('G'), mods | M::CTRL), // '\a' + #[cfg(unix)] + '\x08' => E(K::Backspace, mods), // '\b' + #[cfg(windows)] + '\x08' => E(K::Char('H'), mods | M::CTRL), + #[cfg(unix)] + '\x09' => { + // '\t' + if mods.contains(M::SHIFT) { + mods.remove(M::SHIFT); + E(K::BackTab, mods) + } else { + E(K::Tab, mods) + } + } + #[cfg(windows)] + '\x09' => E(K::Char('I'), mods | M::CTRL), + '\x0a' => E(K::Char('J'), mods | M::CTRL), // '\n' (10) + '\x0b' => E(K::Char('K'), mods | M::CTRL), + '\x0c' => E(K::Char('L'), mods | M::CTRL), + #[cfg(unix)] + '\x0d' => E(K::Enter, mods), // '\r' (13) + #[cfg(windows)] + '\x0d' => E(K::Char('M'), mods | M::CTRL), + '\x0e' => E(K::Char('N'), mods | M::CTRL), + '\x0f' => E(K::Char('O'), mods | M::CTRL), + '\x10' => E(K::Char('P'), mods | M::CTRL), + '\x11' => E(K::Char('Q'), mods | M::CTRL), + '\x12' => E(K::Char('R'), mods | M::CTRL), + '\x13' => E(K::Char('S'), mods | M::CTRL), + '\x14' => E(K::Char('T'), mods | M::CTRL), + '\x15' => E(K::Char('U'), mods | M::CTRL), + '\x16' => E(K::Char('V'), mods | M::CTRL), + '\x17' => E(K::Char('W'), mods | M::CTRL), + '\x18' => E(K::Char('X'), mods | M::CTRL), + '\x19' => E(K::Char('Y'), mods | M::CTRL), + '\x1a' => E(K::Char('Z'), mods | M::CTRL), + '\x1b' => E(K::Esc, mods), // Ctrl-[, '\e' + '\x1c' => E(K::Char('\\'), mods | M::CTRL), + '\x1d' => E(K::Char(']'), mods | M::CTRL), + '\x1e' => E(K::Char('^'), mods | M::CTRL), + '\x1f' => E(K::Char('_'), mods | M::CTRL), + '\x7f' => E(K::Backspace, mods), // Rubout, Ctrl-? + '\u{9b}' => E(K::Esc, mods | M::SHIFT), + _ => E(K::Null, mods), + } + } + + /// Constructor from `char` with Ctrl modifier + pub fn ctrl(c: char) -> Self { + Self::new(c, Modifiers::CTRL) + } + + /// Constructor from `char` with Alt modifier + pub fn alt(c: char) -> Self { + Self::new(c, Modifiers::ALT) + } + + /// ctrl-a => ctrl-A (uppercase) + /// shift-A => A (no SHIFT modifier) + /// shift-Tab => BackTab + pub fn normalize(e: Self) -> Self { + use {KeyCode as K, KeyEvent as E, Modifiers as M}; + + match e { + E(K::Char(c), m) if c.is_ascii_control() => Self::new(c, m), + E(K::Char(c), m) if c.is_ascii_lowercase() && m.contains(M::CTRL) => { + E(K::Char(c.to_ascii_uppercase()), m) + } + E(K::Char(c), m) if c.is_ascii_uppercase() && m.contains(M::SHIFT) => { + E(K::Char(c), m ^ M::SHIFT) + } + E(K::Tab, m) if m.contains(M::SHIFT) => E(K::BackTab, m ^ M::SHIFT), + _ => e, + } + } +} + +impl From for KeyEvent { + fn from(c: char) -> Self { + Self::new(c, Modifiers::NONE) + } +} + +/// Input key pressed +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum KeyCode { + /// Unsupported escape sequence (on unix platform) UnknownEscSeq, - Backspace, // Ctrl('H') + /// ⌫ or Ctrl-H + Backspace, + /// ⇤ (usually Shift-Tab) BackTab, + /// Paste (on unix platform) BracketedPasteStart, + /// Paste (on unix platform) BracketedPasteEnd, + /// Single char Char(char), - ControlDown, - ControlLeft, - ControlRight, - ControlUp, - Ctrl(char), + /// ⌦ Delete, + /// ↓ arrow key Down, + /// ⇲ End, - Enter, // Ctrl('M') - Esc, // Ctrl('[') + /// ↵ or Ctrl-M + Enter, + /// Escape or Ctrl-[ + Esc, + /// Function key F(u8), + /// ⇱ Home, + /// Insert key Insert, + /// ← arrow key Left, - Meta(char), + /// \0 Null, + /// ⇟ PageDown, + /// ⇞ PageUp, + /// → arrow key Right, - ShiftDown, - ShiftLeft, - ShiftRight, - ShiftUp, - Tab, // Ctrl('I') + /// ⇥ or Ctrl-I + Tab, + /// ↑ arrow key Up, } -#[cfg(any(windows, unix))] -pub fn char_to_key_press(c: char) -> KeyPress { - if !c.is_control() { - return KeyPress::Char(c); - } - #[allow(clippy::match_same_arms)] - match c { - '\x00' => KeyPress::Ctrl(' '), - '\x01' => KeyPress::Ctrl('A'), - '\x02' => KeyPress::Ctrl('B'), - '\x03' => KeyPress::Ctrl('C'), - '\x04' => KeyPress::Ctrl('D'), - '\x05' => KeyPress::Ctrl('E'), - '\x06' => KeyPress::Ctrl('F'), - '\x07' => KeyPress::Ctrl('G'), - '\x08' => KeyPress::Backspace, // '\b' - '\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'), - '\x14' => KeyPress::Ctrl('T'), - '\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, // Ctrl-[ - '\x1c' => KeyPress::Ctrl('\\'), - '\x1d' => KeyPress::Ctrl(']'), - '\x1e' => KeyPress::Ctrl('^'), - '\x1f' => KeyPress::Ctrl('_'), - '\x7f' => KeyPress::Backspace, // Rubout - _ => KeyPress::Null, +bitflags::bitflags! { + /// The set of modifier keys that were triggered along with a key press. + pub struct Modifiers: u8 { + /// Control modifier + const CTRL = 1<<3; + /// Escape or Alt modifier + const ALT = 1<<2; + /// Shift modifier + const SHIFT = 1<<1; + + /// No modifier + const NONE = 0; + /// Ctrl + Shift + const CTRL_SHIFT = Self::CTRL.bits | Self::SHIFT.bits; + /// Alt + Shift + const ALT_SHIFT = Self::ALT.bits | Self::SHIFT.bits; + /// Ctrl + Alt + const CTRL_ALT = Self::CTRL.bits | Self::ALT.bits; + /// Ctrl + Alt + Shift + const CTRL_ALT_SHIFT = Self::CTRL.bits | Self::ALT.bits | Self::SHIFT.bits; } } #[cfg(test)] mod tests { - use super::{char_to_key_press, KeyPress}; + use super::{KeyCode as K, KeyEvent as E, Modifiers as M}; + + #[test] + fn new() { + assert_eq!(E::ESC, E::new('\x1b', M::NONE)); + } + + #[test] + #[cfg(unix)] + fn from() { + assert_eq!(E(K::Tab, M::NONE), E::from('\t')); + } + + #[test] + #[cfg(windows)] + fn from() { + assert_eq!(E(K::Char('I'), M::CTRL), E::from('\t')); + } #[test] - fn char_to_key() { - assert_eq!(KeyPress::Esc, char_to_key_press('\x1b')); + fn normalize() { + assert_eq!(E::ctrl('A'), E::normalize(E(K::Char('\x01'), M::NONE))); + assert_eq!(E::ctrl('A'), E::normalize(E::ctrl('a'))); + assert_eq!(E::from('A'), E::normalize(E(K::Char('A'), M::SHIFT))); + assert_eq!(E(K::BackTab, M::NONE), E::normalize(E(K::Tab, M::SHIFT))); } } diff --git a/src/layout.rs b/src/layout.rs index 6e317c3064..e70061eac1 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -2,8 +2,8 @@ use std::cmp::{Ord, Ordering, PartialOrd}; #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] pub struct Position { - pub col: usize, - pub row: usize, + pub col: usize, // The leftmost column is number 0. + pub row: usize, // The highest row is number 0. } impl PartialOrd for Position { @@ -25,9 +25,10 @@ impl Ord for Position { pub struct Layout { /// Prompt Unicode/visible width and height pub prompt_size: Position, + pub left_margin: usize, pub default_prompt: bool, - /// Cursor position (relative to the start of the prompt) + /// Cursor position (relative to the end of the prompt) pub cursor: Position, - /// Number of rows used so far (from start of prompt to end of input) + /// Number of rows used so far (from end of prompt to end of input) pub end: Position, } diff --git a/src/lib.rs b/src/lib.rs index 2754d72887..3f13d383ae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,8 +15,10 @@ //! Err(_) => println!("No input"), //! } //! ``` -// #![feature(non_exhaustive)] +#![warn(missing_docs)] +mod binding; +mod command; pub mod completion; pub mod config; mod edit; @@ -29,22 +31,23 @@ mod keys; mod kill_ring; mod layout; pub mod line_buffer; -mod undo; - mod tty; +mod undo; +pub mod validate; -use std::collections::HashMap; use std::fmt; -use std::io::{self, Write}; +use std::io::{self, BufRead, Write}; use std::path::Path; use std::result; use std::sync::{Arc, Mutex, RwLock}; use log::debug; +use radix_trie::Trie; use unicode_width::UnicodeWidthStr; use crate::tty::{RawMode, Renderer, Term, Terminal}; +pub use crate::binding::{ConditionalEventHandler, Event, EventContext, EventHandler}; use crate::completion::{longest_common_prefix, Candidate, Completer}; pub use crate::config::{ ColorMode, CompletionType, Config, EditMode, HistoryDuplicates, OutputStreamType, @@ -52,12 +55,13 @@ pub use crate::config::{ use crate::edit::State; use crate::highlight::Highlighter; use crate::hint::Hinter; -use crate::history::{Direction, History}; -pub use crate::keymap::{Anchor, At, CharSearch, Cmd, Movement, RepeatCount, Word}; +use crate::history::{History, SearchDirection}; +pub use crate::keymap::{Anchor, At, CharSearch, Cmd, InputMode, Movement, RepeatCount, Word}; use crate::keymap::{InputState, Refresher}; -pub use crate::keys::KeyPress; -use crate::kill_ring::{KillRing, Mode}; -use crate::line_buffer::WordAction; +pub use crate::keys::{KeyCode, KeyEvent, Modifiers}; +use crate::kill_ring::KillRing; + +use crate::validate::Validator; /// The error type for I/O and Linux Syscalls (Errno) pub type Result = result::Result; @@ -69,6 +73,11 @@ fn complete_line( input_state: &mut InputState, config: &Config, ) -> Result> { + #[cfg(all(unix, feature = "with-fuzzy"))] + use skim::prelude::{ + unbounded, Skim, SkimItem, SkimItemReceiver, SkimItemSender, SkimOptionsBuilder, + }; + let completer = s.helper.unwrap(); // get a list of completions let (start, candidates) = completer.complete(&s.line, s.line.pos(), &s.ctx)?; @@ -94,12 +103,11 @@ fn complete_line( Borrowed(candidate) };*/ completer.update(&mut s.line, start, candidate); - s.refresh_line()?; } else { // Restore current edited line s.line.update(&backup, backup_pos); - s.refresh_line()?; } + s.refresh_line()?; cmd = s.next_cmd(input_state, rdr, true)?; match cmd { @@ -170,10 +178,7 @@ fn complete_line( { cmd = s.next_cmd(input_state, rdr, false)?; } - match cmd { - Cmd::SelfInsert(1, 'y') | Cmd::SelfInsert(1, 'Y') => true, - _ => false, - } + matches!(cmd, Cmd::SelfInsert(1, 'y') | Cmd::SelfInsert(1, 'Y')) } else { true }; @@ -184,6 +189,64 @@ fn complete_line( Ok(None) } } else { + // if fuzzy feature is enabled and on unix based systems check for the + // corresponding completion_type + #[cfg(all(unix, feature = "with-fuzzy"))] + { + use std::borrow::Cow; + if CompletionType::Fuzzy == config.completion_type() { + struct Candidate { + index: usize, + text: String, + } + impl SkimItem for Candidate { + fn text(&self) -> Cow { + Cow::Borrowed(&self.text) + } + } + + let (tx_item, rx_item): (SkimItemSender, SkimItemReceiver) = unbounded(); + + candidates + .iter() + .enumerate() + .map(|(i, c)| Candidate { + index: i, + text: c.display().to_owned(), + }) + .for_each(|c| { + let _ = tx_item.send(Arc::new(c)); + }); + drop(tx_item); // so that skim could know when to stop waiting for more items. + + // setup skim and run with input options + // will display UI for fuzzy search and return selected results + // by default skim multi select is off so only expect one selection + + let options = SkimOptionsBuilder::default() + .height(Some("20%")) + .prompt(Some("? ")) + .reverse(true) + .build() + .unwrap(); + + let selected_items = Skim::run_with(&options, Some(rx_item)) + .map(|out| out.selected_items) + .unwrap_or_else(Vec::new); + + // match the first (and only) returned option with the candidate and update the + // line otherwise only refresh line to clear the skim UI changes + if let Some(item) = selected_items.first() { + let item: &Candidate = (*item).as_any() // cast to Any + .downcast_ref::() // downcast to concrete type + .expect("something wrong with downcast"); + if let Some(candidate) = candidates.get(item.index) { + completer.update(&mut s.line, start, candidate.replacement()); + } + } + s.refresh_line()?; + } + }; Ok(None) } } @@ -195,11 +258,14 @@ fn complete_hint_line(s: &mut State<'_, '_, H>) -> Result<()> { None => return Ok(()), }; s.line.move_end(); - if s.line.yank(hint, 1).is_none() { + if let Some(text) = hint.completion() { + if s.line.yank(text, 1).is_none() { + s.out.beep()?; + } + } else { s.out.beep()?; } - s.refresh_line_with_msg(None)?; - Ok(()) + s.refresh_line() } fn page_completions( @@ -239,6 +305,8 @@ fn page_completions( && cmd != Cmd::SelfInsert(1, ' ') && cmd != Cmd::Kill(Movement::BackwardChar(1)) && cmd != Cmd::AcceptLine + && cmd != Cmd::Newline + && !matches!(cmd, Cmd::AcceptOrInsertLine { .. }) { cmd = s.next_cmd(input_state, rdr, false)?; } @@ -246,15 +314,13 @@ fn page_completions( Cmd::SelfInsert(1, 'y') | Cmd::SelfInsert(1, 'Y') | Cmd::SelfInsert(1, ' ') => { pause_row += s.out.get_rows() - 1; } - Cmd::AcceptLine => { + Cmd::AcceptLine | Cmd::Newline | Cmd::AcceptOrInsertLine { .. } => { pause_row += 1; } _ => break, } - s.out.write_and_flush(b"\n")?; - } else { - s.out.write_and_flush(b"\n")?; } + s.out.write_and_flush(b"\n")?; ab.clear(); for col in 0..num_cols { let i = (col * num_rows) + row; @@ -276,6 +342,8 @@ fn page_completions( s.out.write_and_flush(ab.as_bytes())?; } s.out.write_and_flush(b"\n")?; + s.layout.end.row = 0; // dirty way to make clear_old_rows do nothing + s.layout.cursor.row = 0; s.refresh_line()?; Ok(None) } @@ -297,7 +365,7 @@ fn reverse_incremental_search( let mut search_buf = String::new(); let mut history_idx = history.len() - 1; - let mut direction = Direction::Reverse; + let mut direction = SearchDirection::Reverse; let mut success = true; let mut cmd; @@ -320,7 +388,7 @@ fn reverse_incremental_search( continue; } Cmd::ReverseSearchHistory => { - direction = Direction::Reverse; + direction = SearchDirection::Reverse; if history_idx > 0 { history_idx -= 1; } else { @@ -329,7 +397,7 @@ fn reverse_incremental_search( } } Cmd::ForwardSearchHistory => { - direction = Direction::Forward; + direction = SearchDirection::Forward; if history_idx < history.len() - 1 { history_idx += 1; } else { @@ -352,11 +420,9 @@ fn reverse_incremental_search( } } success = match history.search(&search_buf, history_idx, direction) { - Some(idx) => { - history_idx = idx; - let entry = history.get(idx).unwrap(); - let pos = entry.find(&search_buf).unwrap(); - s.line.update(entry, pos); + Some(sr) => { + history_idx = sr.idx; + s.line.update(sr.entry, sr.pos); true } _ => false, @@ -374,14 +440,14 @@ fn readline_edit( initial: Option<(&str, &str)>, editor: &mut Editor, original_mode: &tty::Mode, + term_key_map: tty::KeyMap, ) -> Result { - let helper = editor.helper.as_ref(); - let mut stdout = editor.term.create_writer(); editor.reset_kill_ring(); // TODO recreate a new kill ring vs Arc> let ctx = Context::new(&editor.history); - let mut s = State::new(&mut stdout, prompt, helper, ctx); + let mut s = State::new(&mut stdout, prompt, editor.helper.as_ref(), ctx); + let mut input_state = InputState::new(&editor.config, Arc::clone(&editor.custom_bindings)); s.line.set_delete_listener(editor.kill_ring.clone()); @@ -392,21 +458,27 @@ fn readline_edit( .update((left.to_owned() + right).as_ref(), left.len()); } - let mut rdr = editor.term.create_reader(&editor.config)?; - if editor.term.is_output_tty() { - s.move_cursor_at_leftmost(&mut rdr)?; + let mut rdr = editor.term.create_reader(&editor.config, term_key_map)?; + if editor.term.is_output_tty() && editor.config.check_cursor_position() { + if let Err(e) = s.move_cursor_at_leftmost(&mut rdr) { + if s.out.sigwinch() { + s.out.update_size(); + } else { + return Err(e); + } + } } s.refresh_line()?; loop { - let rc = s.next_cmd(&mut input_state, &mut rdr, false); - let mut cmd = rc?; + let mut cmd = s.next_cmd(&mut input_state, &mut rdr, false)?; if cmd.should_reset_kill_ring() { editor.reset_kill_ring(); } - // autocomplete + // First trigger commands that need extra input + if cmd == Cmd::Complete && s.helper.is_some() { let next = complete_line(&mut rdr, &mut s, &mut input_state, &editor.config)?; if let Some(next) = next { @@ -416,19 +488,6 @@ fn readline_edit( } } - if let Cmd::CompleteHint = cmd { - complete_hint_line(&mut s)?; - continue; - } - - if let Cmd::SelfInsert(n, c) = cmd { - s.edit_insert(c, n)?; - continue; - } else if let Cmd::Insert(n, text) = cmd { - s.edit_yank(&input_state, &text, Anchor::Before, n)?; - continue; - } - if cmd == Cmd::ReverseSearchHistory { // Search history backward let next = @@ -440,164 +499,51 @@ fn readline_edit( } } - match cmd { - Cmd::Move(Movement::BeginningOfLine) => { - // Move to the beginning of line. - s.edit_move_home()? - } - Cmd::Move(Movement::ViFirstPrint) => { - s.edit_move_home()?; - s.edit_move_to_next_word(At::Start, Word::Big, 1)? - } - Cmd::Move(Movement::BackwardChar(n)) => { - // Move back a character. - s.edit_move_backward(n)? - } - Cmd::ReplaceChar(n, c) => s.edit_replace_char(c, n)?, - Cmd::Replace(mvt, text) => { - s.edit_kill(&mvt)?; - if let Some(text) = text { - s.edit_insert_text(&text)? - } - } - Cmd::Overwrite(c) => { - s.edit_overwrite_char(c)?; - } - Cmd::EndOfFile => { - if !input_state.is_emacs_mode() && !s.line.is_empty() { - s.edit_move_end()?; - break; - } else if s.line.is_empty() { - return Err(error::ReadlineError::Eof); - } else { - s.edit_delete(1)? - } - } - Cmd::Move(Movement::EndOfLine) => { - // Move to the end of line. - s.edit_move_end()? - } - Cmd::Move(Movement::ForwardChar(n)) => { - // Move forward a character. - s.edit_move_forward(n)? - } - Cmd::ClearScreen => { - // Clear the screen leaving the current line at the top of the screen. - s.clear_screen()?; - s.refresh_line()? - } - Cmd::NextHistory => { - // Fetch the next command from the history list. - s.edit_history_next(false)? - } - Cmd::PreviousHistory => { - // Fetch the previous command from the history list. - s.edit_history_next(true)? - } - Cmd::HistorySearchBackward => s.edit_history_search(Direction::Reverse)?, - Cmd::HistorySearchForward => s.edit_history_search(Direction::Forward)?, - Cmd::TransposeChars => { - // Exchange the char before cursor with the character at cursor. - s.edit_transpose_chars()? - } - #[cfg(unix)] - Cmd::QuotedInsert => { - // Quoted insert - use tty::RawReader; - let c = rdr.next_char()?; - s.edit_insert(c, 1)? - } - Cmd::Yank(n, anchor) => { - // retrieve (yank) last item killed - let mut kill_ring = editor.kill_ring.lock().unwrap(); - if let Some(text) = kill_ring.yank() { - 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) - } - } - Cmd::AcceptLine => { - #[cfg(test)] - { - editor.term.cursor = s.layout.cursor.col; - } - // Accept the line regardless of where the cursor is. - if s.has_hint() || !s.is_default_prompt() { - // Force a refresh without hints to leave the previous - // line as the user typed it after a newline. - s.refresh_line_with_msg(None)?; - } - s.edit_move_end()?; - break; - } - Cmd::BeginningOfHistory => { - // move to first entry in history - s.edit_history(true)? - } - Cmd::EndOfHistory => { - // move to last entry in history - s.edit_history(false)? - } - Cmd::Move(Movement::BackwardWord(n, word_def)) => { - // move backwards one word - s.edit_move_to_prev_word(word_def, n)? - } - Cmd::CapitalizeWord => { - // capitalize word after point - s.edit_word(WordAction::CAPITALIZE)? - } - Cmd::Kill(ref mvt) => { - s.edit_kill(mvt)?; - } - Cmd::Move(Movement::ForwardWord(n, at, word_def)) => { - // move forwards one word - s.edit_move_to_next_word(at, word_def, n)? - } - Cmd::DowncaseWord => { - // lowercase word after point - s.edit_word(WordAction::LOWERCASE)? - } - Cmd::TransposeWords(n) => { - // transpose words - s.edit_transpose_words(n)? - } - Cmd::UpcaseWord => { - // uppercase word after point - s.edit_word(WordAction::UPPERCASE)? - } - Cmd::YankPop => { - // yank-pop - let mut kill_ring = editor.kill_ring.lock().unwrap(); - if let Some((yank_size, text)) = kill_ring.yank_pop() { - s.edit_yank_pop(yank_size, text)? - } - } - Cmd::Move(Movement::ViCharSearch(n, cs)) => s.edit_move_to(cs, n)?, - Cmd::Undo(n) => { - if s.changes.borrow_mut().undo(&mut s.line, n) { - s.refresh_line()?; - } - } - Cmd::Interrupt => { - return Err(error::ReadlineError::Interrupted); - } - #[cfg(unix)] - Cmd::Suspend => { - original_mode.disable_raw_mode()?; - tty::suspend()?; - editor.term.enable_raw_mode()?; // TODO original_mode may have changed - s.refresh_line()?; - continue; - } - Cmd::Noop | _ => { - // Ignore the character typed. - } + #[cfg(unix)] + if cmd == Cmd::Suspend { + original_mode.disable_raw_mode()?; + tty::suspend()?; + let _ = editor.term.enable_raw_mode()?; // TODO original_mode may have changed + s.refresh_line()?; + continue; + } + + #[cfg(unix)] + if cmd == Cmd::QuotedInsert { + // Quoted insert + use crate::tty::RawReader; + let c = rdr.next_char()?; + s.edit_insert(c, 1)?; + continue; + } + + #[cfg(windows)] + if cmd == Cmd::PasteFromClipboard { + use crate::tty::RawReader; + let clipboard = rdr.read_pasted_text()?; + s.edit_yank(&input_state, &clipboard[..], Anchor::Before, 1)?; + } + + // Tiny test quirk + #[cfg(test)] + if matches!( + cmd, + Cmd::AcceptLine | Cmd::Newline | Cmd::AcceptOrInsertLine { .. } + ) { + editor.term.cursor = s.layout.cursor.col; + } + + // Execute things can be done solely on a state object + match command::execute(cmd, &mut s, &input_state, &editor.kill_ring, &editor.config)? { + command::Status::Proceed => continue, + command::Status::Submit => break, } } + + // Move to end, in case cursor was in the middle of the line, so that + // next thing application prints goes after the input + s.edit_move_buffer_end()?; + if cfg!(windows) { let _ = original_mode; // silent warning } @@ -621,9 +567,9 @@ fn readline_raw( initial: Option<(&str, &str)>, editor: &mut Editor, ) -> Result { - let original_mode = editor.term.enable_raw_mode()?; + let (original_mode, term_key_map) = editor.term.enable_raw_mode()?; let guard = Guard(&original_mode); - let user_input = readline_edit(prompt, initial, editor, &original_mode); + let user_input = readline_edit(prompt, initial, editor, &original_mode, term_key_map); if editor.config.auto_add_history() { if let Ok(ref line) = user_input { editor.add_history_entry(line.as_str()); @@ -637,12 +583,94 @@ fn readline_raw( user_input } -fn readline_direct() -> Result { - let mut line = String::new(); - if io::stdin().read_line(&mut line)? > 0 { - Ok(line) - } else { - Err(error::ReadlineError::Eof) +// Helper to handle backspace characters in a direct input +fn apply_backspace_direct(input: &str) -> String { + // Setup the output buffer + // No '\b' in the input in the common case, so set the capacity to the input + // length + let mut out = String::with_capacity(input.len()); + + // Keep track of the size of each grapheme from the input + // As many graphemes as input bytes in the common case + let mut grapheme_sizes: Vec = Vec::with_capacity(input.len()); + + for g in unicode_segmentation::UnicodeSegmentation::graphemes(input, true) { + match g { + // backspace char + "\u{0008}" => { + if let Some(n) = grapheme_sizes.pop() { + // Remove the last grapheme + out.truncate(out.len() - n as usize); + } + } + _ => { + out.push_str(g); + grapheme_sizes.push(g.len() as u8); + } + } + } + + out +} + +fn readline_direct( + mut reader: impl BufRead, + mut writer: impl Write, + validator: &Option, +) -> Result { + let mut input = String::new(); + + loop { + match reader.read_line(&mut input)? { + 0 => return Err(error::ReadlineError::Eof), + _ => { + // Remove trailing newline + let trailing_n = input.ends_with('\n'); + let trailing_r; + + if trailing_n { + input.pop(); + trailing_r = input.ends_with('\r'); + if trailing_r { + input.pop(); + } + } else { + trailing_r = false; + } + + input = apply_backspace_direct(&input); + + match validator.as_ref() { + None => return Ok(input), + Some(v) => { + let mut ctx = input.as_str(); + let mut ctx = validate::ValidationContext::new(&mut ctx); + + match v.validate(&mut ctx)? { + validate::ValidationResult::Valid(msg) => { + if let Some(msg) = msg { + writer.write_all(msg.as_bytes())?; + } + return Ok(input); + } + validate::ValidationResult::Invalid(Some(msg)) => { + writer.write_all(msg.as_bytes())?; + } + validate::ValidationResult::Incomplete => { + // Add newline and keep on taking input + if trailing_r { + input.push('\r'); + } + if trailing_n { + input.push('\n'); + } + } + _ => {} + } + } + } + } + } } } @@ -652,7 +680,7 @@ fn readline_direct() -> Result { /// (parse current line once) pub trait Helper where - Self: Completer + Hinter + Highlighter, + Self: Completer + Hinter + Highlighter + Validator, { } @@ -677,7 +705,7 @@ impl<'h> Context<'h> { /// Return an immutable reference to the history object. pub fn history(&self) -> &History { - &self.history + self.history } /// The history index we are currently editing @@ -693,7 +721,7 @@ pub struct Editor { helper: Option, kill_ring: Arc>, config: Config, - custom_bindings: Arc>>, + custom_bindings: Arc>>, } #[allow(clippy::new_without_default)] @@ -710,6 +738,7 @@ impl Editor { config.output_stream(), config.tab_stop(), config.bell_style(), + config.enable_bracketed_paste(), ); Self { term, @@ -717,7 +746,7 @@ impl Editor { helper: None, kill_ring: Arc::new(Mutex::new(KillRing::new(60))), config, - custom_bindings: Arc::new(RwLock::new(HashMap::new())), + custom_bindings: Arc::new(RwLock::new(Trie::new())), } } @@ -750,13 +779,13 @@ impl Editor { stdout.write_all(prompt.as_bytes())?; stdout.flush()?; - readline_direct() + readline_direct(io::stdin().lock(), io::stderr(), &self.helper) } else if self.term.is_stdin_tty() { readline_raw(prompt, initial, self) } else { debug!(target: "rustyline", "stdin is not a tty"); // Not a tty: read from file / pipe. - readline_direct() + readline_direct(io::stdin().lock(), io::stderr(), &self.helper) } } @@ -766,10 +795,15 @@ impl Editor { } /// Save the history in the specified file. - pub fn save_history + ?Sized>(&self, path: &P) -> Result<()> { + pub fn save_history + ?Sized>(&mut self, path: &P) -> Result<()> { self.history.save(path) } + /// Append new entries in the specified file. + pub fn append_history + ?Sized>(&mut self, path: &P) -> Result<()> { + self.history.append(path) + } + /// Add a new entry in the history. pub fn add_history_entry + Into>(&mut self, line: S) -> bool { self.history.add(line) @@ -807,23 +841,28 @@ impl Editor { } /// Bind a sequence to a command. - pub fn bind_sequence(&mut self, key_seq: KeyPress, cmd: Cmd) -> Option { + pub fn bind_sequence, R: Into>( + &mut self, + key_seq: E, + handler: R, + ) -> Option { if let Ok(mut bindings) = self.custom_bindings.write() { - bindings.insert(key_seq, cmd) + bindings.insert(Event::normalize(key_seq.into()), handler.into()) } else { None } } /// Remove a binding for the given sequence. - pub fn unbind_sequence(&mut self, key_seq: KeyPress) -> Option { + pub fn unbind_sequence>(&mut self, key_seq: E) -> Option { if let Ok(mut bindings) = self.custom_bindings.write() { - bindings.remove(&key_seq) + bindings.remove(&Event::normalize(key_seq.into())) } else { None } } + /// Returns an iterator over edited lines /// ``` /// let mut rl = rustyline::Editor::<()>::new(); /// for readline in rl.iter("> ") { @@ -838,7 +877,7 @@ impl Editor { /// } /// } /// ``` - pub fn iter<'a>(&'a mut self, prompt: &'a str) -> Iter<'_, H> { + pub fn iter<'a>(&'a mut self, prompt: &'a str) -> impl Iterator> + 'a { Iter { editor: self, prompt, @@ -897,8 +936,7 @@ impl fmt::Debug for Editor { } } -/// Edited lines iterator -pub struct Iter<'a, H: Helper> { +struct Iter<'a, H: Helper> { editor: &'a mut Editor, prompt: &'a str, } @@ -921,3 +959,6 @@ impl<'a, H: Helper> Iterator for Iter<'a, H> { extern crate assert_matches; #[cfg(test)] mod test; + +#[cfg(doctest)] +doc_comment::doctest!("../README.md"); diff --git a/src/line_buffer.rs b/src/line_buffer.rs index 1536c7b292..f14abba368 100644 --- a/src/line_buffer.rs +++ b/src/line_buffer.rs @@ -1,6 +1,7 @@ //! Line buffer with current cursor position use crate::keymap::{At, CharSearch, Movement, RepeatCount, Word}; use std::cell::RefCell; +use std::cmp::min; use std::fmt; use std::iter; use std::ops::{Deref, Index, Range}; @@ -11,13 +12,17 @@ use unicode_segmentation::UnicodeSegmentation; /// Default maximum buffer size for the line read pub(crate) const MAX_LINE: usize = 4096; +pub(crate) const INDENT: &str = " "; /// Word's case change #[derive(Clone, Copy)] pub enum WordAction { - CAPITALIZE, - LOWERCASE, - UPPERCASE, + /// Capitalize word + Capitalize, + /// lowercase word + Lowercase, + /// uppercase word + Uppercase, } /// Delete (kill) direction @@ -47,6 +52,8 @@ pub(crate) trait ChangeListener: DeleteListener { fn replace(&mut self, idx: usize, old: &str, new: &str); } +// TODO split / cache lines ? + /// Represent the current input (text and cursor position). /// /// The methods do text manipulations or/and cursor movements. @@ -160,6 +167,23 @@ impl LineBuffer { } } + fn end_of_line(&self) -> usize { + if let Some(n) = self.buf[self.pos..].find('\n') { + n + self.pos + } else { + self.buf.len() + } + } + + fn start_of_line(&self) -> usize { + if let Some(i) = self.buf[..self.pos].rfind('\n') { + // `i` is before the new line, e.g. at the end of the previous one. + i + 1 + } else { + 0 + } + } + /// Returns the character at current cursor position. pub(crate) fn grapheme_at_cursor(&self) -> Option<&str> { if self.pos == self.buf.len() { @@ -236,7 +260,7 @@ impl LineBuffer { if n == 1 { self.insert_str(pos, text); } else { - let text = iter::repeat(text).take(n).collect::(); + let text = text.repeat(n); self.insert_str(pos, &text); } self.pos += shift; @@ -274,8 +298,8 @@ impl LineBuffer { } } - /// Move cursor to the start of the line. - pub fn move_home(&mut self) -> bool { + /// Move cursor to the start of the buffer. + pub fn move_buffer_start(&mut self) -> bool { if self.pos > 0 { self.pos = 0; true @@ -284,8 +308,8 @@ impl LineBuffer { } } - /// Move cursor to the end of the line. - pub fn move_end(&mut self) -> bool { + /// Move cursor to the end of the buffer. + pub fn move_buffer_end(&mut self) -> bool { if self.pos == self.buf.len() { false } else { @@ -294,6 +318,33 @@ impl LineBuffer { } } + /// Move cursor to the start of the line. + pub fn move_home(&mut self) -> bool { + let start = self.start_of_line(); + if self.pos > start { + self.pos = start; + true + } else { + false + } + } + + /// Move cursor to the end of the line. + pub fn move_end(&mut self) -> bool { + let end = self.end_of_line(); + if self.pos == end { + false + } else { + self.pos = end; + true + } + } + + /// Is cursor at the end of input (whitespaces after cursor is discarded) + pub fn is_end_of_input(&self) -> bool { + self.pos >= self.buf.trim_end().len() + } + /// Delete the character at the right of the cursor without altering the /// cursor position. Basically this is what happens with the "Delete" /// keyboard key. @@ -327,6 +378,22 @@ impl LineBuffer { /// Kill the text from point to the end of the line. pub fn kill_line(&mut self) -> bool { + if !self.buf.is_empty() && self.pos < self.buf.len() { + let start = self.pos; + let end = self.end_of_line(); + if start == end { + self.delete(1); + } else { + self.drain(start..end, Direction::Forward); + } + true + } else { + false + } + } + + /// Kill the text from point to the end of the buffer. + pub fn kill_buffer(&mut self) -> bool { if !self.buf.is_empty() && self.pos < self.buf.len() { let start = self.pos; let end = self.buf.len(); @@ -339,6 +406,23 @@ impl LineBuffer { /// Kill backward from point to the beginning of the line. pub fn discard_line(&mut self) -> bool { + if self.pos > 0 && !self.buf.is_empty() { + let start = self.start_of_line(); + let end = self.pos; + if end == start { + self.backspace(1) + } else { + self.drain(start..end, Direction::Backward); + self.pos = start; + true + } + } else { + false + } + } + + /// Kill backward from point to the beginning of the buffer. + pub fn discard_buffer(&mut self) -> bool { if self.pos > 0 && !self.buf.is_empty() { let end = self.pos; self.drain(0..end, Direction::Backward); @@ -480,6 +564,110 @@ impl LineBuffer { } } + /// Moves the cursor to the same column in the line above + pub fn move_to_line_up(&mut self, n: RepeatCount) -> bool { + match self.buf[..self.pos].rfind('\n') { + Some(off) => { + let column = self.buf[off + 1..self.pos].graphemes(true).count(); + + let mut dest_start = self.buf[..off].rfind('\n').map(|n| n + 1).unwrap_or(0); + let mut dest_end = off; + for _ in 1..n { + if dest_start == 0 { + break; + } + dest_end = dest_start - 1; + dest_start = self.buf[..dest_end].rfind('\n').map(|n| n + 1).unwrap_or(0); + } + let gidx = self.buf[dest_start..dest_end] + .grapheme_indices(true) + .nth(column); + + self.pos = gidx.map(|(idx, _)| dest_start + idx).unwrap_or(off); // if there's no enough columns + true + } + None => false, + } + } + + /// N lines up starting from the current one + /// + /// Fails if the cursor is on the first line + fn n_lines_up(&self, n: RepeatCount) -> Option<(usize, usize)> { + let mut start = if let Some(off) = self.buf[..self.pos].rfind('\n') { + off + 1 + } else { + return None; + }; + let end = self.buf[self.pos..] + .find('\n') + .map(|x| self.pos + x + 1) + .unwrap_or_else(|| self.buf.len()); + for _ in 0..n { + if let Some(off) = self.buf[..start - 1].rfind('\n') { + start = off + 1 + } else { + start = 0; + break; + } + } + Some((start, end)) + } + + /// N lines down starting from the current one + /// + /// Fails if the cursor is on the last line + fn n_lines_down(&self, n: RepeatCount) -> Option<(usize, usize)> { + let mut end = if let Some(off) = self.buf[self.pos..].find('\n') { + self.pos + off + 1 + } else { + return None; + }; + let start = self.buf[..self.pos].rfind('\n').unwrap_or(0); + for _ in 0..n { + if let Some(off) = self.buf[end..].find('\n') { + end = end + off + 1 + } else { + end = self.buf.len(); + break; + }; + } + Some((start, end)) + } + + /// Moves the cursor to the same column in the line above + pub fn move_to_line_down(&mut self, n: RepeatCount) -> bool { + match self.buf[self.pos..].find('\n') { + Some(off) => { + let line_start = self.buf[..self.pos].rfind('\n').map(|n| n + 1).unwrap_or(0); + let column = self.buf[line_start..self.pos].graphemes(true).count(); + let mut dest_start = self.pos + off + 1; + let mut dest_end = self.buf[dest_start..] + .find('\n') + .map(|v| dest_start + v) + .unwrap_or_else(|| self.buf.len()); + for _ in 1..n { + if dest_end == self.buf.len() { + break; + } + dest_start = dest_end + 1; + dest_end = self.buf[dest_start..] + .find('\n') + .map(|v| dest_start + v) + .unwrap_or_else(|| self.buf.len()); + } + self.pos = self.buf[dest_start..dest_end] + .grapheme_indices(true) + .nth(column) + .map(|(idx, _)| dest_start + idx) + .unwrap_or(dest_end); // if there's no enough columns + debug_assert!(self.pos <= self.buf.len()); + true + } + None => false, + } + } + fn search_char_pos(&self, cs: CharSearch, n: RepeatCount) -> Option { let mut shift = 0; let search_result = match cs { @@ -508,23 +696,19 @@ 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() - } - }) - } else { - None - } + search_result.map(|pos| 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() + } + }) } /// Move cursor to the matching character position. @@ -550,6 +734,7 @@ impl LineBuffer { } } + /// Delete range specified by `cs` search. 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), @@ -605,13 +790,13 @@ impl LineBuffer { .drain(start..end, Direction::default()) .collect::(); let result = match a { - WordAction::CAPITALIZE => { + WordAction::Capitalize => { let ch = (&word).graphemes(true).next().unwrap(); let cap = ch.to_uppercase(); cap + &word[ch.len()..].to_lowercase() } - WordAction::LOWERCASE => word.to_lowercase(), - WordAction::UPPERCASE => word.to_uppercase(), + WordAction::Lowercase => word.to_lowercase(), + WordAction::Uppercase => word.to_uppercase(), }; self.insert_str(start, &result); self.pos = start + result.len(); @@ -716,71 +901,95 @@ impl LineBuffer { return None; } match *mvt { - Movement::WholeLine => Some(self.buf.clone()), + Movement::WholeLine => { + let start = self.start_of_line(); + let end = self.end_of_line(); + if start == end { + None + } else { + Some(self.buf[start..self.pos].to_owned()) + } + } Movement::BeginningOfLine => { - if self.pos == 0 { + let start = self.start_of_line(); + if self.pos == start { None } else { - Some(self.buf[..self.pos].to_owned()) + Some(self.buf[start..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 + self.next_word_pos(0, At::Start, Word::Big, 1) + .map(|pos| self.buf[pos..self.pos].to_owned()) } } Movement::EndOfLine => { + let end = self.end_of_line(); + if self.pos == end { + None + } else { + Some(self.buf[self.pos..end].to_owned()) + } + } + Movement::EndOfBuffer => { 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_owned()) - } else { + Movement::WholeBuffer => { + if self.buf.is_empty() { None + } else { + Some(self.buf.clone()) } } - 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_owned()) - } else { + Movement::BeginningOfBuffer => { + if self.pos == 0 { None + } else { + Some(self.buf[..self.pos].to_owned()) } } + Movement::BackwardWord(n, word_def) => self + .prev_word_pos(self.pos, word_def, n) + .map(|pos| self.buf[pos..self.pos].to_owned()), + Movement::ForwardWord(n, at, word_def) => self + .next_word_pos(self.pos, at, word_def, n) + .map(|pos| self.buf[self.pos..pos].to_owned()), 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), }; - if let Some(pos) = search_result { - Some(match cs { - 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 - } + search_result.map(|pos| match cs { + 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(), + }) } - Movement::BackwardChar(n) => { - if let Some(pos) = self.prev_pos(n) { - Some(self.buf[pos..self.pos].to_owned()) + Movement::BackwardChar(n) => self + .prev_pos(n) + .map(|pos| self.buf[pos..self.pos].to_owned()), + Movement::ForwardChar(n) => self + .next_pos(n) + .map(|pos| self.buf[self.pos..pos].to_owned()), + Movement::LineUp(n) => { + if let Some((start, end)) = self.n_lines_up(n) { + Some(self.buf[start..end].to_owned()) } else { None } } - Movement::ForwardChar(n) => { - if let Some(pos) = self.next_pos(n) { - Some(self.buf[self.pos..pos].to_owned()) + Movement::LineDown(n) => { + if let Some((start, end)) = self.n_lines_down(n) { + Some(self.buf[start..end].to_owned()) } else { None } @@ -788,11 +997,9 @@ impl LineBuffer { } } + /// Kill range specified by `mvt`. pub fn kill(&mut self, mvt: &Movement) -> bool { - let notify = match *mvt { - Movement::ForwardChar(_) | Movement::BackwardChar(_) => false, - _ => true, - }; + let notify = !matches!(*mvt, Movement::ForwardChar(_) | Movement::BackwardChar(_)); if notify { if let Some(dl) = self.dl.as_ref() { let mut dl = dl.lock().unwrap(); @@ -829,9 +1036,37 @@ impl LineBuffer { self.delete_word(at, word_def, n) } Movement::ViCharSearch(n, cs) => self.delete_to(cs, n), + Movement::LineUp(n) => { + if let Some((start, end)) = self.n_lines_up(n) { + self.delete_range(start..end); + true + } else { + false + } + } + Movement::LineDown(n) => { + if let Some((start, end)) = self.n_lines_down(n) { + self.delete_range(start..end); + true + } else { + false + } + } Movement::ViFirstPrint => { false // TODO } + Movement::EndOfBuffer => { + // Kill the text from point to the end of the buffer. + self.kill_buffer() + } + Movement::BeginningOfBuffer => { + // Kill backward from point to the beginning of the buffer. + self.discard_buffer() + } + Movement::WholeBuffer => { + self.move_buffer_start(); + self.kill_buffer() + } }; if notify { if let Some(dl) = self.dl.as_ref() { @@ -841,6 +1076,68 @@ impl LineBuffer { } killed } + + /// Indent range specified by `mvt`. + pub fn indent(&mut self, mvt: &Movement, amount: usize, dedent: bool) -> bool { + let pair = match *mvt { + // All inline operators are the same: indent current line + Movement::WholeLine + | Movement::BeginningOfLine + | Movement::ViFirstPrint + | Movement::EndOfLine + | Movement::BackwardChar(..) + | Movement::ForwardChar(..) + | Movement::ViCharSearch(..) => Some((self.pos, self.pos)), + Movement::EndOfBuffer => Some((self.pos, self.buf.len())), + Movement::WholeBuffer => Some((0, self.buf.len())), + Movement::BeginningOfBuffer => Some((0, self.pos)), + Movement::BackwardWord(n, word_def) => self + .prev_word_pos(self.pos, word_def, n) + .map(|pos| (pos, self.pos)), + Movement::ForwardWord(n, at, word_def) => self + .next_word_pos(self.pos, at, word_def, n) + .map(|pos| (self.pos, pos)), + Movement::LineUp(n) => self.n_lines_up(n), + Movement::LineDown(n) => self.n_lines_down(n), + }; + let (start, end) = pair.unwrap_or((self.pos, self.pos)); + let start = self.buf[..start] + .rfind('\n') + .map(|pos| pos + 1) + .unwrap_or(0); + let end = self.buf[end..] + .rfind('\n') + .map(|pos| end + pos) + .unwrap_or_else(|| self.buf.len()); + let mut index = start; + if dedent { + for line in self.buf[start..end].to_string().split('\n') { + let max = line.len() - line.trim_start().len(); + let deleting = min(max, amount); + self.drain(index..index + deleting, Default::default()); + if self.pos >= index { + if self.pos.saturating_sub(index) < deleting { + // don't wrap into the previous line + self.pos = index; + } else { + self.pos -= deleting; + } + } + index += line.len() + 1 - deleting; + } + } else { + for line in self.buf[start..end].to_string().split('\n') { + for off in (0..amount).step_by(INDENT.len()) { + self.insert_str(index, &INDENT[..min(amount - off, INDENT.len())]); + } + if self.pos >= index { + self.pos += amount; + } + index += amount + line.len() + 1; + } + } + true + } } impl Deref for LineBuffer { @@ -944,18 +1241,18 @@ mod test { let push = s.insert('α', 1).unwrap(); assert_eq!("α", s.buf); assert_eq!(2, s.pos); - assert_eq!(true, push); + assert!(push); let push = s.insert('ß', 1).unwrap(); assert_eq!("αß", s.buf); assert_eq!(4, s.pos); - assert_eq!(true, push); + assert!(push); s.pos = 0; let push = s.insert('γ', 1).unwrap(); assert_eq!("γαß", s.buf); assert_eq!(2, s.pos); - assert_eq!(false, push); + assert!(!push); } #[test] @@ -983,22 +1280,72 @@ mod test { let ok = s.move_backward(1); assert_eq!("αß", s.buf); assert_eq!(2, s.pos); - assert_eq!(true, ok); + assert!(ok); let ok = s.move_forward(1); assert_eq!("αß", s.buf); assert_eq!(4, s.pos); - assert_eq!(true, ok); + assert!(ok); let ok = s.move_home(); assert_eq!("αß", s.buf); assert_eq!(0, s.pos); - assert_eq!(true, ok); + assert!(ok); let ok = s.move_end(); assert_eq!("αß", s.buf); assert_eq!(4, s.pos); - assert_eq!(true, ok); + assert!(ok); + } + + #[test] + fn move_home_end_multiline() { + let text = "αa\nsdf ßc\nasdf"; + let mut s = LineBuffer::init(text, 7, None); + let ok = s.move_home(); + assert_eq!(text, s.buf); + assert_eq!(4, s.pos); + assert!(ok); + + let ok = s.move_home(); + assert_eq!(text, s.buf); + assert_eq!(4, s.pos); + assert!(!ok); + + let ok = s.move_end(); + assert_eq!(text, s.buf); + assert_eq!(11, s.pos); + assert!(ok); + + let ok = s.move_end(); + assert_eq!(text, s.buf); + assert_eq!(11, s.pos); + assert!(!ok); + } + + #[test] + fn move_buffer_multiline() { + let text = "αa\nsdf ßc\nasdf"; + let mut s = LineBuffer::init(text, 7, None); + let ok = s.move_buffer_start(); + assert_eq!(text, s.buf); + assert_eq!(0, s.pos); + assert!(ok); + + let ok = s.move_buffer_start(); + assert_eq!(text, s.buf); + assert_eq!(0, s.pos); + assert!(!ok); + + let ok = s.move_buffer_end(); + assert_eq!(text, s.buf); + assert_eq!(text.len(), s.pos); + assert!(ok); + + let ok = s.move_buffer_end(); + assert_eq!(text, s.buf); + assert_eq!(text.len(), s.pos); + assert!(!ok); } #[test] @@ -1006,11 +1353,11 @@ mod test { let mut s = LineBuffer::init("ag̈", 4, None); assert_eq!(4, s.len()); let ok = s.move_backward(1); - assert_eq!(true, ok); + assert!(ok); assert_eq!(1, s.pos); let ok = s.move_forward(1); - assert_eq!(true, ok); + assert!(ok); assert_eq!(4, s.pos); } @@ -1026,7 +1373,7 @@ mod test { let ok = s.backspace(1); assert_eq!("", s.buf); assert_eq!(0, s.pos); - assert_eq!(true, ok); + assert!(ok); cl.borrow().assert_deleted_str_eq("α"); } @@ -1037,15 +1384,71 @@ mod test { let ok = s.kill_line(); assert_eq!("αßγ", s.buf); assert_eq!(6, s.pos); - assert_eq!(true, ok); + assert!(ok); cl.borrow().assert_deleted_str_eq("δε"); s.pos = 4; let ok = s.discard_line(); assert_eq!("γ", s.buf); assert_eq!(0, s.pos); - assert_eq!(true, ok); + assert!(ok); + cl.borrow().assert_deleted_str_eq("αß"); + } + + #[test] + fn kill_multiline() { + let cl = Listener::new(); + let mut s = LineBuffer::init("αß\nγδ 12\nε f4", 7, Some(cl.clone())); + + let ok = s.kill_line(); + assert_eq!("αß\nγ\nε f4", s.buf); + assert_eq!(7, s.pos); + assert!(ok); + cl.borrow().assert_deleted_str_eq("δ 12"); + + let ok = s.kill_line(); + assert_eq!("αß\nγε f4", s.buf); + assert_eq!(7, s.pos); + assert!(ok); + cl.borrow().assert_deleted_str_eq("\n"); + + let ok = s.kill_line(); + assert_eq!("αß\nγ", s.buf); + assert_eq!(7, s.pos); + assert!(ok); + cl.borrow().assert_deleted_str_eq("ε f4"); + + let ok = s.kill_line(); + assert_eq!(7, s.pos); + assert!(!ok); + } + + #[test] + fn discard_multiline() { + let cl = Listener::new(); + let mut s = LineBuffer::init("αß\nc γδε", 9, Some(cl.clone())); + + let ok = s.discard_line(); + assert_eq!("αß\nδε", s.buf); + assert_eq!(5, s.pos); + assert!(ok); + cl.borrow().assert_deleted_str_eq("c γ"); + + let ok = s.discard_line(); + assert_eq!("αßδε", s.buf); + assert_eq!(4, s.pos); + assert!(ok); + cl.borrow().assert_deleted_str_eq("\n"); + + let ok = s.discard_line(); + assert_eq!("δε", s.buf); + assert_eq!(0, s.pos); + assert!(ok); cl.borrow().assert_deleted_str_eq("αß"); + + let ok = s.discard_line(); + assert_eq!(0, s.pos); + assert!(!ok); } #[test] @@ -1054,21 +1457,21 @@ mod test { let ok = s.transpose_chars(); assert_eq!("ßac", s.buf); assert_eq!(3, s.pos); - assert_eq!(true, ok); + assert!(ok); s.buf = String::from("aßc"); s.pos = 3; let ok = s.transpose_chars(); assert_eq!("acß", s.buf); assert_eq!(4, s.pos); - assert_eq!(true, ok); + assert!(ok); s.buf = String::from("aßc"); s.pos = 4; let ok = s.transpose_chars(); assert_eq!("acß", s.buf); assert_eq!(4, s.pos); - assert_eq!(true, ok); + assert!(ok); } #[test] @@ -1077,16 +1480,16 @@ mod test { let ok = s.move_to_prev_word(Word::Emacs, 1); assert_eq!("a ß c", s.buf); assert_eq!(2, s.pos); // before 'ß' - assert!(true, ok); + assert!(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!(ok); assert_eq!(6, s.pos); // before 'c' let ok = s.move_to_prev_word(Word::Emacs, 2); - assert!(true, ok); + assert!(ok); assert_eq!(0, s.pos); } @@ -1138,17 +1541,17 @@ mod test { fn move_to_forward() { let mut s = LineBuffer::init("αßγδε", 2, None); let ok = s.move_to(CharSearch::ForwardBefore('ε'), 1); - assert_eq!(true, ok); + assert!(ok); assert_eq!(6, s.pos); let mut s = LineBuffer::init("αßγδε", 2, None); let ok = s.move_to(CharSearch::Forward('ε'), 1); - assert_eq!(true, ok); + assert!(ok); assert_eq!(8, s.pos); let mut s = LineBuffer::init("αßγδε", 2, None); let ok = s.move_to(CharSearch::Forward('ε'), 10); - assert_eq!(true, ok); + assert!(ok); assert_eq!(8, s.pos); } @@ -1156,12 +1559,12 @@ mod test { fn move_to_backward() { let mut s = LineBuffer::init("αßγδε", 8, None); let ok = s.move_to(CharSearch::BackwardAfter('ß'), 1); - assert_eq!(true, ok); + assert!(ok); assert_eq!(4, s.pos); let mut s = LineBuffer::init("αßγδε", 8, None); let ok = s.move_to(CharSearch::Backward('ß'), 1); - assert_eq!(true, ok); + assert!(ok); assert_eq!(2, s.pos); } @@ -1172,7 +1575,7 @@ mod test { let ok = s.delete_prev_word(Word::Big, 1); assert_eq!("a c", s.buf); assert_eq!(2, s.pos); - assert_eq!(true, ok); + assert!(ok); cl.borrow().assert_deleted_str_eq("ß "); } @@ -1181,20 +1584,20 @@ mod test { 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!(true, ok); + assert!(ok); assert_eq!(4, s.pos); // after 'ß' let ok = s.move_to_next_word(At::AfterEnd, Word::Emacs, 1); - assert_eq!(true, ok); + assert!(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!(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!(ok); assert_eq!(7, s.pos); // after 'c' } @@ -1204,7 +1607,7 @@ mod test { let ok = s.move_to_next_word(At::BeforeEnd, Word::Vi, 1); assert_eq!("a ßeta c", s.buf); assert_eq!(6, s.pos); - assert_eq!(true, ok); + assert!(ok); } #[test] @@ -1257,7 +1660,7 @@ mod test { let ok = s.move_to_next_word(At::Start, Word::Emacs, 1); assert_eq!("a ß c", s.buf); assert_eq!(6, s.pos); - assert_eq!(true, ok); + assert!(ok); } #[test] @@ -1311,14 +1714,14 @@ mod test { let ok = s.delete_word(At::AfterEnd, Word::Emacs, 1); assert_eq!("a c", s.buf); assert_eq!(1, s.pos); - assert_eq!(true, ok); + assert!(ok); cl.borrow().assert_deleted_str_eq(" ß"); 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!(true, ok); + assert!(ok); cl.borrow().assert_deleted_str_eq("test"); } @@ -1329,7 +1732,7 @@ mod test { let ok = s.delete_word(At::Start, Word::Emacs, 1); assert_eq!("a c", s.buf); assert_eq!(2, s.pos); - assert_eq!(true, ok); + assert!(ok); cl.borrow().assert_deleted_str_eq("ß "); } @@ -1338,14 +1741,14 @@ mod test { 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); + assert!(ok); cl.borrow().assert_deleted_str_eq("ßγδ"); assert_eq!("αε", s.buf); assert_eq!(2, s.pos); let mut s = LineBuffer::init("αßγδε", 2, Some(cl.clone())); let ok = s.delete_to(CharSearch::Forward('ε'), 1); - assert_eq!(true, ok); + assert!(ok); cl.borrow().assert_deleted_str_eq("ßγδε"); assert_eq!("α", s.buf); assert_eq!(2, s.pos); @@ -1356,14 +1759,14 @@ mod test { 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); + assert!(ok); cl.borrow().assert_deleted_str_eq("ßγδ"); assert_eq!("αε", s.buf); assert_eq!(2, s.pos); let mut s = LineBuffer::init("αßγδε", 8, Some(cl.clone())); let ok = s.delete_to(CharSearch::Backward('ß'), 1); - assert_eq!(true, ok); + assert!(ok); cl.borrow().assert_deleted_str_eq("ßγδ"); assert_eq!("αε", s.buf); assert_eq!(2, s.pos); @@ -1372,22 +1775,22 @@ mod test { #[test] fn edit_word() { let mut s = LineBuffer::init("a ßeta c", 1, None); - assert!(s.edit_word(WordAction::UPPERCASE)); + 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, None); - assert!(s.edit_word(WordAction::LOWERCASE)); + 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, None); - assert!(s.edit_word(WordAction::CAPITALIZE)); + 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, None); - assert!(s.edit_word(WordAction::CAPITALIZE)); + assert!(s.edit_word(WordAction::Capitalize)); assert_eq!("tEst", s.buf); assert_eq!(4, s.pos); } @@ -1410,4 +1813,44 @@ mod test { let mut s = LineBuffer::init("ßeta / __", 9, None); assert!(!s.transpose_words(1)); } + + #[test] + fn move_by_line() { + let text = "aa123\nsdf bc\nasdf"; + let mut s = LineBuffer::init(text, 14, None); + // move up + let ok = s.move_to_line_up(1); + assert_eq!(7, s.pos); + assert!(ok); + + let ok = s.move_to_line_up(1); + assert_eq!(1, s.pos); + assert!(ok); + + let ok = s.move_to_line_up(1); + assert_eq!(1, s.pos); + assert!(!ok); + + // move down + let ok = s.move_to_line_down(1); + assert_eq!(7, s.pos); + assert!(ok); + + let ok = s.move_to_line_down(1); + assert_eq!(14, s.pos); + assert!(ok); + + let ok = s.move_to_line_down(1); + assert_eq!(14, s.pos); + assert!(!ok); + + // move by multiple steps + let ok = s.move_to_line_up(2); + assert_eq!(1, s.pos); + assert!(ok); + + let ok = s.move_to_line_down(2); + assert_eq!(14, s.pos); + assert!(ok); + } } diff --git a/src/test/common.rs b/src/test/common.rs index 798d61904c..0f31dcefa0 100644 --- a/src/test/common.rs +++ b/src/test/common.rs @@ -2,21 +2,16 @@ use super::{assert_cursor, assert_line, assert_line_with_initial, init_editor}; use crate::config::EditMode; use crate::error::ReadlineError; -use crate::keys::KeyPress; +use crate::keys::{KeyCode as K, KeyEvent as E, Modifiers as M}; #[test] fn home_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { - assert_cursor( - *mode, - ("", ""), - &[KeyPress::Home, KeyPress::Enter], - ("", ""), - ); + assert_cursor(*mode, ("", ""), &[E(K::Home, M::NONE), E::ENTER], ("", "")); assert_cursor( *mode, ("Hi", ""), - &[KeyPress::Home, KeyPress::Enter], + &[E(K::Home, M::NONE), E::ENTER], ("", "Hi"), ); if *mode == EditMode::Vi { @@ -24,7 +19,7 @@ fn home_key() { assert_cursor( *mode, ("Hi", ""), - &[KeyPress::Esc, KeyPress::Home, KeyPress::Enter], + &[E::ESC, E(K::Home, M::NONE), E::ENTER], ("", "Hi"), ); } @@ -34,17 +29,17 @@ fn home_key() { #[test] fn end_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { - assert_cursor(*mode, ("", ""), &[KeyPress::End, KeyPress::Enter], ("", "")); + assert_cursor(*mode, ("", ""), &[E(K::End, M::NONE), E::ENTER], ("", "")); assert_cursor( *mode, ("H", "i"), - &[KeyPress::End, KeyPress::Enter], + &[E(K::End, M::NONE), E::ENTER], ("Hi", ""), ); assert_cursor( *mode, ("", "Hi"), - &[KeyPress::End, KeyPress::Enter], + &[E(K::End, M::NONE), E::ENTER], ("Hi", ""), ); if *mode == EditMode::Vi { @@ -52,7 +47,7 @@ fn end_key() { assert_cursor( *mode, ("", "Hi"), - &[KeyPress::Esc, KeyPress::End, KeyPress::Enter], + &[E::ESC, E(K::End, M::NONE), E::ENTER], ("Hi", ""), ); } @@ -65,19 +60,19 @@ fn left_key() { assert_cursor( *mode, ("Hi", ""), - &[KeyPress::Left, KeyPress::Enter], + &[E(K::Left, M::NONE), E::ENTER], ("H", "i"), ); assert_cursor( *mode, ("H", "i"), - &[KeyPress::Left, KeyPress::Enter], + &[E(K::Left, M::NONE), E::ENTER], ("", "Hi"), ); assert_cursor( *mode, ("", "Hi"), - &[KeyPress::Left, KeyPress::Enter], + &[E(K::Left, M::NONE), E::ENTER], ("", "Hi"), ); if *mode == EditMode::Vi { @@ -85,7 +80,7 @@ fn left_key() { assert_cursor( *mode, ("Bye", ""), - &[KeyPress::Esc, KeyPress::Left, KeyPress::Enter], + &[E::ESC, E(K::Left, M::NONE), E::ENTER], ("B", "ye"), ); } @@ -95,28 +90,23 @@ fn left_key() { #[test] fn right_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { - assert_cursor( - *mode, - ("", ""), - &[KeyPress::Right, KeyPress::Enter], - ("", ""), - ); + assert_cursor(*mode, ("", ""), &[E(K::Right, M::NONE), E::ENTER], ("", "")); assert_cursor( *mode, ("", "Hi"), - &[KeyPress::Right, KeyPress::Enter], + &[E(K::Right, M::NONE), E::ENTER], ("H", "i"), ); assert_cursor( *mode, ("B", "ye"), - &[KeyPress::Right, KeyPress::Enter], + &[E(K::Right, M::NONE), E::ENTER], ("By", "e"), ); assert_cursor( *mode, ("H", "i"), - &[KeyPress::Right, KeyPress::Enter], + &[E(K::Right, M::NONE), E::ENTER], ("Hi", ""), ); if *mode == EditMode::Vi { @@ -124,7 +114,7 @@ fn right_key() { assert_cursor( *mode, ("", "Hi"), - &[KeyPress::Esc, KeyPress::Right, KeyPress::Enter], + &[E::ESC, E(K::Right, M::NONE), E::ENTER], ("H", "i"), ); } @@ -134,22 +124,18 @@ fn right_key() { #[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"); + assert_line(*mode, &[E::ENTER], ""); + assert_line(*mode, &[E::from('a'), E::ENTER], "a"); + assert_line_with_initial(*mode, ("Hi", ""), &[E::ENTER], "Hi"); + assert_line_with_initial(*mode, ("", "Hi"), &[E::ENTER], "Hi"); + assert_line_with_initial(*mode, ("H", "i"), &[E::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"); + assert_line(*mode, &[E::ESC, E::ENTER], ""); + assert_line(*mode, &[E::from('a'), E::ESC, E::ENTER], "a"); + assert_line_with_initial(*mode, ("Hi", ""), &[E::ESC, E::ENTER], "Hi"); + assert_line_with_initial(*mode, ("", "Hi"), &[E::ESC, E::ENTER], "Hi"); + assert_line_with_initial(*mode, ("H", "i"), &[E::ESC, E::ENTER], "Hi"); } } } @@ -157,16 +143,12 @@ fn enter_key() { #[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"); + assert_line(*mode, &[E::ctrl('J')], ""); + assert_line(*mode, &[E::from('a'), E::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", - ); + assert_line(*mode, &[E::ESC, E::ctrl('J')], ""); + assert_line(*mode, &[E::from('a'), E::ESC, E::ctrl('J')], "a"); } } } @@ -174,53 +156,35 @@ fn newline_key() { #[test] fn eof_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { - let mut editor = init_editor(*mode, &[KeyPress::Ctrl('D')]); + let mut editor = init_editor(*mode, &[E::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')], + &[E::from('a'), E::ctrl('D'), E::ENTER], "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", - ); + assert_line(EditMode::Vi, &[E::from('a'), E::ctrl('D')], "a"); + assert_line(EditMode::Vi, &[E::from('a'), E::ESC, E::ctrl('D')], "a"); + assert_line_with_initial(EditMode::Emacs, ("", "Hi"), &[E::ctrl('D'), E::ENTER], "i"); + assert_line_with_initial(EditMode::Vi, ("", "Hi"), &[E::ctrl('D')], "Hi"); + assert_line_with_initial(EditMode::Vi, ("", "Hi"), &[E::ESC, E::ctrl('D')], "Hi"); } #[test] fn interrupt_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { - let mut editor = init_editor(*mode, &[KeyPress::Ctrl('C')]); + let mut editor = init_editor(*mode, &[E::ctrl('C')]); let err = editor.readline(">>"); assert_matches!(err, Err(ReadlineError::Interrupted)); - let mut editor = init_editor(*mode, &[KeyPress::Ctrl('C')]); + let mut editor = init_editor(*mode, &[E::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 mut editor = init_editor(*mode, &[E::ESC, E::ctrl('C')]); let err = editor.readline_with_initial(">>", ("Hi", "")); assert_matches!(err, Err(ReadlineError::Interrupted)); } @@ -233,13 +197,13 @@ fn delete_key() { assert_cursor( *mode, ("a", ""), - &[KeyPress::Delete, KeyPress::Enter], + &[E(K::Delete, M::NONE), E::ENTER], ("a", ""), ); assert_cursor( *mode, ("", "a"), - &[KeyPress::Delete, KeyPress::Enter], + &[E(K::Delete, M::NONE), E::ENTER], ("", ""), ); if *mode == EditMode::Vi { @@ -247,7 +211,7 @@ fn delete_key() { assert_cursor( *mode, ("", "a"), - &[KeyPress::Esc, KeyPress::Delete, KeyPress::Enter], + &[E::ESC, E(K::Delete, M::NONE), E::ENTER], ("", ""), ); } @@ -257,24 +221,14 @@ fn delete_key() { #[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"), - ); + assert_cursor(*mode, ("a", "b"), &[E::ctrl('T'), E::ENTER], ("ba", "")); + assert_cursor(*mode, ("ab", "cd"), &[E::ctrl('T'), E::ENTER], ("acb", "d")); if *mode == EditMode::Vi { // vi command mode assert_cursor( *mode, ("ab", ""), - &[KeyPress::Esc, KeyPress::Ctrl('T'), KeyPress::Enter], + &[E::ESC, E::ctrl('T'), E::ENTER], ("ba", ""), ); } @@ -287,21 +241,16 @@ fn ctrl_u() { assert_cursor( *mode, ("start of line ", "end"), - &[KeyPress::Ctrl('U'), KeyPress::Enter], - ("", "end"), - ); - assert_cursor( - *mode, - ("", "end"), - &[KeyPress::Ctrl('U'), KeyPress::Enter], + &[E::ctrl('U'), E::ENTER], ("", "end"), ); + assert_cursor(*mode, ("", "end"), &[E::ctrl('U'), E::ENTER], ("", "end")); if *mode == EditMode::Vi { // vi command mode assert_cursor( *mode, ("start of line ", "end"), - &[KeyPress::Esc, KeyPress::Ctrl('U'), KeyPress::Enter], + &[E::ESC, E::ctrl('U'), E::ENTER], ("", " end"), ); } @@ -315,7 +264,7 @@ fn ctrl_v() { assert_cursor( *mode, ("", ""), - &[KeyPress::Ctrl('V'), KeyPress::Char('\t'), KeyPress::Enter], + &[E::ctrl('V'), E(K::Char('\t'), M::NONE), E::ENTER], ("\t", ""), ); if *mode == EditMode::Vi { @@ -323,12 +272,7 @@ fn ctrl_v() { assert_cursor( *mode, ("", ""), - &[ - KeyPress::Esc, - KeyPress::Ctrl('V'), - KeyPress::Char('\t'), - KeyPress::Enter, - ], + &[E::ESC, E::ctrl('V'), E(K::Char('\t'), M::NONE), E::ENTER], ("\t", ""), ); } @@ -341,13 +285,13 @@ fn ctrl_w() { assert_cursor( *mode, ("Hello, ", "world"), - &[KeyPress::Ctrl('W'), KeyPress::Enter], + &[E::ctrl('W'), E::ENTER], ("", "world"), ); assert_cursor( *mode, ("Hello, world.", ""), - &[KeyPress::Ctrl('W'), KeyPress::Enter], + &[E::ctrl('W'), E::ENTER], ("Hello, ", ""), ); if *mode == EditMode::Vi { @@ -355,7 +299,7 @@ fn ctrl_w() { assert_cursor( *mode, ("Hello, world.", ""), - &[KeyPress::Esc, KeyPress::Ctrl('W'), KeyPress::Enter], + &[E::ESC, E::ctrl('W'), E::ENTER], ("Hello, ", "."), ); } @@ -368,7 +312,7 @@ fn ctrl_y() { assert_cursor( *mode, ("Hello, ", "world"), - &[KeyPress::Ctrl('W'), KeyPress::Ctrl('Y'), KeyPress::Enter], + &[E::ctrl('W'), E::ctrl('Y'), E::ENTER], ("Hello, ", "world"), ); } @@ -380,7 +324,7 @@ fn ctrl__() { assert_cursor( *mode, ("Hello, ", "world"), - &[KeyPress::Ctrl('W'), KeyPress::Ctrl('_'), KeyPress::Enter], + &[E::ctrl('W'), E::ctrl('_'), E::ENTER], ("Hello, ", "world"), ); if *mode == EditMode::Vi { @@ -388,12 +332,7 @@ fn ctrl__() { assert_cursor( *mode, ("Hello, ", "world"), - &[ - KeyPress::Esc, - KeyPress::Ctrl('W'), - KeyPress::Ctrl('_'), - KeyPress::Enter, - ], + &[E::ESC, E::ctrl('W'), E::ctrl('_'), E::ENTER], ("Hello,", " world"), ); } diff --git a/src/test/emacs.rs b/src/test/emacs.rs index dbd9d33b51..aa3e0b66d6 100644 --- a/src/test/emacs.rs +++ b/src/test/emacs.rs @@ -1,16 +1,22 @@ //! Emacs specific key bindings use super::{assert_cursor, assert_history}; use crate::config::EditMode; -use crate::keys::KeyPress; +use crate::keys::{KeyCode as K, KeyEvent as E, Modifiers as M}; #[test] fn ctrl_a() { assert_cursor( EditMode::Emacs, ("Hi", ""), - &[KeyPress::Ctrl('A'), KeyPress::Enter], + &[E::ctrl('A'), E::ENTER], ("", "Hi"), ); + assert_cursor( + EditMode::Emacs, + ("test test\n123", "foo"), + &[E::ctrl('A'), E::ENTER], + ("test test\n", "123foo"), + ); } #[test] @@ -18,9 +24,15 @@ fn ctrl_e() { assert_cursor( EditMode::Emacs, ("", "Hi"), - &[KeyPress::Ctrl('E'), KeyPress::Enter], + &[E::ctrl('E'), E::ENTER], ("Hi", ""), ); + assert_cursor( + EditMode::Emacs, + ("foo", "test test\n123"), + &[E::ctrl('E'), E::ENTER], + ("footest test", "\n123"), + ); } #[test] @@ -28,24 +40,19 @@ fn ctrl_b() { assert_cursor( EditMode::Emacs, ("Hi", ""), - &[KeyPress::Ctrl('B'), KeyPress::Enter], + &[E::ctrl('B'), E::ENTER], ("H", "i"), ); assert_cursor( EditMode::Emacs, ("Hi", ""), - &[KeyPress::Meta('2'), KeyPress::Ctrl('B'), KeyPress::Enter], + &[E::alt('2'), E::ctrl('B'), E::ENTER], ("", "Hi"), ); assert_cursor( EditMode::Emacs, ("", "Hi"), - &[ - KeyPress::Meta('-'), - KeyPress::Meta('2'), - KeyPress::Ctrl('B'), - KeyPress::Enter, - ], + &[E::alt('-'), E::alt('2'), E::ctrl('B'), E::ENTER], ("Hi", ""), ); } @@ -55,24 +62,19 @@ fn ctrl_f() { assert_cursor( EditMode::Emacs, ("", "Hi"), - &[KeyPress::Ctrl('F'), KeyPress::Enter], + &[E::ctrl('F'), E::ENTER], ("H", "i"), ); assert_cursor( EditMode::Emacs, ("", "Hi"), - &[KeyPress::Meta('2'), KeyPress::Ctrl('F'), KeyPress::Enter], + &[E::alt('2'), E::ctrl('F'), E::ENTER], ("Hi", ""), ); assert_cursor( EditMode::Emacs, ("Hi", ""), - &[ - KeyPress::Meta('-'), - KeyPress::Meta('2'), - KeyPress::Ctrl('F'), - KeyPress::Enter, - ], + &[E::alt('-'), E::alt('2'), E::ctrl('F'), E::ENTER], ("", "Hi"), ); } @@ -82,24 +84,19 @@ fn ctrl_h() { assert_cursor( EditMode::Emacs, ("Hi", ""), - &[KeyPress::Ctrl('H'), KeyPress::Enter], + &[E::ctrl('H'), E::ENTER], ("H", ""), ); assert_cursor( EditMode::Emacs, ("Hi", ""), - &[KeyPress::Meta('2'), KeyPress::Ctrl('H'), KeyPress::Enter], + &[E::alt('2'), E::ctrl('H'), E::ENTER], ("", ""), ); assert_cursor( EditMode::Emacs, ("", "Hi"), - &[ - KeyPress::Meta('-'), - KeyPress::Meta('2'), - KeyPress::Ctrl('H'), - KeyPress::Enter, - ], + &[E::alt('-'), E::alt('2'), E::ctrl('H'), E::ENTER], ("", ""), ); } @@ -109,19 +106,19 @@ fn backspace() { assert_cursor( EditMode::Emacs, ("", ""), - &[KeyPress::Backspace, KeyPress::Enter], + &[E::BACKSPACE, E::ENTER], ("", ""), ); assert_cursor( EditMode::Emacs, ("Hi", ""), - &[KeyPress::Backspace, KeyPress::Enter], + &[E::BACKSPACE, E::ENTER], ("H", ""), ); assert_cursor( EditMode::Emacs, ("", "Hi"), - &[KeyPress::Backspace, KeyPress::Enter], + &[E::BACKSPACE, E::ENTER], ("", "Hi"), ); } @@ -131,34 +128,86 @@ fn ctrl_k() { assert_cursor( EditMode::Emacs, ("Hi", ""), - &[KeyPress::Ctrl('K'), KeyPress::Enter], + &[E::ctrl('K'), E::ENTER], ("Hi", ""), ); assert_cursor( EditMode::Emacs, ("", "Hi"), - &[KeyPress::Ctrl('K'), KeyPress::Enter], + &[E::ctrl('K'), E::ENTER], ("", ""), ); assert_cursor( EditMode::Emacs, ("B", "ye"), - &[KeyPress::Ctrl('K'), KeyPress::Enter], + &[E::ctrl('K'), E::ENTER], ("B", ""), ); + assert_cursor( + EditMode::Emacs, + ("Hi", "foo\nbar"), + &[E::ctrl('K'), E::ENTER], + ("Hi", "\nbar"), + ); + assert_cursor( + EditMode::Emacs, + ("Hi", "\nbar"), + &[E::ctrl('K'), E::ENTER], + ("Hi", "bar"), + ); + assert_cursor( + EditMode::Emacs, + ("Hi", "bar"), + &[E::ctrl('K'), E::ENTER], + ("Hi", ""), + ); } +#[test] +fn ctrl_u() { + assert_cursor( + EditMode::Emacs, + ("", "Hi"), + &[E::ctrl('U'), E::ENTER], + ("", "Hi"), + ); + assert_cursor( + EditMode::Emacs, + ("Hi", ""), + &[E::ctrl('U'), E::ENTER], + ("", ""), + ); + assert_cursor( + EditMode::Emacs, + ("B", "ye"), + &[E::ctrl('U'), E::ENTER], + ("", "ye"), + ); + assert_cursor( + EditMode::Emacs, + ("foo\nbar", "Hi"), + &[E::ctrl('U'), E::ENTER], + ("foo\n", "Hi"), + ); + assert_cursor( + EditMode::Emacs, + ("foo\n", "Hi"), + &[E::ctrl('U'), E::ENTER], + ("foo", "Hi"), + ); + assert_cursor( + EditMode::Emacs, + ("foo", "Hi"), + &[E::ctrl('U'), E::ENTER], + ("", "Hi"), + ); +} #[test] fn ctrl_n() { assert_history( EditMode::Emacs, &["line1", "line2"], - &[ - KeyPress::Ctrl('P'), - KeyPress::Ctrl('P'), - KeyPress::Ctrl('N'), - KeyPress::Enter, - ], + &[E::ctrl('P'), E::ctrl('P'), E::ctrl('N'), E::ENTER], "", ("line2", ""), ); @@ -169,7 +218,7 @@ fn ctrl_p() { assert_history( EditMode::Emacs, &["line1"], - &[KeyPress::Ctrl('P'), KeyPress::Enter], + &[E::ctrl('P'), E::ENTER], "", ("line1", ""), ); @@ -180,7 +229,7 @@ fn ctrl_t() { /* FIXME assert_cursor( ("ab", "cd"), - &[KeyPress::Meta('2'), KeyPress::Ctrl('T'), KeyPress::Enter], + &[E::alt('2'), E::ctrl('T'), E::ENTER], ("acdb", ""), );*/ } @@ -190,12 +239,7 @@ fn ctrl_x_ctrl_u() { assert_cursor( EditMode::Emacs, ("Hello, ", "world"), - &[ - KeyPress::Ctrl('W'), - KeyPress::Ctrl('X'), - KeyPress::Ctrl('U'), - KeyPress::Enter, - ], + &[E::ctrl('W'), E::ctrl('X'), E::ctrl('U'), E::ENTER], ("Hello, ", "world"), ); } @@ -205,19 +249,19 @@ fn meta_b() { assert_cursor( EditMode::Emacs, ("Hello, world!", ""), - &[KeyPress::Meta('B'), KeyPress::Enter], + &[E::alt('B'), E::ENTER], ("Hello, ", "world!"), ); assert_cursor( EditMode::Emacs, ("Hello, world!", ""), - &[KeyPress::Meta('2'), KeyPress::Meta('B'), KeyPress::Enter], + &[E::alt('2'), E::alt('B'), E::ENTER], ("", "Hello, world!"), ); assert_cursor( EditMode::Emacs, ("", "Hello, world!"), - &[KeyPress::Meta('-'), KeyPress::Meta('B'), KeyPress::Enter], + &[E::alt('-'), E::alt('B'), E::ENTER], ("Hello", ", world!"), ); } @@ -227,19 +271,19 @@ fn meta_f() { assert_cursor( EditMode::Emacs, ("", "Hello, world!"), - &[KeyPress::Meta('F'), KeyPress::Enter], + &[E::alt('F'), E::ENTER], ("Hello", ", world!"), ); assert_cursor( EditMode::Emacs, ("", "Hello, world!"), - &[KeyPress::Meta('2'), KeyPress::Meta('F'), KeyPress::Enter], + &[E::alt('2'), E::alt('F'), E::ENTER], ("Hello, world", "!"), ); assert_cursor( EditMode::Emacs, ("Hello, world!", ""), - &[KeyPress::Meta('-'), KeyPress::Meta('F'), KeyPress::Enter], + &[E::alt('-'), E::alt('F'), E::ENTER], ("Hello, ", "world!"), ); } @@ -249,19 +293,19 @@ fn meta_c() { assert_cursor( EditMode::Emacs, ("hi", ""), - &[KeyPress::Meta('C'), KeyPress::Enter], + &[E::alt('C'), E::ENTER], ("hi", ""), ); assert_cursor( EditMode::Emacs, ("", "hi"), - &[KeyPress::Meta('C'), KeyPress::Enter], + &[E::alt('C'), E::ENTER], ("Hi", ""), ); /* FIXME assert_cursor( ("", "hi test"), - &[KeyPress::Meta('2'), KeyPress::Meta('C'), KeyPress::Enter], + &[E::alt('2'), E::alt('C'), E::ENTER], ("Hi Test", ""), );*/ } @@ -271,19 +315,19 @@ fn meta_l() { assert_cursor( EditMode::Emacs, ("Hi", ""), - &[KeyPress::Meta('L'), KeyPress::Enter], + &[E::alt('L'), E::ENTER], ("Hi", ""), ); assert_cursor( EditMode::Emacs, ("", "HI"), - &[KeyPress::Meta('L'), KeyPress::Enter], + &[E::alt('L'), E::ENTER], ("hi", ""), ); /* FIXME assert_cursor( ("", "HI TEST"), - &[KeyPress::Meta('2'), KeyPress::Meta('L'), KeyPress::Enter], + &[E::alt('2'), E::alt('L'), E::ENTER], ("hi test", ""), );*/ } @@ -293,19 +337,19 @@ fn meta_u() { assert_cursor( EditMode::Emacs, ("hi", ""), - &[KeyPress::Meta('U'), KeyPress::Enter], + &[E::alt('U'), E::ENTER], ("hi", ""), ); assert_cursor( EditMode::Emacs, ("", "hi"), - &[KeyPress::Meta('U'), KeyPress::Enter], + &[E::alt('U'), E::ENTER], ("HI", ""), ); /* FIXME assert_cursor( ("", "hi test"), - &[KeyPress::Meta('2'), KeyPress::Meta('U'), KeyPress::Enter], + &[E::alt('2'), E::alt('U'), E::ENTER], ("HI TEST", ""), );*/ } @@ -315,13 +359,13 @@ fn meta_d() { assert_cursor( EditMode::Emacs, ("Hello", ", world!"), - &[KeyPress::Meta('D'), KeyPress::Enter], + &[E::alt('D'), E::ENTER], ("Hello", "!"), ); assert_cursor( EditMode::Emacs, ("Hello", ", world!"), - &[KeyPress::Meta('2'), KeyPress::Meta('D'), KeyPress::Enter], + &[E::alt('2'), E::alt('D'), E::ENTER], ("Hello", ""), ); } @@ -331,13 +375,13 @@ fn meta_t() { assert_cursor( EditMode::Emacs, ("Hello", ", world!"), - &[KeyPress::Meta('T'), KeyPress::Enter], + &[E::alt('T'), E::ENTER], ("world, Hello", "!"), ); /* FIXME assert_cursor( ("One Two", " Three Four"), - &[KeyPress::Meta('T'), KeyPress::Enter], + &[E::alt('T'), E::ENTER], ("One Four Three Two", ""), );*/ } @@ -348,12 +392,12 @@ fn meta_y() { EditMode::Emacs, ("Hello, world", "!"), &[ - KeyPress::Ctrl('W'), - KeyPress::Left, - KeyPress::Ctrl('W'), - KeyPress::Ctrl('Y'), - KeyPress::Meta('Y'), - KeyPress::Enter, + E::ctrl('W'), + E(K::Left, M::NONE), + E::ctrl('W'), + E::ctrl('Y'), + E::alt('Y'), + E::ENTER, ], ("world", " !"), ); @@ -364,7 +408,7 @@ fn meta_backspace() { assert_cursor( EditMode::Emacs, ("Hello, wor", "ld!"), - &[KeyPress::Meta('\x08'), KeyPress::Enter], + &[E(K::Backspace, M::ALT), E::ENTER], ("Hello, ", "ld!"), ); } @@ -374,7 +418,7 @@ fn meta_digit() { assert_cursor( EditMode::Emacs, ("", ""), - &[KeyPress::Meta('3'), KeyPress::Char('h'), KeyPress::Enter], + &[E::alt('3'), E::from('h'), E::ENTER], ("hhh", ""), ); } diff --git a/src/test/highlight.rs b/src/test/highlight.rs new file mode 100644 index 0000000000..905197adc9 --- /dev/null +++ b/src/test/highlight.rs @@ -0,0 +1,22 @@ +use crate::highlight::split_highlight; + +#[test] +fn split_bold() { + let (a, b) = split_highlight("\x1b[1mword1 word2\x1b[0m", 5); + assert_eq!(a, "\x1b[1mword1\x1b[0m"); + assert_eq!(b, "\x1b[1m word2\x1b[0m"); +} + +#[test] +fn split_at_the_reset() { + let (a, b) = split_highlight("\x1b[1mword1\x1b[0m word2", 5); + assert_eq!(a, "\x1b[1mword1\x1b[0m"); + assert_eq!(b, " word2"); +} + +#[test] +fn split_nowhere() { + let (a, b) = split_highlight("\x1b[1mword1\x1b[0m word2", 6); + assert_eq!(a, "\x1b[1mword1\x1b[0m "); + assert_eq!(b, "word2"); +} diff --git a/src/test/history.rs b/src/test/history.rs index 88cb3b5b87..ab869249c6 100644 --- a/src/test/history.rs +++ b/src/test/history.rs @@ -1,7 +1,7 @@ //! History related commands tests use super::assert_history; use crate::config::EditMode; -use crate::keys::KeyPress; +use crate::keys::{KeyCode as K, KeyEvent as E, Modifiers as M}; #[test] fn down_key() { @@ -9,14 +9,19 @@ fn down_key() { assert_history( *mode, &["line1"], - &[KeyPress::Down, KeyPress::Enter], + &[E(K::Down, M::NONE), E::ENTER], "", ("", ""), ); assert_history( *mode, &["line1", "line2"], - &[KeyPress::Up, KeyPress::Up, KeyPress::Down, KeyPress::Enter], + &[ + E(K::Up, M::NONE), + E(K::Up, M::NONE), + E(K::Down, M::NONE), + E::ENTER, + ], "", ("line2", ""), ); @@ -24,10 +29,10 @@ fn down_key() { *mode, &["line1"], &[ - KeyPress::Char('a'), - KeyPress::Up, - KeyPress::Down, // restore original line - KeyPress::Enter, + E::from('a'), + E(K::Up, M::NONE), + E(K::Down, M::NONE), // restore original line + E::ENTER, ], "", ("a", ""), @@ -36,9 +41,9 @@ fn down_key() { *mode, &["line1"], &[ - KeyPress::Char('a'), - KeyPress::Down, // noop - KeyPress::Enter, + E::from('a'), + E(K::Down, M::NONE), // noop + E::ENTER, ], "", ("a", ""), @@ -49,18 +54,18 @@ fn down_key() { #[test] fn up_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { - assert_history(*mode, &[], &[KeyPress::Up, KeyPress::Enter], "", ("", "")); + assert_history(*mode, &[], &[E(K::Up, M::NONE), E::ENTER], "", ("", "")); assert_history( *mode, &["line1"], - &[KeyPress::Up, KeyPress::Enter], + &[E(K::Up, M::NONE), E::ENTER], "", ("line1", ""), ); assert_history( *mode, &["line1", "line2"], - &[KeyPress::Up, KeyPress::Up, KeyPress::Enter], + &[E(K::Up, M::NONE), E(K::Up, M::NONE), E::ENTER], "", ("line1", ""), ); @@ -73,7 +78,7 @@ fn ctrl_r() { assert_history( *mode, &[], - &[KeyPress::Ctrl('R'), KeyPress::Char('o'), KeyPress::Enter], + &[E::ctrl('R'), E::from('o'), E::ENTER], "", ("o", ""), ); @@ -81,10 +86,10 @@ fn ctrl_r() { *mode, &["rustc", "cargo"], &[ - KeyPress::Ctrl('R'), - KeyPress::Char('o'), - KeyPress::Right, // just to assert cursor pos - KeyPress::Enter, + E::ctrl('R'), + E::from('o'), + E(K::Right, M::NONE), // just to assert cursor pos + E::ENTER, ], "", ("cargo", ""), @@ -93,10 +98,10 @@ fn ctrl_r() { *mode, &["rustc", "cargo"], &[ - KeyPress::Ctrl('R'), - KeyPress::Char('u'), - KeyPress::Right, // just to assert cursor pos - KeyPress::Enter, + E::ctrl('R'), + E::from('u'), + E(K::Right, M::NONE), // just to assert cursor pos + E::ENTER, ], "", ("ru", "stc"), @@ -105,11 +110,11 @@ fn ctrl_r() { *mode, &["rustc", "cargo"], &[ - KeyPress::Ctrl('R'), - KeyPress::Char('r'), - KeyPress::Char('u'), - KeyPress::Right, // just to assert cursor pos - KeyPress::Enter, + E::ctrl('R'), + E::from('r'), + E::from('u'), + E(K::Right, M::NONE), // just to assert cursor pos + E::ENTER, ], "", ("r", "ustc"), @@ -118,11 +123,11 @@ fn ctrl_r() { *mode, &["rustc", "cargo"], &[ - KeyPress::Ctrl('R'), - KeyPress::Char('r'), - KeyPress::Ctrl('R'), - KeyPress::Right, // just to assert cursor pos - KeyPress::Enter, + E::ctrl('R'), + E::from('r'), + E::ctrl('R'), + E(K::Right, M::NONE), // just to assert cursor pos + E::ENTER, ], "", ("r", "ustc"), @@ -131,11 +136,11 @@ fn ctrl_r() { *mode, &["rustc", "cargo"], &[ - KeyPress::Ctrl('R'), - KeyPress::Char('r'), - KeyPress::Char('z'), // no match - KeyPress::Right, // just to assert cursor pos - KeyPress::Enter, + E::ctrl('R'), + E::from('r'), + E::from('z'), // no match + E(K::Right, M::NONE), // just to assert cursor pos + E::ENTER, ], "", ("car", "go"), @@ -144,11 +149,11 @@ fn ctrl_r() { 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, + E::from('a'), + E::ctrl('R'), + E::from('r'), + E::ctrl('G'), // abort (FIXME: doesn't work with vi mode) + E::ENTER, ], "", ("a", ""), @@ -162,7 +167,7 @@ fn ctrl_r_with_long_prompt() { assert_history( *mode, &["rustc", "cargo"], - &[KeyPress::Ctrl('R'), KeyPress::Char('o'), KeyPress::Enter], + &[E::ctrl('R'), E::from('o'), E::ENTER], ">>>>>>>>>>>>>>>>>>>>>>>>>>> ", ("cargo", ""), ); @@ -176,12 +181,12 @@ fn ctrl_s() { *mode, &["rustc", "cargo"], &[ - KeyPress::Ctrl('R'), - KeyPress::Char('r'), - KeyPress::Ctrl('R'), - KeyPress::Ctrl('S'), - KeyPress::Right, // just to assert cursor pos - KeyPress::Enter, + E::ctrl('R'), + E::from('r'), + E::ctrl('R'), + E::ctrl('S'), + E(K::Right, M::NONE), // just to assert cursor pos + E::ENTER, ], "", ("car", "go"), @@ -194,14 +199,14 @@ fn meta_lt() { assert_history( EditMode::Emacs, &[""], - &[KeyPress::Meta('<'), KeyPress::Enter], + &[E::alt('<'), E::ENTER], "", ("", ""), ); assert_history( EditMode::Emacs, &["rustc", "cargo"], - &[KeyPress::Meta('<'), KeyPress::Enter], + &[E::alt('<'), E::ENTER], "", ("rustc", ""), ); @@ -212,14 +217,14 @@ fn meta_gt() { assert_history( EditMode::Emacs, &[""], - &[KeyPress::Meta('>'), KeyPress::Enter], + &[E::alt('>'), E::ENTER], "", ("", ""), ); assert_history( EditMode::Emacs, &["rustc", "cargo"], - &[KeyPress::Meta('<'), KeyPress::Meta('>'), KeyPress::Enter], + &[E::alt('<'), E::alt('>'), E::ENTER], "", ("", ""), ); @@ -227,10 +232,10 @@ fn meta_gt() { EditMode::Emacs, &["rustc", "cargo"], &[ - KeyPress::Char('a'), - KeyPress::Meta('<'), - KeyPress::Meta('>'), // restore original line - KeyPress::Enter, + E::from('a'), + E::alt('<'), + E::alt('>'), // restore original line + E::ENTER, ], "", ("a", ""), diff --git a/src/test/mod.rs b/src/test/mod.rs index b995b905f3..e9875b773f 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -1,24 +1,27 @@ -use std::collections::HashMap; use std::sync::{Arc, RwLock}; use std::vec::IntoIter; +use radix_trie::Trie; + use crate::completion::Completer; use crate::config::{Config, EditMode}; use crate::edit::init_state; use crate::highlight::Highlighter; use crate::hint::Hinter; use crate::keymap::{Cmd, InputState}; -use crate::keys::KeyPress; +use crate::keys::{KeyCode as K, KeyEvent, KeyEvent as E, Modifiers as M}; use crate::tty::Sink; -use crate::{Context, Editor, Helper, Result}; +use crate::validate::Validator; +use crate::{apply_backspace_direct, readline_direct, Context, Editor, Helper, Result}; mod common; mod emacs; mod history; mod vi_cmd; mod vi_insert; +mod highlight; -fn init_editor(mode: EditMode, keys: &[KeyPress]) -> Editor<()> { +fn init_editor(mode: EditMode, keys: &[KeyEvent]) -> Editor<()> { let config = Config::builder().edit_mode(mode).build(); let mut editor = Editor::<()>::with_config(config); editor.term.keys.extend(keys.iter().cloned()); @@ -38,10 +41,17 @@ impl Completer for SimpleCompleter { Ok((0, vec![line.to_owned() + "t"])) } } +impl Hinter for SimpleCompleter { + type Hint = String; + + fn hint(&self, _line: &str, _pos: usize, _ctx: &Context<'_>) -> Option { + None + } +} impl Helper for SimpleCompleter {} -impl Hinter for SimpleCompleter {} impl Highlighter for SimpleCompleter {} +impl Validator for SimpleCompleter {} #[test] fn complete_line() { @@ -50,18 +60,23 @@ fn complete_line() { let helper = Some(SimpleCompleter); let mut s = init_state(&mut out, "rus", 3, helper.as_ref(), &history); let config = Config::default(); - let mut input_state = InputState::new(&config, Arc::new(RwLock::new(HashMap::new()))); - let keys = vec![KeyPress::Enter]; - let mut rdr: IntoIter = keys.into_iter(); + let mut input_state = InputState::new(&config, Arc::new(RwLock::new(Trie::new()))); + let keys = vec![E::ENTER]; + let mut rdr: IntoIter = keys.into_iter(); let cmd = super::complete_line(&mut rdr, &mut s, &mut input_state, &Config::default()).unwrap(); - assert_eq!(Some(Cmd::AcceptLine), cmd); + assert_eq!( + Some(Cmd::AcceptOrInsertLine { + accept_in_the_middle: true + }), + 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) { +fn assert_line(mode: EditMode, keys: &[KeyEvent], expected_line: &str) { let mut editor = init_editor(mode, keys); let actual_line = editor.readline(">>").unwrap(); assert_eq!(expected_line, actual_line); @@ -73,7 +88,7 @@ fn assert_line(mode: EditMode, keys: &[KeyPress], expected_line: &str) { fn assert_line_with_initial( mode: EditMode, initial: (&str, &str), - keys: &[KeyPress], + keys: &[KeyEvent], expected_line: &str, ) { let mut editor = init_editor(mode, keys); @@ -84,7 +99,7 @@ fn assert_line_with_initial( // `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)) { +fn assert_cursor(mode: EditMode, initial: (&str, &str), keys: &[KeyEvent], 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); @@ -97,7 +112,7 @@ fn assert_cursor(mode: EditMode, initial: (&str, &str), keys: &[KeyPress], expec fn assert_history( mode: EditMode, entries: &[&str], - keys: &[KeyPress], + keys: &[KeyEvent], prompt: &str, expected: (&str, &str), ) { @@ -115,7 +130,7 @@ fn assert_history( #[test] fn unknown_esc_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { - assert_line(*mode, &[KeyPress::UnknownEscSeq, KeyPress::Enter], ""); + assert_line(*mode, &[E(K::UnknownEscSeq, M::NONE), E::ENTER], ""); } } @@ -130,3 +145,29 @@ fn test_sync() { fn assert_sync() {} assert_sync::>(); } + +#[test] +fn test_apply_backspace_direct() { + assert_eq!( + &apply_backspace_direct("Hel\u{0008}\u{0008}el\u{0008}llo ☹\u{0008}☺"), + "Hello ☺" + ); +} + +#[test] +fn test_readline_direct() { + use std::io::Cursor; + + let mut write_buf = vec![]; + let output = readline_direct( + Cursor::new("([)\n\u{0008}\n\n\r\n])".as_bytes()), + Cursor::new(&mut write_buf), + &Some(crate::validate::MatchingBracketValidator::new()), + ); + + assert_eq!( + &write_buf, + b"Mismatched brackets: '[' is not properly closed" + ); + assert_eq!(&output.unwrap(), "([\n\n\r\n])"); +} diff --git a/src/test/vi_cmd.rs b/src/test/vi_cmd.rs index 1c2c143193..be8437d2dc 100644 --- a/src/test/vi_cmd.rs +++ b/src/test/vi_cmd.rs @@ -1,14 +1,14 @@ //! Vi command mode specific key bindings use super::{assert_cursor, assert_history}; use crate::config::EditMode; -use crate::keys::KeyPress; +use crate::keys::KeyEvent as E; #[test] fn dollar() { assert_cursor( EditMode::Vi, ("", "Hi"), - &[KeyPress::Esc, KeyPress::Char('$'), KeyPress::Enter], + &[E::ESC, E::from('$'), E::ENTER], ("Hi", ""), // FIXME ); } @@ -23,13 +23,7 @@ fn semi_colon() { assert_cursor( EditMode::Vi, ("", "Hello, world!"), - &[ - KeyPress::Esc, - KeyPress::Char('f'), - KeyPress::Char('o'), - KeyPress::Char(';'), - KeyPress::Enter, - ], + &[E::ESC, E::from('f'), E::from('o'), E::from(';'), E::ENTER], ("Hello, w", "orld!"), ); } @@ -39,13 +33,7 @@ fn comma() { assert_cursor( EditMode::Vi, ("Hello, w", "orld!"), - &[ - KeyPress::Esc, - KeyPress::Char('f'), - KeyPress::Char('l'), - KeyPress::Char(','), - KeyPress::Enter, - ], + &[E::ESC, E::from('f'), E::from('l'), E::from(','), E::ENTER], ("Hel", "lo, world!"), ); } @@ -55,7 +43,7 @@ fn zero() { assert_cursor( EditMode::Vi, ("Hi", ""), - &[KeyPress::Esc, KeyPress::Char('0'), KeyPress::Enter], + &[E::ESC, E::from('0'), E::ENTER], ("", "Hi"), ); } @@ -65,7 +53,7 @@ fn caret() { assert_cursor( EditMode::Vi, (" Hi", ""), - &[KeyPress::Esc, KeyPress::Char('^'), KeyPress::Enter], + &[E::ESC, E::from('^'), E::ENTER], (" ", "Hi"), ); } @@ -75,12 +63,7 @@ fn a() { assert_cursor( EditMode::Vi, ("B", "e"), - &[ - KeyPress::Esc, - KeyPress::Char('a'), - KeyPress::Char('y'), - KeyPress::Enter, - ], + &[E::ESC, E::from('a'), E::from('y'), E::ENTER], ("By", "e"), ); } @@ -90,12 +73,7 @@ fn uppercase_a() { assert_cursor( EditMode::Vi, ("", "By"), - &[ - KeyPress::Esc, - KeyPress::Char('A'), - KeyPress::Char('e'), - KeyPress::Enter, - ], + &[E::ESC, E::from('A'), E::from('e'), E::ENTER], ("Bye", ""), ); } @@ -105,18 +83,13 @@ fn b() { assert_cursor( EditMode::Vi, ("Hello, world!", ""), - &[KeyPress::Esc, KeyPress::Char('b'), KeyPress::Enter], + &[E::ESC, E::from('b'), E::ENTER], ("Hello, ", "world!"), ); assert_cursor( EditMode::Vi, ("Hello, world!", ""), - &[ - KeyPress::Esc, - KeyPress::Char('2'), - KeyPress::Char('b'), - KeyPress::Enter, - ], + &[E::ESC, E::from('2'), E::from('b'), E::ENTER], ("Hello", ", world!"), ); } @@ -126,18 +99,13 @@ fn uppercase_b() { assert_cursor( EditMode::Vi, ("Hello, world!", ""), - &[KeyPress::Esc, KeyPress::Char('B'), KeyPress::Enter], + &[E::ESC, E::from('B'), E::ENTER], ("Hello, ", "world!"), ); assert_cursor( EditMode::Vi, ("Hello, world!", ""), - &[ - KeyPress::Esc, - KeyPress::Char('2'), - KeyPress::Char('B'), - KeyPress::Enter, - ], + &[E::ESC, E::from('2'), E::from('B'), E::ENTER], ("", "Hello, world!"), ); } @@ -147,35 +115,30 @@ fn uppercase_c() { assert_cursor( EditMode::Vi, ("Hello, w", "orld!"), - &[ - KeyPress::Esc, - KeyPress::Char('C'), - KeyPress::Char('i'), - KeyPress::Enter, - ], + &[E::ESC, E::from('C'), E::from('i'), E::ENTER], ("Hello, i", ""), ); } #[test] fn ctrl_k() { - for key in &[KeyPress::Char('D'), KeyPress::Ctrl('K')] { + for key in &[E::from('D'), E::ctrl('K')] { assert_cursor( EditMode::Vi, ("Hi", ""), - &[KeyPress::Esc, *key, KeyPress::Enter], + &[E::ESC, *key, E::ENTER], ("H", ""), ); assert_cursor( EditMode::Vi, ("", "Hi"), - &[KeyPress::Esc, *key, KeyPress::Enter], + &[E::ESC, *key, E::ENTER], ("", ""), ); assert_cursor( EditMode::Vi, ("By", "e"), - &[KeyPress::Esc, *key, KeyPress::Enter], + &[E::ESC, *key, E::ENTER], ("B", ""), ); } @@ -186,18 +149,13 @@ fn e() { assert_cursor( EditMode::Vi, ("", "Hello, world!"), - &[KeyPress::Esc, KeyPress::Char('e'), KeyPress::Enter], + &[E::ESC, E::from('e'), E::ENTER], ("Hell", "o, world!"), ); assert_cursor( EditMode::Vi, ("", "Hello, world!"), - &[ - KeyPress::Esc, - KeyPress::Char('2'), - KeyPress::Char('e'), - KeyPress::Enter, - ], + &[E::ESC, E::from('2'), E::from('e'), E::ENTER], ("Hello, worl", "d!"), ); } @@ -207,18 +165,13 @@ fn uppercase_e() { assert_cursor( EditMode::Vi, ("", "Hello, world!"), - &[KeyPress::Esc, KeyPress::Char('E'), KeyPress::Enter], + &[E::ESC, E::from('E'), E::ENTER], ("Hello", ", world!"), ); assert_cursor( EditMode::Vi, ("", "Hello, world!"), - &[ - KeyPress::Esc, - KeyPress::Char('2'), - KeyPress::Char('E'), - KeyPress::Enter, - ], + &[E::ESC, E::from('2'), E::from('E'), E::ENTER], ("Hello, world", "!"), ); } @@ -228,24 +181,13 @@ fn f() { assert_cursor( EditMode::Vi, ("", "Hello, world!"), - &[ - KeyPress::Esc, - KeyPress::Char('f'), - KeyPress::Char('r'), - KeyPress::Enter, - ], + &[E::ESC, E::from('f'), E::from('r'), E::ENTER], ("Hello, wo", "rld!"), ); assert_cursor( EditMode::Vi, ("", "Hello, world!"), - &[ - KeyPress::Esc, - KeyPress::Char('3'), - KeyPress::Char('f'), - KeyPress::Char('l'), - KeyPress::Enter, - ], + &[E::ESC, E::from('3'), E::from('f'), E::from('l'), E::ENTER], ("Hello, wor", "ld!"), ); } @@ -255,24 +197,13 @@ fn uppercase_f() { assert_cursor( EditMode::Vi, ("Hello, world!", ""), - &[ - KeyPress::Esc, - KeyPress::Char('F'), - KeyPress::Char('r'), - KeyPress::Enter, - ], + &[E::ESC, E::from('F'), E::from('r'), E::ENTER], ("Hello, wo", "rld!"), ); assert_cursor( EditMode::Vi, ("Hello, world!", ""), - &[ - KeyPress::Esc, - KeyPress::Char('3'), - KeyPress::Char('F'), - KeyPress::Char('l'), - KeyPress::Enter, - ], + &[E::ESC, E::from('3'), E::from('F'), E::from('l'), E::ENTER], ("He", "llo, world!"), ); } @@ -282,12 +213,7 @@ fn i() { assert_cursor( EditMode::Vi, ("Be", ""), - &[ - KeyPress::Esc, - KeyPress::Char('i'), - KeyPress::Char('y'), - KeyPress::Enter, - ], + &[E::ESC, E::from('i'), E::from('y'), E::ENTER], ("By", "e"), ); } @@ -297,12 +223,7 @@ fn uppercase_i() { assert_cursor( EditMode::Vi, ("Be", ""), - &[ - KeyPress::Esc, - KeyPress::Char('I'), - KeyPress::Char('y'), - KeyPress::Enter, - ], + &[E::ESC, E::from('I'), E::from('y'), E::ENTER], ("y", "Be"), ); } @@ -312,12 +233,7 @@ fn u() { assert_cursor( EditMode::Vi, ("Hello, ", "world"), - &[ - KeyPress::Esc, - KeyPress::Ctrl('W'), - KeyPress::Char('u'), - KeyPress::Enter, - ], + &[E::ESC, E::ctrl('W'), E::from('u'), E::ENTER], ("Hello,", " world"), ); } @@ -327,18 +243,13 @@ fn w() { assert_cursor( EditMode::Vi, ("", "Hello, world!"), - &[KeyPress::Esc, KeyPress::Char('w'), KeyPress::Enter], + &[E::ESC, E::from('w'), E::ENTER], ("Hello", ", world!"), ); assert_cursor( EditMode::Vi, ("", "Hello, world!"), - &[ - KeyPress::Esc, - KeyPress::Char('2'), - KeyPress::Char('w'), - KeyPress::Enter, - ], + &[E::ESC, E::from('2'), E::from('w'), E::ENTER], ("Hello, ", "world!"), ); } @@ -348,18 +259,13 @@ fn uppercase_w() { assert_cursor( EditMode::Vi, ("", "Hello, world!"), - &[KeyPress::Esc, KeyPress::Char('W'), KeyPress::Enter], + &[E::ESC, E::from('W'), E::ENTER], ("Hello, ", "world!"), ); assert_cursor( EditMode::Vi, ("", "Hello, world!"), - &[ - KeyPress::Esc, - KeyPress::Char('2'), - KeyPress::Char('W'), - KeyPress::Enter, - ], + &[E::ESC, E::from('2'), E::from('W'), E::ENTER], ("Hello, world", "!"), ); } @@ -369,7 +275,7 @@ fn x() { assert_cursor( EditMode::Vi, ("", "a"), - &[KeyPress::Esc, KeyPress::Char('x'), KeyPress::Enter], + &[E::ESC, E::from('x'), E::ENTER], ("", ""), ); } @@ -379,28 +285,24 @@ fn uppercase_x() { assert_cursor( EditMode::Vi, ("Hi", ""), - &[KeyPress::Esc, KeyPress::Char('X'), KeyPress::Enter], + &[E::ESC, E::from('X'), E::ENTER], ("", "i"), ); } #[test] fn h() { - for key in &[ - KeyPress::Char('h'), - KeyPress::Ctrl('H'), - KeyPress::Backspace, - ] { + for key in &[E::from('h'), E::ctrl('H'), E::BACKSPACE] { assert_cursor( EditMode::Vi, ("Bye", ""), - &[KeyPress::Esc, *key, KeyPress::Enter], + &[E::ESC, *key, E::ENTER], ("B", "ye"), ); assert_cursor( EditMode::Vi, ("Bye", ""), - &[KeyPress::Esc, KeyPress::Char('2'), *key, KeyPress::Enter], + &[E::ESC, E::from('2'), *key, E::ENTER], ("", "Bye"), ); } @@ -408,17 +310,17 @@ fn h() { #[test] fn l() { - for key in &[KeyPress::Char('l'), KeyPress::Char(' ')] { + for key in &[E::from('l'), E::from(' ')] { assert_cursor( EditMode::Vi, ("", "Hi"), - &[KeyPress::Esc, *key, KeyPress::Enter], + &[E::ESC, *key, E::ENTER], ("H", "i"), ); assert_cursor( EditMode::Vi, ("", "Hi"), - &[KeyPress::Esc, KeyPress::Char('2'), *key, KeyPress::Enter], + &[E::ESC, E::from('2'), *key, E::ENTER], ("Hi", ""), ); } @@ -426,21 +328,70 @@ fn l() { #[test] fn j() { - for key in &[ - KeyPress::Char('j'), - KeyPress::Char('+'), - KeyPress::Ctrl('N'), - ] { + for key in &[E::from('j'), E::from('+')] { + assert_cursor( + EditMode::Vi, + ("Hel", "lo,\nworld!"), + // NOTE: escape moves backwards on char + &[E::ESC, *key, E::ENTER], + ("Hello,\nwo", "rld!"), + ); + assert_cursor( + EditMode::Vi, + ("", "One\nTwo\nThree"), + &[E::ESC, E::from('2'), *key, E::ENTER], + ("One\nTwo\n", "Three"), + ); + assert_cursor( + EditMode::Vi, + ("Hel", "lo,\nworld!"), + // NOTE: escape moves backwards on char + &[E::ESC, E::from('7'), *key, E::ENTER], + ("Hello,\nwo", "rld!"), + ); + } +} + +#[test] +fn k() { + for key in &[E::from('k'), E::from('-')] { + assert_cursor( + EditMode::Vi, + ("Hello,\nworl", "d!"), + // NOTE: escape moves backwards on char + &[E::ESC, *key, E::ENTER], + ("Hel", "lo,\nworld!"), + ); + assert_cursor( + EditMode::Vi, + ("One\nTwo\nT", "hree"), + // NOTE: escape moves backwards on char + &[E::ESC, E::from('2'), *key, E::ENTER], + ("", "One\nTwo\nThree"), + ); + assert_cursor( + EditMode::Vi, + ("Hello,\nworl", "d!"), + // NOTE: escape moves backwards on char + &[E::ESC, E::from('5'), *key, E::ENTER], + ("Hel", "lo,\nworld!"), + ); + assert_cursor( + EditMode::Vi, + ("first line\nshort\nlong line", ""), + &[E::ESC, *key, E::ENTER], + ("first line\nshort", "\nlong line"), + ); + } +} + +#[test] +fn ctrl_n() { + for key in &[E::ctrl('N')] { assert_history( EditMode::Vi, &["line1", "line2"], - &[ - KeyPress::Esc, - KeyPress::Ctrl('P'), - KeyPress::Ctrl('P'), - *key, - KeyPress::Enter, - ], + &[E::ESC, E::ctrl('P'), E::ctrl('P'), *key, E::ENTER], "", ("line2", ""), ); @@ -448,16 +399,12 @@ fn j() { } #[test] -fn k() { - for key in &[ - KeyPress::Char('k'), - KeyPress::Char('-'), - KeyPress::Ctrl('P'), - ] { +fn ctrl_p() { + for key in &[E::ctrl('P')] { assert_history( EditMode::Vi, &["line1"], - &[KeyPress::Esc, *key, KeyPress::Enter], + &[E::ESC, *key, E::ENTER], "", ("line1", ""), ); @@ -469,12 +416,7 @@ fn p() { assert_cursor( EditMode::Vi, ("Hello, ", "world"), - &[ - KeyPress::Esc, - KeyPress::Ctrl('W'), - KeyPress::Char('p'), - KeyPress::Enter, - ], + &[E::ESC, E::ctrl('W'), E::from('p'), E::ENTER], (" Hello", ",world"), ); } @@ -484,12 +426,7 @@ fn uppercase_p() { assert_cursor( EditMode::Vi, ("Hello, ", "world"), - &[ - KeyPress::Esc, - KeyPress::Ctrl('W'), - KeyPress::Char('P'), - KeyPress::Enter, - ], + &[E::ESC, E::ctrl('W'), E::from('P'), E::ENTER], ("Hello", ", world"), ); } @@ -499,24 +436,13 @@ fn r() { assert_cursor( EditMode::Vi, ("Hi", ", world!"), - &[ - KeyPress::Esc, - KeyPress::Char('r'), - KeyPress::Char('o'), - KeyPress::Enter, - ], + &[E::ESC, E::from('r'), E::from('o'), E::ENTER], ("H", "o, world!"), ); assert_cursor( EditMode::Vi, ("He", "llo, world!"), - &[ - KeyPress::Esc, - KeyPress::Char('4'), - KeyPress::Char('r'), - KeyPress::Char('i'), - KeyPress::Enter, - ], + &[E::ESC, E::from('4'), E::from('r'), E::from('i'), E::ENTER], ("Hiii", "i, world!"), ); } @@ -526,24 +452,13 @@ fn s() { assert_cursor( EditMode::Vi, ("Hi", ", world!"), - &[ - KeyPress::Esc, - KeyPress::Char('s'), - KeyPress::Char('o'), - KeyPress::Enter, - ], + &[E::ESC, E::from('s'), E::from('o'), E::ENTER], ("Ho", ", world!"), ); assert_cursor( EditMode::Vi, ("He", "llo, world!"), - &[ - KeyPress::Esc, - KeyPress::Char('4'), - KeyPress::Char('s'), - KeyPress::Char('i'), - KeyPress::Enter, - ], + &[E::ESC, E::from('4'), E::from('s'), E::from('i'), E::ENTER], ("Hi", ", world!"), ); } @@ -553,7 +468,7 @@ fn uppercase_s() { assert_cursor( EditMode::Vi, ("Hello, ", "world"), - &[KeyPress::Esc, KeyPress::Char('S'), KeyPress::Enter], + &[E::ESC, E::from('S'), E::ENTER], ("", ""), ); } @@ -563,24 +478,13 @@ fn t() { assert_cursor( EditMode::Vi, ("", "Hello, world!"), - &[ - KeyPress::Esc, - KeyPress::Char('t'), - KeyPress::Char('r'), - KeyPress::Enter, - ], + &[E::ESC, E::from('t'), E::from('r'), E::ENTER], ("Hello, w", "orld!"), ); assert_cursor( EditMode::Vi, ("", "Hello, world!"), - &[ - KeyPress::Esc, - KeyPress::Char('3'), - KeyPress::Char('t'), - KeyPress::Char('l'), - KeyPress::Enter, - ], + &[E::ESC, E::from('3'), E::from('t'), E::from('l'), E::ENTER], ("Hello, wo", "rld!"), ); } @@ -590,24 +494,90 @@ fn uppercase_t() { assert_cursor( EditMode::Vi, ("Hello, world!", ""), - &[ - KeyPress::Esc, - KeyPress::Char('T'), - KeyPress::Char('r'), - KeyPress::Enter, - ], + &[E::ESC, E::from('T'), E::from('r'), E::ENTER], ("Hello, wor", "ld!"), ); assert_cursor( EditMode::Vi, ("Hello, world!", ""), - &[ - KeyPress::Esc, - KeyPress::Char('3'), - KeyPress::Char('T'), - KeyPress::Char('l'), - KeyPress::Enter, - ], + &[E::ESC, E::from('3'), E::from('T'), E::from('l'), E::ENTER], ("Hel", "lo, world!"), ); } + +#[test] +fn indent() { + assert_cursor( + EditMode::Vi, + ("Hello, world!", ""), + &[E::ESC, E::from('>'), E::from('>'), E::ENTER], + (" Hello, world", "!"), // Esc moves to the left + ); + assert_cursor( + EditMode::Vi, + ("line1\nline2", ""), + &[E::ESC, E::from('>'), E::from('>'), E::ENTER], + ("line1\n line", "2"), // Esc moves to the left + ); + assert_cursor( + EditMode::Vi, + ("line1\nline2", ""), + &[E::ESC, E::from('>'), E::from('k'), E::ENTER], + (" line1\n line", "2"), // Esc moves to the left + ); + assert_cursor( + EditMode::Vi, + (" li", "ne1\n line2"), + &[E::ESC, E::from('>'), E::from('j'), E::ENTER], + (" l", "ine1\n line2"), // Esc moves to the left + ); + assert_cursor( + EditMode::Vi, + (" ", "line1\n line2"), + &[E::ESC, E::from('>'), E::from('j'), E::ENTER], + (" ", " line1\n line2"), // Esc moves to the left + ); + assert_cursor( + EditMode::Vi, + (" ", "line1\n line2"), + &[E::ESC, E::from('>'), E::from('j'), E::ENTER], + (" ", " line1\n line2"), // Esc moves to the left + ); +} + +#[test] +fn dedent() { + assert_cursor( + EditMode::Vi, + (" line1\n line2", ""), + &[E::ESC, E::from('<'), E::from('<'), E::ENTER], + (" line1\nline", "2"), + ); + + assert_cursor( + EditMode::Vi, + (" line1\n line2", ""), + &[E::ESC, E::from('<'), E::from('k'), E::ENTER], + ("line1\nline", "2"), + ); + + assert_cursor( + EditMode::Vi, + (" li", "ne1\n line2"), + &[E::ESC, E::from('<'), E::from('j'), E::ENTER], + ("l", "ine1\nline2"), + ); + + assert_cursor( + EditMode::Vi, + (" ", "line1\n line2"), + &[E::ESC, E::from('<'), E::from('j'), E::ENTER], + ("", "line1\nline2"), + ); + assert_cursor( + EditMode::Vi, + ("line", "1\n line2"), + &[E::ESC, E::from('<'), E::from('j'), E::ENTER], + ("lin", "e1\nline2"), + ); +} diff --git a/src/test/vi_insert.rs b/src/test/vi_insert.rs index 608e2ac760..8a18529631 100644 --- a/src/test/vi_insert.rs +++ b/src/test/vi_insert.rs @@ -1,16 +1,11 @@ //! Vi insert mode specific key bindings use super::assert_cursor; use crate::config::EditMode; -use crate::keys::KeyPress; +use crate::keys::KeyEvent as E; #[test] fn insert_mode_by_default() { - assert_cursor( - EditMode::Vi, - ("", ""), - &[KeyPress::Char('a'), KeyPress::Enter], - ("a", ""), - ); + assert_cursor(EditMode::Vi, ("", ""), &[E::from('a'), E::ENTER], ("a", "")); } #[test] @@ -18,29 +13,24 @@ fn ctrl_h() { assert_cursor( EditMode::Vi, ("Hi", ""), - &[KeyPress::Ctrl('H'), KeyPress::Enter], + &[E::ctrl('H'), E::ENTER], ("H", ""), ); } #[test] fn backspace() { - assert_cursor( - EditMode::Vi, - ("", ""), - &[KeyPress::Backspace, KeyPress::Enter], - ("", ""), - ); + assert_cursor(EditMode::Vi, ("", ""), &[E::BACKSPACE, E::ENTER], ("", "")); assert_cursor( EditMode::Vi, ("Hi", ""), - &[KeyPress::Backspace, KeyPress::Enter], + &[E::BACKSPACE, E::ENTER], ("H", ""), ); assert_cursor( EditMode::Vi, ("", "Hi"), - &[KeyPress::Backspace, KeyPress::Enter], + &[E::BACKSPACE, E::ENTER], ("", "Hi"), ); } @@ -50,7 +40,7 @@ fn esc() { assert_cursor( EditMode::Vi, ("", ""), - &[KeyPress::Char('a'), KeyPress::Esc, KeyPress::Enter], + &[E::from('a'), E::ESC, E::ENTER], ("", "a"), ); } diff --git a/src/tty/mod.rs b/src/tty/mod.rs index 3f6097ef23..d0a1f5c2a6 100644 --- a/src/tty/mod.rs +++ b/src/tty/mod.rs @@ -1,10 +1,14 @@ //! This module implements and describes common TTY methods & traits + +use unicode_width::UnicodeWidthStr; + use crate::config::{BellStyle, ColorMode, Config, OutputStreamType}; -use crate::highlight::Highlighter; -use crate::keys::KeyPress; +use crate::edit::Prompt; +use crate::highlight::{Highlighter, PromptInfo, split_highlight}; +use crate::keys::KeyEvent; use crate::layout::{Layout, Position}; use crate::line_buffer::LineBuffer; -use crate::Result; +use crate::{Cmd, Result}; /// Terminal state pub trait RawMode: Sized { @@ -15,12 +19,14 @@ pub trait RawMode: Sized { /// Translate bytes read from stdin to keys. pub trait RawReader { /// Blocking read of key pressed. - fn next_key(&mut self, single_esc_abort: bool) -> Result; + fn next_key(&mut self, single_esc_abort: bool) -> Result; /// For CTRL-V support #[cfg(unix)] fn next_char(&mut self) -> Result; /// Bracketed paste fn read_pasted_text(&mut self) -> Result; + /// Check if `key` is bound to a peculiar command + fn find_binding(&self, key: &KeyEvent) -> Option; } /// Display prompt, line and cursor in terminal output @@ -33,7 +39,7 @@ pub trait Renderer { #[allow(clippy::too_many_arguments)] fn refresh_line( &mut self, - prompt: &str, + prompt: &Prompt, line: &LineBuffer, hint: Option<&str>, old_layout: &Layout, @@ -41,9 +47,49 @@ pub trait Renderer { highlighter: Option<&dyn Highlighter>, ) -> Result<()>; + /// Compute layout for rendering prompt + line + some info (either hint, + /// validation msg, ...). on the screen. Depending on screen width, line + /// wrapping may be applied. + fn compute_layout( + &self, + prompt: &Prompt, + line: &LineBuffer, + info: Option<&str>, + ) -> Layout { + // calculate the desired position of the cursor + let pos = line.pos(); + let left_margin = if prompt.has_continuation { + prompt.size.col + } else { + 0 + }; + let cursor = self.calculate_position(&line[..pos], + prompt.size, left_margin); + // calculate the position of the end of the input line + let mut end = if pos == line.len() { + cursor + } else { + self.calculate_position(&line[pos..], cursor, left_margin) + }; + if let Some(info) = info { + end = self.calculate_position(info, end, left_margin); + } + + let new_layout = Layout { + prompt_size: prompt.size, + left_margin, + default_prompt: prompt.is_default, + cursor, + end, + }; + debug_assert!(new_layout.cursor <= new_layout.end); + new_layout + } + /// 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 calculate_position(&self, s: &str, orig: Position, left_margin: usize) + -> Position; fn write_and_flush(&self, buf: &[u8]) -> Result<()>; @@ -78,7 +124,7 @@ impl<'a, R: Renderer + ?Sized> Renderer for &'a mut R { fn refresh_line( &mut self, - prompt: &str, + prompt: &Prompt, line: &LineBuffer, hint: Option<&str>, old_layout: &Layout, @@ -88,8 +134,10 @@ impl<'a, R: Renderer + ?Sized> Renderer for &'a mut R { (**self).refresh_line(prompt, line, hint, old_layout, new_layout, highlighter) } - fn calculate_position(&self, s: &str, orig: Position) -> Position { - (**self).calculate_position(s, orig) + fn calculate_position(&self, s: &str, orig: Position, left_margin: usize) + -> Position + { + (**self).calculate_position(s, orig, left_margin) } fn write_and_flush(&self, buf: &[u8]) -> Result<()> { @@ -129,8 +177,40 @@ impl<'a, R: Renderer + ?Sized> Renderer for &'a mut R { } } +// ignore ANSI escape sequence +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() + } +} + /// Terminal contract pub trait Term { + type KeyMap; type Reader: RawReader; // rl_instream type Writer: Renderer; // rl_outstream type Mode: RawMode; @@ -140,6 +220,7 @@ pub trait Term { stream: OutputStreamType, tab_stop: usize, bell_style: BellStyle, + enable_bracketed_paste: bool, ) -> Self; /// Check if current terminal can provide a rich line-editing user /// interface. @@ -149,26 +230,99 @@ pub trait Term { /// check if output stream is connected to a terminal. fn is_output_tty(&self) -> bool; /// Enable RAW mode for the terminal. - fn enable_raw_mode(&mut self) -> Result; + fn enable_raw_mode(&mut self) -> Result<(Self::Mode, Self::KeyMap)>; /// Create a RAW reader - fn create_reader(&self, config: &Config) -> Result; + fn create_reader(&self, config: &Config, key_map: Self::KeyMap) -> Result; /// Create a writer fn create_writer(&self) -> Self::Writer; } -cfg_if::cfg_if! { - if #[cfg(any(test, target_arch = "wasm32"))] { - mod test; - pub use self::test::*; - } else if #[cfg(windows)] { - // If on Windows platform import Windows TTY module - // and re-export into mod.rs scope - mod windows; - pub use self::windows::*; - } else if #[cfg(unix)] { - // If on Unix platform import Unix TTY module - // and re-export into mod.rs scope - mod unix; - pub use self::unix::*; +fn add_prompt_and_highlight( + mut push_str: F, highlighter: Option<&dyn Highlighter>, + line: &LineBuffer, prompt: &Prompt) + where F: FnMut(&str), +{ + if let Some(highlighter) = highlighter { + if prompt.has_continuation { + if &line[..] == "" { + // line.lines() is an empty iterator for empty line so + // we need to treat it as a special case + let prompt = highlighter.highlight_prompt(prompt.text, + PromptInfo { + is_default: prompt.is_default, + offset: 0, + cursor: Some(0), + input: "", + line: "", + line_no: 0, + }); + push_str(&prompt); + } else { + let highlighted = highlighter.highlight(line, line.pos()); + let lines = line.split('\n'); + let mut highlighted_left = highlighted.to_string(); + let mut offset = 0; + for (line_no, orig) in lines.enumerate() { + let (hl, tail) = split_highlight(&highlighted_left, + orig.len()+1); + let has_cursor = + line.pos() > offset && line.pos() < orig.len(); + let prompt = highlighter.highlight_prompt(prompt.text, + PromptInfo { + is_default: prompt.is_default, + offset, + cursor: if has_cursor { + Some(line.pos() - offset) + } else { + None + }, + input: line, + line: orig, + line_no, + }); + push_str(&prompt); + push_str(&hl); + highlighted_left = tail.to_string(); + offset += orig.len() + 1; + } + } + } else { + // display the prompt + push_str(&highlighter.highlight_prompt(prompt.text, + PromptInfo { + is_default: prompt.is_default, + offset: 0, + cursor: Some(line.pos()), + input: line, + line: line, + line_no: 0, + })); + // display the input line + push_str(&highlighter.highlight(line, line.pos())); + } + } else { + // display the prompt + push_str(prompt.text); + // display the input line + push_str(line); } } + +// If on Windows platform import Windows TTY module +// and re-export into mod.rs scope +#[cfg(all(windows, not(target_arch = "wasm32")))] +mod windows; +#[cfg(all(windows, not(target_arch = "wasm32")))] +pub use self::windows::*; + +// If on Unix platform import Unix TTY module +// and re-export into mod.rs scope +#[cfg(all(unix, not(target_arch = "wasm32")))] +mod unix; +#[cfg(all(unix, not(target_arch = "wasm32")))] +pub use self::unix::*; + +#[cfg(any(test, target_arch = "wasm32"))] +mod test; +#[cfg(any(test, target_arch = "wasm32"))] +pub use self::test::*; diff --git a/src/tty/test.rs b/src/tty/test.rs index 34333b1a87..9f89cd4acb 100644 --- a/src/tty/test.rs +++ b/src/tty/test.rs @@ -6,12 +6,14 @@ use std::vec::IntoIter; use super::{RawMode, RawReader, Renderer, Term}; use crate::config::{BellStyle, ColorMode, Config, OutputStreamType}; use crate::error::ReadlineError; +use crate::edit::Prompt; use crate::highlight::Highlighter; -use crate::keys::KeyPress; +use crate::keys::KeyEvent; use crate::layout::{Layout, Position}; use crate::line_buffer::LineBuffer; -use crate::Result; +use crate::{Cmd, Result}; +pub type KeyMap = (); pub type Mode = (); impl RawMode for Mode { @@ -20,8 +22,8 @@ impl RawMode for Mode { } } -impl<'a> RawReader for Iter<'a, KeyPress> { - fn next_key(&mut self, _: bool) -> Result { +impl<'a> RawReader for Iter<'a, KeyEvent> { + fn next_key(&mut self, _: bool) -> Result { match self.next() { Some(key) => Ok(*key), None => Err(ReadlineError::Eof), @@ -36,10 +38,14 @@ impl<'a> RawReader for Iter<'a, KeyPress> { fn read_pasted_text(&mut self) -> Result { unimplemented!() } + + fn find_binding(&self, _: &KeyEvent) -> Option { + None + } } -impl RawReader for IntoIter { - fn next_key(&mut self, _: bool) -> Result { +impl RawReader for IntoIter { + fn next_key(&mut self, _: bool) -> Result { match self.next() { Some(key) => Ok(key), None => Err(ReadlineError::Eof), @@ -48,8 +54,9 @@ impl RawReader for IntoIter { #[cfg(unix)] fn next_char(&mut self) -> Result { + use crate::keys::{KeyCode as K, KeyEvent as E, Modifiers as M}; match self.next() { - Some(KeyPress::Char(c)) => Ok(c), + Some(E(K::Char(c), M::NONE)) => Ok(c), None => Err(ReadlineError::Eof), _ => unimplemented!(), } @@ -58,6 +65,10 @@ impl RawReader for IntoIter { fn read_pasted_text(&mut self) -> Result { unimplemented!() } + + fn find_binding(&self, _: &KeyEvent) -> Option { + None + } } pub struct Sink {} @@ -69,7 +80,7 @@ impl Sink { } impl Renderer for Sink { - type Reader = IntoIter; + type Reader = IntoIter; fn move_cursor(&mut self, _: Position, _: Position) -> Result<()> { Ok(()) @@ -77,7 +88,7 @@ impl Renderer for Sink { fn refresh_line( &mut self, - _prompt: &str, + _prompt: &Prompt, _line: &LineBuffer, _hint: Option<&str>, _old_layout: &Layout, @@ -87,7 +98,9 @@ impl Renderer for Sink { Ok(()) } - fn calculate_position(&self, s: &str, orig: Position) -> Position { + fn calculate_position(&self, s: &str, orig: Position, _left_margin: usize) + -> Position + { let mut pos = orig; pos.col += s.len(); pos @@ -123,7 +136,7 @@ impl Renderer for Sink { false } - fn move_cursor_at_leftmost(&mut self, _: &mut IntoIter) -> Result<()> { + fn move_cursor_at_leftmost(&mut self, _: &mut IntoIter) -> Result<()> { Ok(()) } } @@ -132,15 +145,16 @@ pub type Terminal = DummyTerminal; #[derive(Clone, Debug)] pub struct DummyTerminal { - pub keys: Vec, + pub keys: Vec, pub cursor: usize, // cursor position before last command pub color_mode: ColorMode, pub bell_style: BellStyle, } impl Term for DummyTerminal { + type KeyMap = KeyMap; type Mode = Mode; - type Reader = IntoIter; + type Reader = IntoIter; type Writer = Sink; fn new( @@ -148,6 +162,7 @@ impl Term for DummyTerminal { _stream: OutputStreamType, _tab_stop: usize, bell_style: BellStyle, + _enable_bracketed_paste: bool, ) -> DummyTerminal { DummyTerminal { keys: Vec::new(), @@ -159,10 +174,16 @@ impl Term for DummyTerminal { // Init checks: + #[cfg(not(target_arch = "wasm32"))] fn is_unsupported(&self) -> bool { false } + #[cfg(target_arch = "wasm32")] + fn is_unsupported(&self) -> bool { + true + } + fn is_stdin_tty(&self) -> bool { true } @@ -173,11 +194,11 @@ impl Term for DummyTerminal { // Interactive loop: - fn enable_raw_mode(&mut self) -> Result { - Ok(()) + fn enable_raw_mode(&mut self) -> Result<(Mode, KeyMap)> { + Ok(((), ())) } - fn create_reader(&self, _: &Config) -> Result> { + fn create_reader(&self, _: &Config, _: KeyMap) -> Result { Ok(self.keys.clone().into_iter()) } diff --git a/src/tty/unix.rs b/src/tty/unix.rs index a98c7e191d..79b24d75d4 100644 --- a/src/tty/unix.rs +++ b/src/tty/unix.rs @@ -1,30 +1,29 @@ //! Unix specific definitions -use std; -use std::cmp::Ordering; -use std::io::{self, Read, Write}; +use std::cmp; +use std::io::{self, ErrorKind, Read, Write}; use std::os::unix::io::{AsRawFd, RawFd}; use std::sync; -use std::sync::atomic; +use std::sync::atomic::{AtomicBool, Ordering}; -use libc; use log::{debug, warn}; -use nix; use nix::poll::{self, PollFlags}; use nix::sys::signal; use nix::sys::termios; -use nix::sys::termios::SetArg; +use nix::sys::termios::{SetArg, SpecialCharacterIndices as SCI, Termios}; use unicode_segmentation::UnicodeSegmentation; -use unicode_width::UnicodeWidthStr; use utf8parse::{Parser, Receiver}; -use super::{RawMode, RawReader, Renderer, Term}; +use super::{width, RawMode, RawReader, Renderer, Term}; use crate::config::{BellStyle, ColorMode, Config, OutputStreamType}; use crate::error; +use crate::edit::Prompt; use crate::highlight::Highlighter; -use crate::keys::{self, KeyPress}; +use crate::keys::{KeyCode as K, KeyEvent, KeyEvent as E, Modifiers as M}; use crate::layout::{Layout, Position}; use crate::line_buffer::LineBuffer; -use crate::Result; +use crate::{Cmd, Result}; +use std::collections::HashMap; +use crate::tty::add_prompt_and_highlight; const STDIN_FILENO: RawFd = libc::STDIN_FILENO; @@ -45,14 +44,34 @@ impl AsRawFd for OutputStreamType { nix::ioctl_read_bad!(win_size, libc::TIOCGWINSZ, libc::winsize); -#[allow(clippy::identity_conversion)] +#[allow(clippy::useless_conversion)] fn get_win_size(fileno: &T) -> (usize, usize) { use std::mem::zeroed; + if cfg!(test) { + return (80, 24); + } + unsafe { let mut size: libc::winsize = zeroed(); match win_size(fileno.as_raw_fd(), &mut size) { - Ok(0) => (size.ws_col as usize, size.ws_row as usize), // TODO getCursorPosition + Ok(0) => { + // In linux pseudo-terminals are created with dimensions of + // zero. If host application didn't initialize the correct + // size before start we treat zero size as 80 columns and + // infinite rows + let cols = if size.ws_col == 0 { + 80 + } else { + size.ws_col as usize + }; + let rows = if size.ws_row == 0 { + usize::MAX + } else { + size.ws_row as usize + }; + (cols, rows) + } _ => (80, 24), } } @@ -79,6 +98,11 @@ fn is_a_tty(fd: RawFd) -> bool { unsafe { libc::isatty(fd) != 0 } } +pub type PosixKeyMap = HashMap; +#[cfg(not(test))] +pub type KeyMap = PosixKeyMap; + +#[must_use = "You must restore default mode (disable_raw_mode)"] pub struct PosixMode { termios: termios::Termios, out: Option, @@ -115,9 +139,7 @@ impl Read for StdinRaw { }; if res == -1 { let error = io::Error::last_os_error(); - if error.kind() != io::ErrorKind::Interrupted - || SIGWINCH.load(atomic::Ordering::Relaxed) - { + if error.kind() != ErrorKind::Interrupted || SIGWINCH.load(Ordering::Relaxed) { return Err(error); } } else { @@ -135,6 +157,7 @@ pub struct PosixRawReader { buf: [u8; 1], parser: Parser, receiver: Utf8, + key_map: PosixKeyMap, } struct Utf8 { @@ -142,9 +165,35 @@ struct Utf8 { valid: bool, } +const UP: char = 'A'; // kcuu1, kUP* +const DOWN: char = 'B'; // kcud1, kDN* +const RIGHT: char = 'C'; // kcuf1, kRIT* +const LEFT: char = 'D'; // kcub1, kLFT* +const END: char = 'F'; // kend* +const HOME: char = 'H'; // khom* +const INSERT: char = '2'; // kic* +const DELETE: char = '3'; // kdch1, kDC* +const PAGE_UP: char = '5'; // kpp, kPRV* +const PAGE_DOWN: char = '6'; // knp, kNXT* + +const RXVT_HOME: char = '7'; +const RXVT_END: char = '8'; + +const SHIFT: char = '2'; +const ALT: char = '3'; +const ALT_SHIFT: char = '4'; +const CTRL: char = '5'; +const CTRL_SHIFT: char = '6'; +const CTRL_ALT: char = '7'; +const CTRL_ALT_SHIFT: char = '8'; + +const RXVT_SHIFT: char = '$'; +const RXVT_CTRL: char = '\x1e'; +const RXVT_CTRL_SHIFT: char = '@'; + impl PosixRawReader { - fn new(config: &Config) -> Result { - Ok(Self { + fn new(config: &Config, key_map: PosixKeyMap) -> Self { + Self { stdin: StdinRaw {}, timeout_ms: config.keyseq_timeout(), buf: [0; 1], @@ -153,37 +202,75 @@ impl PosixRawReader { c: None, valid: true, }, - }) + key_map, + } + } + + /// Handle \E sequences + // https://invisible-island.net/xterm/xterm-function-keys.html + fn escape_sequence(&mut self) -> Result { + self._do_escape_sequence(true) } - /// Handle ESC sequences - fn escape_sequence(&mut self) -> Result { + /// Don't call directly, call `PosixRawReader::escape_sequence` instead + fn _do_escape_sequence(&mut self, allow_recurse: bool) -> Result { // Read the next byte representing the escape sequence. let seq1 = self.next_char()?; if seq1 == '[' { - // ESC [ sequences. (CSI) + // \E[ sequences. (CSI) self.escape_csi() } else if seq1 == 'O' { // xterm - // ESC O sequences. (SS3) + // \EO sequences. (SS3) self.escape_o() } else if seq1 == '\x1b' { - // ESC ESC - Ok(KeyPress::Esc) + // \E\E — used by rxvt, iTerm (under default config), etc. + // ``` + // \E\E[A => Alt-Up + // \E\E[B => Alt-Down + // \E\E[C => Alt-Right + // \E\E[D => Alt-Left + // ``` + // + // In general this more or less works just adding ALT to an existing + // key, but has a wrinkle in that `ESC ESC` without anything + // following should be interpreted as the the escape key. + // + // We handle this by polling to see if there's anything coming + // within our timeout, and if so, recursing once, but adding alt to + // what we read. + if !allow_recurse { + return Ok(E::ESC); + } + let timeout = if self.timeout_ms < 0 { + 100 + } else { + self.timeout_ms + }; + match self.poll(timeout) { + // Ignore poll errors, it's very likely we'll pick them up on + // the next read anyway. + Ok(0) | Err(_) => Ok(E::ESC), + Ok(n) => { + debug_assert!(n > 0, "{}", n); + // recurse, and add the alt modifier. + let E(k, m) = self._do_escape_sequence(false)?; + Ok(E(k, m | M::ALT)) + } + } } else { - // TODO ESC-R (r): Undo all changes made to this line. - Ok(KeyPress::Meta(seq1)) + Ok(E::alt(seq1)) } } - /// Handle ESC [ escape sequences - fn escape_csi(&mut self) -> Result { + /// Handle \E[ escape sequences + fn escape_csi(&mut self) -> Result { let seq2 = self.next_char()?; if seq2.is_digit(10) { match seq2 { '0' | '9' => { - debug!(target: "rustyline", "unsupported esc sequence: ESC [ {:?}", seq2); - Ok(KeyPress::UnknownEscSeq) + debug!(target: "rustyline", "unsupported esc sequence: \\E[{:?}", seq2); + Ok(E(K::UnknownEscSeq, M::NONE)) } _ => { // Extended escape, read additional byte. @@ -194,72 +281,85 @@ impl PosixRawReader { let seq3 = self.next_char()?; // Linux console Ok(match seq3 { - 'A' => KeyPress::F(1), - 'B' => KeyPress::F(2), - 'C' => KeyPress::F(3), - 'D' => KeyPress::F(4), - 'E' => KeyPress::F(5), + 'A' => E(K::F(1), M::NONE), + 'B' => E(K::F(2), M::NONE), + 'C' => E(K::F(3), M::NONE), + 'D' => E(K::F(4), M::NONE), + 'E' => E(K::F(5), M::NONE), _ => { - debug!(target: "rustyline", "unsupported esc sequence: ESC [ [ {:?}", seq3); - KeyPress::UnknownEscSeq + debug!(target: "rustyline", "unsupported esc sequence: \\E[[{:?}", seq3); + E(K::UnknownEscSeq, M::NONE) } }) } 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 - 'Z' => KeyPress::BackTab, + UP => E(K::Up, M::NONE), + DOWN => E(K::Down, M::NONE), + RIGHT => E(K::Right, M::NONE), + LEFT => E(K::Left, M::NONE), + //'E' => E(K::, M::), // Ignore + END => E(K::End, M::NONE), + //'G' => E(K::, M::), // Ignore + HOME => E(K::Home, M::NONE), // khome + //'J' => E(K::, M::), // clr_eos + //'K' => E(K::, M::), // clr_eol + //'L' => E(K::, M::), // il1 + //'M' => E(K::, M::), // kmous + //'P' => E(K::Delete, M::NONE), // dch1 + 'Z' => E(K::BackTab, M::NONE), + 'a' => E(K::Up, M::SHIFT), // rxvt: kind or kUP + 'b' => E(K::Down, M::SHIFT), // rxvt: kri or kDN + 'c' => E(K::Right, M::SHIFT), // rxvt + 'd' => E(K::Left, M::SHIFT), // rxvt _ => { - debug!(target: "rustyline", "unsupported esc sequence: ESC [ {:?}", seq2); - KeyPress::UnknownEscSeq + debug!(target: "rustyline", "unsupported esc sequence: \\E[{:?}", seq2); + E(K::UnknownEscSeq, M::NONE) } }) } } - /// Handle ESC [ escape sequences + /// Handle \E[ escape sequences #[allow(clippy::cognitive_complexity)] - fn extended_escape(&mut self, seq2: char) -> Result { + fn extended_escape(&mut self, seq2: char) -> Result { let seq3 = 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 + '1' | RXVT_HOME => E(K::Home, M::NONE), // tmux, xrvt + INSERT => E(K::Insert, M::NONE), + DELETE => E(K::Delete, M::NONE), + '4' | RXVT_END => E(K::End, M::NONE), // tmux, xrvt + PAGE_UP => E(K::PageUp, M::NONE), + PAGE_DOWN => E(K::PageDown, M::NONE), _ => { debug!(target: "rustyline", - "unsupported esc sequence: ESC [ {} ~", seq2); - KeyPress::UnknownEscSeq + "unsupported esc sequence: \\E[{}~", seq2); + E(K::UnknownEscSeq, M::NONE) } }) } else if seq3.is_digit(10) { let seq4 = 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 + ('1', '1') => E(K::F(1), M::NONE), // rxvt-unicode + ('1', '2') => E(K::F(2), M::NONE), // rxvt-unicode + ('1', '3') => E(K::F(3), M::NONE), // rxvt-unicode + ('1', '4') => E(K::F(4), M::NONE), // rxvt-unicode + ('1', '5') => E(K::F(5), M::NONE), // kf5 + ('1', '7') => E(K::F(6), M::NONE), // kf6 + ('1', '8') => E(K::F(7), M::NONE), // kf7 + ('1', '9') => E(K::F(8), M::NONE), // kf8 + ('2', '0') => E(K::F(9), M::NONE), // kf9 + ('2', '1') => E(K::F(10), M::NONE), // kf10 + ('2', '3') => E(K::F(11), M::NONE), // kf11 + ('2', '4') => E(K::F(12), M::NONE), // kf12 + //('6', '2') => KeyCode::ScrollUp, + //('6', '3') => KeyCode::ScrollDown, _ => { debug!(target: "rustyline", - "unsupported esc sequence: ESC [ {}{} ~", seq2, seq3); - KeyPress::UnknownEscSeq + "unsupported esc sequence: \\E[{}{}~", seq2, seq3); + E(K::UnknownEscSeq, M::NONE) } }) } else if seq4 == ';' { @@ -268,37 +368,63 @@ impl PosixRawReader { let seq6 = self.next_char()?; if seq6.is_digit(10) { self.next_char()?; // 'R' expected + Ok(E(K::UnknownEscSeq, M::NONE)) } else if seq6 == 'R' { + Ok(E(K::UnknownEscSeq, M::NONE)) + } else if seq6 == '~' { + Ok(match (seq2, seq3, seq5) { + ('1', '5', CTRL) => E(K::F(5), M::CTRL), + //('1', '5', '6') => E(K::F(17), M::CTRL), + ('1', '7', CTRL) => E(K::F(6), M::CTRL), + //('1', '7', '6') => E(K::F(18), M::CTRL), + ('1', '8', CTRL) => E(K::F(7), M::CTRL), + ('1', '9', CTRL) => E(K::F(8), M::CTRL), + //('1', '9', '6') => E(K::F(19), M::CTRL), + ('2', '0', CTRL) => E(K::F(9), M::CTRL), + //('2', '0', '6') => E(K::F(21), M::CTRL), + ('2', '1', CTRL) => E(K::F(10), M::CTRL), + //('2', '1', '6') => E(K::F(22), M::CTRL), + ('2', '3', CTRL) => E(K::F(11), M::CTRL), + //('2', '3', '6') => E(K::F(23), M::CTRL), + ('2', '4', CTRL) => E(K::F(12), M::CTRL), + //('2', '4', '6') => E(K::F(24), M::CTRL), + _ => { + debug!(target: "rustyline", + "unsupported esc sequence: \\E[{}{};{}~", seq2, seq3, seq5); + E(K::UnknownEscSeq, M::NONE) + } + }) } else { debug!(target: "rustyline", - "unsupported esc sequence: ESC [ {}{} ; {} {}", seq2, seq3, seq5, seq6); + "unsupported esc sequence: \\E[{}{};{}{}", seq2, seq3, seq5, seq6); + Ok(E(K::UnknownEscSeq, M::NONE)) } } else { debug!(target: "rustyline", - "unsupported esc sequence: ESC [ {}{} ; {:?}", seq2, seq3, seq5); + "unsupported esc sequence: \\E[{}{};{:?}", seq2, seq3, seq5); + Ok(E(K::UnknownEscSeq, M::NONE)) } - Ok(KeyPress::UnknownEscSeq) } else if seq4.is_digit(10) { let seq5 = self.next_char()?; if seq5 == '~' { Ok(match (seq2, seq3, seq4) { - ('2', '0', '0') => KeyPress::BracketedPasteStart, - ('2', '0', '1') => KeyPress::BracketedPasteEnd, + ('2', '0', '0') => E(K::BracketedPasteStart, M::NONE), + ('2', '0', '1') => E(K::BracketedPasteEnd, M::NONE), _ => { debug!(target: "rustyline", - "unsupported esc sequence: ESC [ {}{}{}~", seq2, seq3, seq4); - KeyPress::UnknownEscSeq + "unsupported esc sequence: \\E[{}{}{}~", seq2, seq3, seq4); + E(K::UnknownEscSeq, M::NONE) } }) } else { debug!(target: "rustyline", - "unsupported esc sequence: ESC [ {}{}{} {}", seq2, seq3, seq4, seq5); - Ok(KeyPress::UnknownEscSeq) + "unsupported esc sequence: \\E[{}{}{}{}", seq2, seq3, seq4, seq5); + Ok(E(K::UnknownEscSeq, M::NONE)) } } else { debug!(target: "rustyline", - "unsupported esc sequence: ESC [ {}{} {:?}", seq2, seq3, seq4); - Ok(KeyPress::UnknownEscSeq) + "unsupported esc sequence: \\E[{}{}{:?}", seq2, seq3, seq4); + Ok(E(K::UnknownEscSeq, M::NONE)) } } else if seq3 == ';' { let seq4 = self.next_char()?; @@ -306,85 +432,243 @@ impl PosixRawReader { let seq5 = self.next_char()?; if seq5.is_digit(10) { self.next_char()?; // 'R' expected - Ok(KeyPress::UnknownEscSeq) + //('1', '0', UP) => E(K::, M::), // Alt + Shift + Up + Ok(E(K::UnknownEscSeq, M::NONE)) } else 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, + (SHIFT, UP) => E(K::Up, M::SHIFT), // ~ key_sr + (SHIFT, DOWN) => E(K::Down, M::SHIFT), // ~ key_sf + (SHIFT, RIGHT) => E(K::Right, M::SHIFT), + (SHIFT, LEFT) => E(K::Left, M::SHIFT), + (SHIFT, END) => E(K::End, M::SHIFT), // kEND + (SHIFT, HOME) => E(K::Home, M::SHIFT), // kHOM + //('2', 'P') => E(K::F(13), M::NONE), + //('2', 'Q') => E(K::F(14), M::NONE), + //('2', 'S') => E(K::F(16), M::NONE), + (ALT, UP) => E(K::Up, M::ALT), + (ALT, DOWN) => E(K::Down, M::ALT), + (ALT, RIGHT) => E(K::Right, M::ALT), + (ALT, LEFT) => E(K::Left, M::ALT), + (ALT, END) => E(K::End, M::ALT), + (ALT, HOME) => E(K::Home, M::ALT), + (ALT_SHIFT, UP) => E(K::Up, M::ALT_SHIFT), + (ALT_SHIFT, DOWN) => E(K::Down, M::ALT_SHIFT), + (ALT_SHIFT, RIGHT) => E(K::Right, M::ALT_SHIFT), + (ALT_SHIFT, LEFT) => E(K::Left, M::ALT_SHIFT), + (ALT_SHIFT, END) => E(K::End, M::ALT_SHIFT), + (ALT_SHIFT, HOME) => E(K::Home, M::ALT_SHIFT), + (CTRL, UP) => E(K::Up, M::CTRL), + (CTRL, DOWN) => E(K::Down, M::CTRL), + (CTRL, RIGHT) => E(K::Right, M::CTRL), + (CTRL, LEFT) => E(K::Left, M::CTRL), + (CTRL, END) => E(K::End, M::CTRL), + (CTRL, HOME) => E(K::Home, M::CTRL), + (CTRL, 'P') => E(K::F(1), M::CTRL), + (CTRL, 'Q') => E(K::F(2), M::CTRL), + (CTRL, 'S') => E(K::F(4), M::CTRL), + (CTRL, 'p') => E(K::Char('0'), M::CTRL), + (CTRL, 'q') => E(K::Char('1'), M::CTRL), + (CTRL, 'r') => E(K::Char('2'), M::CTRL), + (CTRL, 's') => E(K::Char('3'), M::CTRL), + (CTRL, 't') => E(K::Char('4'), M::CTRL), + (CTRL, 'u') => E(K::Char('5'), M::CTRL), + (CTRL, 'v') => E(K::Char('6'), M::CTRL), + (CTRL, 'w') => E(K::Char('7'), M::CTRL), + (CTRL, 'x') => E(K::Char('8'), M::CTRL), + (CTRL, 'y') => E(K::Char('9'), M::CTRL), + (CTRL_SHIFT, UP) => E(K::Up, M::CTRL_SHIFT), + (CTRL_SHIFT, DOWN) => E(K::Down, M::CTRL_SHIFT), + (CTRL_SHIFT, RIGHT) => E(K::Right, M::CTRL_SHIFT), + (CTRL_SHIFT, LEFT) => E(K::Left, M::CTRL_SHIFT), + (CTRL_SHIFT, END) => E(K::End, M::CTRL_SHIFT), + (CTRL_SHIFT, HOME) => E(K::Home, M::CTRL_SHIFT), + //('6', 'P') => E(K::F(13), M::CTRL), + //('6', 'Q') => E(K::F(14), M::CTRL), + //('6', 'S') => E(K::F(16), M::CTRL), + (CTRL_SHIFT, 'p') => E(K::Char('0'), M::CTRL_SHIFT), + (CTRL_SHIFT, 'q') => E(K::Char('1'), M::CTRL_SHIFT), + (CTRL_SHIFT, 'r') => E(K::Char('2'), M::CTRL_SHIFT), + (CTRL_SHIFT, 's') => E(K::Char('3'), M::CTRL_SHIFT), + (CTRL_SHIFT, 't') => E(K::Char('4'), M::CTRL_SHIFT), + (CTRL_SHIFT, 'u') => E(K::Char('5'), M::CTRL_SHIFT), + (CTRL_SHIFT, 'v') => E(K::Char('6'), M::CTRL_SHIFT), + (CTRL_SHIFT, 'w') => E(K::Char('7'), M::CTRL_SHIFT), + (CTRL_SHIFT, 'x') => E(K::Char('8'), M::CTRL_SHIFT), + (CTRL_SHIFT, 'y') => E(K::Char('9'), M::CTRL_SHIFT), + (CTRL_ALT, UP) => E(K::Up, M::CTRL_ALT), + (CTRL_ALT, DOWN) => E(K::Down, M::CTRL_ALT), + (CTRL_ALT, RIGHT) => E(K::Right, M::CTRL_ALT), + (CTRL_ALT, LEFT) => E(K::Left, M::CTRL_ALT), + (CTRL_ALT, END) => E(K::End, M::CTRL_ALT), + (CTRL_ALT, HOME) => E(K::Home, M::CTRL_ALT), + (CTRL_ALT, 'p') => E(K::Char('0'), M::CTRL_ALT), + (CTRL_ALT, 'q') => E(K::Char('1'), M::CTRL_ALT), + (CTRL_ALT, 'r') => E(K::Char('2'), M::CTRL_ALT), + (CTRL_ALT, 's') => E(K::Char('3'), M::CTRL_ALT), + (CTRL_ALT, 't') => E(K::Char('4'), M::CTRL_ALT), + (CTRL_ALT, 'u') => E(K::Char('5'), M::CTRL_ALT), + (CTRL_ALT, 'v') => E(K::Char('6'), M::CTRL_ALT), + (CTRL_ALT, 'w') => E(K::Char('7'), M::CTRL_ALT), + (CTRL_ALT, 'x') => E(K::Char('8'), M::CTRL_ALT), + (CTRL_ALT, 'y') => E(K::Char('9'), M::CTRL_ALT), + (CTRL_ALT_SHIFT, UP) => E(K::Up, M::CTRL_ALT_SHIFT), + (CTRL_ALT_SHIFT, DOWN) => E(K::Down, M::CTRL_ALT_SHIFT), + (CTRL_ALT_SHIFT, RIGHT) => E(K::Right, M::CTRL_ALT_SHIFT), + (CTRL_ALT_SHIFT, LEFT) => E(K::Left, M::CTRL_ALT_SHIFT), + (CTRL_ALT_SHIFT, END) => E(K::End, M::CTRL_ALT_SHIFT), + (CTRL_ALT_SHIFT, HOME) => E(K::Home, M::CTRL_ALT_SHIFT), + (CTRL_ALT_SHIFT, 'p') => E(K::Char('0'), M::CTRL_ALT_SHIFT), + (CTRL_ALT_SHIFT, 'q') => E(K::Char('1'), M::CTRL_ALT_SHIFT), + (CTRL_ALT_SHIFT, 'r') => E(K::Char('2'), M::CTRL_ALT_SHIFT), + (CTRL_ALT_SHIFT, 's') => E(K::Char('3'), M::CTRL_ALT_SHIFT), + (CTRL_ALT_SHIFT, 't') => E(K::Char('4'), M::CTRL_ALT_SHIFT), + (CTRL_ALT_SHIFT, 'u') => E(K::Char('5'), M::CTRL_ALT_SHIFT), + (CTRL_ALT_SHIFT, 'v') => E(K::Char('6'), M::CTRL_ALT_SHIFT), + (CTRL_ALT_SHIFT, 'w') => E(K::Char('7'), M::CTRL_ALT_SHIFT), + (CTRL_ALT_SHIFT, 'x') => E(K::Char('8'), M::CTRL_ALT_SHIFT), + (CTRL_ALT_SHIFT, 'y') => E(K::Char('9'), M::CTRL_ALT_SHIFT), + // Meta + arrow on (some?) Macs when using iTerm defaults + ('9', UP) => E(K::Up, M::ALT), + ('9', DOWN) => E(K::Down, M::ALT), + ('9', RIGHT) => E(K::Right, M::ALT), + ('9', LEFT) => E(K::Left, M::ALT), + _ => { + debug!(target: "rustyline", + "unsupported esc sequence: \\E[1;{}{:?}", seq4, seq5); + E(K::UnknownEscSeq, M::NONE) + } + }) + } else if seq5 == '~' { + Ok(match (seq2, seq4) { + (INSERT, SHIFT) => E(K::Insert, M::SHIFT), + (INSERT, ALT) => E(K::Insert, M::ALT), + (INSERT, ALT_SHIFT) => E(K::Insert, M::ALT_SHIFT), + (INSERT, CTRL) => E(K::Insert, M::CTRL), + (INSERT, CTRL_SHIFT) => E(K::Insert, M::CTRL_SHIFT), + (INSERT, CTRL_ALT) => E(K::Insert, M::CTRL_ALT), + (INSERT, CTRL_ALT_SHIFT) => E(K::Insert, M::CTRL_ALT_SHIFT), + (DELETE, SHIFT) => E(K::Delete, M::SHIFT), + (DELETE, ALT) => E(K::Delete, M::ALT), + (DELETE, ALT_SHIFT) => E(K::Delete, M::ALT_SHIFT), + (DELETE, CTRL) => E(K::Delete, M::CTRL), + (DELETE, CTRL_SHIFT) => E(K::Delete, M::CTRL_SHIFT), + (DELETE, CTRL_ALT) => E(K::Delete, M::CTRL_ALT), + (DELETE, CTRL_ALT_SHIFT) => E(K::Delete, M::CTRL_ALT_SHIFT), + (PAGE_UP, SHIFT) => E(K::PageUp, M::SHIFT), + (PAGE_UP, ALT) => E(K::PageUp, M::ALT), + (PAGE_UP, ALT_SHIFT) => E(K::PageUp, M::ALT_SHIFT), + (PAGE_UP, CTRL) => E(K::PageUp, M::CTRL), + (PAGE_UP, CTRL_SHIFT) => E(K::PageUp, M::CTRL_SHIFT), + (PAGE_UP, CTRL_ALT) => E(K::PageUp, M::CTRL_ALT), + (PAGE_UP, CTRL_ALT_SHIFT) => E(K::PageUp, M::CTRL_ALT_SHIFT), + (PAGE_DOWN, SHIFT) => E(K::PageDown, M::SHIFT), + (PAGE_DOWN, ALT) => E(K::PageDown, M::ALT), + (PAGE_DOWN, ALT_SHIFT) => E(K::PageDown, M::ALT_SHIFT), + (PAGE_DOWN, CTRL) => E(K::PageDown, M::CTRL), + (PAGE_DOWN, CTRL_SHIFT) => E(K::PageDown, M::CTRL_SHIFT), + (PAGE_DOWN, CTRL_ALT) => E(K::PageDown, M::CTRL_ALT), + (PAGE_DOWN, CTRL_ALT_SHIFT) => E(K::PageDown, M::CTRL_ALT_SHIFT), _ => { debug!(target: "rustyline", - "unsupported esc sequence: ESC [ 1 ; {} {:?}", seq4, seq5); - KeyPress::UnknownEscSeq + "unsupported esc sequence: \\E[{};{:?}~", seq2, seq4); + E(K::UnknownEscSeq, M::NONE) } }) } else { debug!(target: "rustyline", - "unsupported esc sequence: ESC [ {} ; {} {:?}", seq2, seq4, seq5); - Ok(KeyPress::UnknownEscSeq) + "unsupported esc sequence: \\E[{};{}{:?}", seq2, seq4, seq5); + Ok(E(K::UnknownEscSeq, M::NONE)) } } else { debug!(target: "rustyline", - "unsupported esc sequence: ESC [ {} ; {:?}", seq2, seq4); - Ok(KeyPress::UnknownEscSeq) + "unsupported esc sequence: \\E[{};{:?}", seq2, seq4); + Ok(E(K::UnknownEscSeq, M::NONE)) } } else { Ok(match (seq2, seq3) { - ('5', 'A') => KeyPress::ControlUp, - ('5', 'B') => KeyPress::ControlDown, - ('5', 'C') => KeyPress::ControlRight, - ('5', 'D') => KeyPress::ControlLeft, + (DELETE, RXVT_CTRL) => E(K::Delete, M::CTRL), + (DELETE, RXVT_CTRL_SHIFT) => E(K::Delete, M::CTRL_SHIFT), + (CTRL, UP) => E(K::Up, M::CTRL), + (CTRL, DOWN) => E(K::Down, M::CTRL), + (CTRL, RIGHT) => E(K::Right, M::CTRL), + (CTRL, LEFT) => E(K::Left, M::CTRL), + (PAGE_UP, RXVT_CTRL) => E(K::PageUp, M::CTRL), + (PAGE_UP, RXVT_SHIFT) => E(K::PageUp, M::SHIFT), + (PAGE_UP, RXVT_CTRL_SHIFT) => E(K::PageUp, M::CTRL_SHIFT), + (PAGE_DOWN, RXVT_CTRL) => E(K::PageDown, M::CTRL), + (PAGE_DOWN, RXVT_SHIFT) => E(K::PageDown, M::SHIFT), + (PAGE_DOWN, RXVT_CTRL_SHIFT) => E(K::PageDown, M::CTRL_SHIFT), + (RXVT_HOME, RXVT_CTRL) => E(K::Home, M::CTRL), + (RXVT_HOME, RXVT_SHIFT) => E(K::Home, M::SHIFT), + (RXVT_HOME, RXVT_CTRL_SHIFT) => E(K::Home, M::CTRL_SHIFT), + (RXVT_END, RXVT_CTRL) => E(K::End, M::CTRL), // kEND5 or kel + (RXVT_END, RXVT_SHIFT) => E(K::End, M::SHIFT), + (RXVT_END, RXVT_CTRL_SHIFT) => E(K::End, M::CTRL_SHIFT), _ => { debug!(target: "rustyline", - "unsupported esc sequence: ESC [ {} {:?}", seq2, seq3); - KeyPress::UnknownEscSeq + "unsupported esc sequence: \\E[{}{:?}", seq2, seq3); + E(K::UnknownEscSeq, M::NONE) } }) } } - /// Handle ESC O escape sequences - fn escape_o(&mut self) -> Result { + /// Handle \EO escape sequences + fn escape_o(&mut self) -> Result { let seq2 = 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, // rxvt - 'd' => KeyPress::ControlLeft, // rxvt + UP => E(K::Up, M::NONE), + DOWN => E(K::Down, M::NONE), + RIGHT => E(K::Right, M::NONE), + LEFT => E(K::Left, M::NONE), + //'E' => E(K::, M::),// key_b2, kb2 + END => E(K::End, M::NONE), // kend + HOME => E(K::Home, M::NONE), // khome + 'M' => E::ENTER, // kent + 'P' => E(K::F(1), M::NONE), // kf1 + 'Q' => E(K::F(2), M::NONE), // kf2 + 'R' => E(K::F(3), M::NONE), // kf3 + 'S' => E(K::F(4), M::NONE), // kf4 + 'a' => E(K::Up, M::CTRL), + 'b' => E(K::Down, M::CTRL), + 'c' => E(K::Right, M::CTRL), // rxvt + 'd' => E(K::Left, M::CTRL), // rxvt + 'l' => E(K::F(8), M::NONE), + 't' => E(K::F(5), M::NONE), // kf5 or kb1 + 'u' => E(K::F(6), M::NONE), // kf6 or kb2 + 'v' => E(K::F(7), M::NONE), // kf7 or kb3 + 'w' => E(K::F(9), M::NONE), // kf9 or ka1 + 'x' => E(K::F(10), M::NONE), // kf10 or ka2 _ => { - debug!(target: "rustyline", "unsupported esc sequence: ESC O {:?}", seq2); - KeyPress::UnknownEscSeq + debug!(target: "rustyline", "unsupported esc sequence: \\EO{:?}", seq2); + E(K::UnknownEscSeq, M::NONE) } }) } fn poll(&mut self, timeout_ms: i32) -> ::nix::Result { let mut fds = [poll::PollFd::new(STDIN_FILENO, PollFlags::POLLIN)]; - poll::poll(&mut fds, timeout_ms) + let r = poll::poll(&mut fds, timeout_ms); + match r { + Ok(_) => r, + Err(nix::errno::Errno::EINTR) => { + if SIGWINCH.load(Ordering::Relaxed) { + r + } else { + Ok(0) // Ignore EINTR while polling + } + } + Err(_) => r, + } } } impl RawReader for PosixRawReader { - fn next_key(&mut self, single_esc_abort: bool) -> Result { + fn next_key(&mut self, single_esc_abort: bool) -> Result { let c = self.next_char()?; - let mut key = keys::char_to_key_press(c); - if key == KeyPress::Esc { + let mut key = KeyEvent::new(c, M::NONE); + if key == E::ESC { let timeout_ms = if single_esc_abort && self.timeout_ms == -1 { 0 } else { @@ -402,7 +686,7 @@ impl RawReader for PosixRawReader { Err(e) => return Err(e.into()), } } - debug!(target: "rustyline", "key: {:?}", key); + debug!(target: "rustyline", "c: {:?} => key: {:?}", c, key); Ok(key) } @@ -428,7 +712,7 @@ impl RawReader for PosixRawReader { match self.next_char()? { '\x1b' => { let key = self.escape_sequence()?; - if key == KeyPress::BracketedPasteEnd { + if key == E(K::BracketedPasteEnd, M::NONE) { break; } else { continue; // TODO validate @@ -438,9 +722,17 @@ impl RawReader for PosixRawReader { }; } let buffer = buffer.replace("\r\n", "\n"); - let buffer = buffer.replace("\r", "\n"); + let buffer = buffer.replace('\r', "\n"); Ok(buffer) } + + fn find_binding(&self, key: &KeyEvent) -> Option { + let cmd = self.key_map.get(key).cloned(); + if let Some(ref cmd) = cmd { + debug!(target: "rustyline", "terminal key binding: {:?} => {:?}", key, cmd); + } + cmd + } } impl Receiver for Utf8 { @@ -484,6 +776,25 @@ impl PosixRenderer { bell_style, } } + + fn clear_old_rows(&mut self, layout: &Layout) { + use std::fmt::Write; + let current_row = layout.cursor.row; + let old_rows = layout.end.row; + // old_rows < cursor_row if the prompt spans multiple lines and if + // this is the default State. + let cursor_row_movement = old_rows.saturating_sub(current_row); + // move the cursor down as required + if cursor_row_movement > 0 { + write!(self.buffer, "\x1b[{}B", cursor_row_movement).unwrap(); + } + // clear old rows + for _ in 0..old_rows { + self.buffer.push_str("\r\x1b[0K\x1b[A"); + } + // clear the line + self.buffer.push_str("\r\x1b[0K"); + } } impl Renderer for PosixRenderer { @@ -493,7 +804,7 @@ impl Renderer for PosixRenderer { use std::fmt::Write; self.buffer.clear(); let row_ordering = new.row.cmp(&old.row); - if row_ordering == Ordering::Greater { + if row_ordering == cmp::Ordering::Greater { // move down let row_shift = new.row - old.row; if row_shift == 1 { @@ -501,7 +812,7 @@ impl Renderer for PosixRenderer { } else { write!(self.buffer, "\x1b[{}B", row_shift).unwrap(); } - } else if row_ordering == Ordering::Less { + } else if row_ordering == cmp::Ordering::Less { // move up let row_shift = old.row - new.row; if row_shift == 1 { @@ -511,7 +822,7 @@ impl Renderer for PosixRenderer { } } let col_ordering = new.col.cmp(&old.col); - if col_ordering == Ordering::Greater { + if col_ordering == cmp::Ordering::Greater { // move right let col_shift = new.col - old.col; if col_shift == 1 { @@ -519,7 +830,7 @@ impl Renderer for PosixRenderer { } else { write!(self.buffer, "\x1b[{}C", col_shift).unwrap(); } - } else if col_ordering == Ordering::Less { + } else if col_ordering == cmp::Ordering::Less { // move left let col_shift = old.col - new.col; if col_shift == 1 { @@ -533,7 +844,7 @@ impl Renderer for PosixRenderer { fn refresh_line( &mut self, - prompt: &str, + prompt: &Prompt, line: &LineBuffer, hint: Option<&str>, old_layout: &Layout, @@ -543,39 +854,13 @@ impl Renderer for PosixRenderer { use std::fmt::Write; self.buffer.clear(); - let default_prompt = new_layout.default_prompt; let cursor = new_layout.cursor; let end_pos = new_layout.end; - let current_row = old_layout.cursor.row; - let old_rows = old_layout.end.row; - // old_rows < cursor.row if the prompt spans multiple lines and if - // this is the default State. - let cursor_row_movement = old_rows.saturating_sub(current_row); - // move the cursor down as required - if cursor_row_movement > 0 { - write!(self.buffer, "\x1b[{}B", cursor_row_movement).unwrap(); - } - // clear old rows - for _ in 0..old_rows { - self.buffer.push_str("\r\x1b[0K\x1b[A"); - } - // clear the line - self.buffer.push_str("\r\x1b[0K"); + self.clear_old_rows(old_layout); - if let Some(highlighter) = highlighter { - // display the prompt - self.buffer - .push_str(&highlighter.highlight_prompt(prompt, default_prompt)); - // display the input line - self.buffer - .push_str(&highlighter.highlight(line, line.pos())); - } else { - // display the prompt - self.buffer.push_str(prompt); - // display the input line - self.buffer.push_str(line); - } + add_prompt_and_highlight(|s| self.buffer.push_str(s), + highlighter, line, prompt); // display hint if let Some(hint) = hint { if let Some(highlighter) = highlighter { @@ -585,8 +870,13 @@ impl Renderer for PosixRenderer { } } // we have to generate our own newline on line wrap - if end_pos.col == 0 && end_pos.row > 0 { - self.buffer.push_str("\n"); + if end_pos.col == 0 + && end_pos.row > 0 + && !hint + .map(|h| h.ends_with('\n')) + .unwrap_or_else(|| line.ends_with('\n')) + { + self.buffer.push('\n'); } // position the cursor let new_cursor_row_movement = end_pos.row - cursor.row; @@ -612,13 +902,15 @@ impl Renderer for PosixRenderer { /// Control characters are treated as having zero width. /// Characters with 2 column width are correctly handled (not split). - fn calculate_position(&self, s: &str, orig: Position) -> Position { + fn calculate_position(&self, s: &str, orig: Position, left_margin: usize) + -> 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; + pos.col = left_margin; continue; } let cw = if c == "\t" { @@ -657,7 +949,9 @@ impl Renderer for PosixRenderer { /// Check if a SIGWINCH signal has been received fn sigwinch(&self) -> bool { - SIGWINCH.compare_and_swap(true, false, atomic::Ordering::SeqCst) + SIGWINCH + .compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst) + .unwrap_or(false) } /// Try to update the number of columns in the current terminal, @@ -699,43 +993,13 @@ impl Renderer for PosixRenderer { } let col = read_digits_until(rdr, 'R')?; debug!(target: "rustyline", "initial cursor location: {:?}", col); - if col.is_some() && col != Some(1) { + if col != Some(1) { self.write_and_flush(b"\n")?; } Ok(()) } } -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() - } -} - fn read_digits_until(rdr: &mut PosixRawReader, sep: char) -> Result> { let mut num: u32 = 0; loop { @@ -754,7 +1018,7 @@ fn read_digits_until(rdr: &mut PosixRawReader, sep: char) -> Result> } static SIGWINCH_ONCE: sync::Once = sync::Once::new(); -static SIGWINCH: atomic::AtomicBool = atomic::AtomicBool::new(false); +static SIGWINCH: AtomicBool = AtomicBool::new(false); fn install_sigwinch_handler() { SIGWINCH_ONCE.call_once(|| unsafe { @@ -768,10 +1032,17 @@ fn install_sigwinch_handler() { } extern "C" fn sigwinch_handler(_: libc::c_int) { - SIGWINCH.store(true, atomic::Ordering::SeqCst); + SIGWINCH.store(true, Ordering::SeqCst); debug!(target: "rustyline", "SIGWINCH"); } +fn map_key(key_map: &mut HashMap, raw: &Termios, index: SCI, name: &str, cmd: Cmd) { + let cc = char::from(raw.control_chars[index as usize]); + let key = KeyEvent::new(cc, M::NONE); + debug!(target: "rustyline", "{}: {:?}", name, key); + key_map.insert(key, cmd); +} + #[cfg(not(test))] pub type Terminal = PosixTerminal; @@ -784,6 +1055,7 @@ pub struct PosixTerminal { stream_type: OutputStreamType, tab_stop: usize, bell_style: BellStyle, + enable_bracketed_paste: bool, } impl PosixTerminal { @@ -797,6 +1069,7 @@ impl PosixTerminal { } impl Term for PosixTerminal { + type KeyMap = PosixKeyMap; type Mode = PosixMode; type Reader = PosixRawReader; type Writer = PosixRenderer; @@ -806,6 +1079,7 @@ impl Term for PosixTerminal { stream_type: OutputStreamType, tab_stop: usize, bell_style: BellStyle, + enable_bracketed_paste: bool, ) -> Self { let term = Self { unsupported: is_unsupported_term(), @@ -815,6 +1089,7 @@ impl Term for PosixTerminal { stream_type, tab_stop, bell_style, + enable_bracketed_paste, }; if !term.unsupported && term.stdin_isatty && term.stdstream_isatty { install_sigwinch_handler(); @@ -841,11 +1116,11 @@ impl Term for PosixTerminal { // Interactive loop: - fn enable_raw_mode(&mut self) -> Result { + fn enable_raw_mode(&mut self) -> Result<(Self::Mode, PosixKeyMap)> { use nix::errno::Errno::ENOTTY; - use nix::sys::termios::{ControlFlags, InputFlags, LocalFlags, SpecialCharacterIndices}; + use nix::sys::termios::{ControlFlags, InputFlags, LocalFlags}; if !self.stdin_isatty { - return Err(nix::Error::from_errno(ENOTTY).into()); + return Err(ENOTTY.into()); } let original_mode = termios::tcgetattr(STDIN_FILENO)?; let mut raw = original_mode.clone(); @@ -865,26 +1140,38 @@ impl Term for PosixTerminal { // disable echoing, canonical mode, extended input processing and signals 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 + raw.control_chars[SCI::VMIN as usize] = 1; // One character-at-a-time input + raw.control_chars[SCI::VTIME as usize] = 0; // with blocking read + + let mut key_map: HashMap = HashMap::with_capacity(4); + map_key(&mut key_map, &raw, SCI::VEOF, "VEOF", Cmd::EndOfFile); + map_key(&mut key_map, &raw, SCI::VINTR, "VINTR", Cmd::Interrupt); + map_key(&mut key_map, &raw, SCI::VQUIT, "VQUIT", Cmd::Interrupt); + map_key(&mut key_map, &raw, SCI::VSUSP, "VSUSP", Cmd::Suspend); + termios::tcsetattr(STDIN_FILENO, SetArg::TCSADRAIN, &raw)?; // enable bracketed paste - let out = if let Err(e) = write_and_flush(self.stream_type, BRACKETED_PASTE_ON) { + let out = if !self.enable_bracketed_paste { + None + } else if let Err(e) = write_and_flush(self.stream_type, BRACKETED_PASTE_ON) { debug!(target: "rustyline", "Cannot enable bracketed paste: {}", e); None } else { Some(self.stream_type) }; - Ok(PosixMode { - termios: original_mode, - out, - }) + Ok(( + PosixMode { + termios: original_mode, + out, + }, + key_map, + )) } /// Create a RAW reader - fn create_reader(&self, config: &Config) -> Result { - PosixRawReader::new(config) + fn create_reader(&self, config: &Config, key_map: PosixKeyMap) -> Result { + Ok(PosixRawReader::new(config, key_map)) } fn create_writer(&self) -> PosixRenderer { @@ -923,12 +1210,14 @@ fn write_and_flush(out: OutputStreamType, buf: &[u8]) -> Result<()> { mod test { use super::{Position, PosixRenderer, PosixTerminal, Renderer}; use crate::config::{BellStyle, OutputStreamType}; + use crate::line_buffer::LineBuffer; + use crate::edit::Prompt; #[test] #[ignore] fn prompt_with_ansi_escape_codes() { let out = PosixRenderer::new(OutputStreamType::Stdout, 4, true, BellStyle::default()); - let pos = out.calculate_position("\x1b[1;32m>>\x1b[0m ", Position::default()); + let pos = out.calculate_position("\x1b[1;32m>>\x1b[0m ", Position::default(), 0); assert_eq!(3, pos.col); assert_eq!(0, pos.row); } @@ -936,10 +1225,10 @@ mod test { #[test] fn test_unsupported_term() { ::std::env::set_var("TERM", "xterm"); - assert_eq!(false, super::is_unsupported_term()); + assert!(!super::is_unsupported_term()); ::std::env::set_var("TERM", "dumb"); - assert_eq!(true, super::is_unsupported_term()); + assert!(super::is_unsupported_term()); } #[test] @@ -953,4 +1242,32 @@ mod test { fn assert_sync() {} assert_sync::(); } + + #[test] + fn test_line_wrap() { + let mut out = PosixRenderer::new(OutputStreamType::Stdout, 4, true, BellStyle::default()); + let prompt = Prompt { + text: "> ", + is_default: true, + size: out.calculate_position("> ", Position::default(), 0), + has_continuation: false, + }; + + let mut line = LineBuffer::init("", 0, None); + let old_layout = out.compute_layout(&prompt, &line, None); + assert_eq!(Position { col: 2, row: 0 }, old_layout.cursor); + assert_eq!(old_layout.cursor, old_layout.end); + + assert_eq!(Some(true), line.insert('a', out.cols - prompt.size.col + 1)); + let new_layout = out.compute_layout(&prompt, &line, None); + assert_eq!(Position { col: 1, row: 1 }, new_layout.cursor); + assert_eq!(new_layout.cursor, new_layout.end); + out.refresh_line(&prompt, &line, None, &old_layout, &new_layout, None) + .unwrap(); + #[rustfmt::skip] + assert_eq!( + "\r\u{1b}[0K> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\u{1b}[1C", + out.buffer + ); + } } diff --git a/src/tty/windows.rs b/src/tty/windows.rs index 302504ecc6..20796e3e91 100644 --- a/src/tty/windows.rs +++ b/src/tty/windows.rs @@ -1,22 +1,30 @@ //! Windows specific definitions +#![allow(clippy::try_err)] // suggested fix does not work (cannot infer...) + use std::io::{self, Write}; use std::mem; -use std::sync::atomic; - -use log::debug; -use unicode_width::UnicodeWidthChar; -use winapi::shared::minwindef::{DWORD, WORD}; +use std::sync::atomic::{AtomicBool, Ordering}; + +use log::{debug, warn}; +use unicode_segmentation::UnicodeSegmentation; +use unicode_width::UnicodeWidthStr; +use winapi::shared::minwindef::{BOOL, DWORD, FALSE, TRUE, WORD}; +use winapi::shared::winerror; +use winapi::um::handleapi::INVALID_HANDLE_VALUE; +use winapi::um::wincon::{self, CONSOLE_SCREEN_BUFFER_INFO, COORD}; use winapi::um::winnt::{CHAR, HANDLE}; -use winapi::um::{consoleapi, handleapi, processenv, winbase, wincon, winuser}; +use winapi::um::{consoleapi, processenv, winbase, winuser}; -use super::{RawMode, RawReader, Renderer, Term}; +use super::{width, RawMode, RawReader, Renderer, Term}; use crate::config::{BellStyle, ColorMode, Config, OutputStreamType}; +use crate::edit::Prompt; use crate::error; use crate::highlight::Highlighter; -use crate::keys::{self, KeyPress}; +use crate::keys::{KeyCode as K, KeyEvent, Modifiers as M}; use crate::layout::{Layout, Position}; use crate::line_buffer::LineBuffer; -use crate::Result; +use crate::{error, Cmd, Result}; +use crate::tty::add_prompt_and_highlight; const STDIN_FILENO: DWORD = winbase::STD_INPUT_HANDLE; const STDOUT_FILENO: DWORD = winbase::STD_OUTPUT_HANDLE; @@ -24,7 +32,7 @@ const STDERR_FILENO: DWORD = winbase::STD_ERROR_HANDLE; fn get_std_handle(fd: DWORD) -> Result { let handle = unsafe { processenv::GetStdHandle(fd) }; - if handle == handleapi::INVALID_HANDLE_VALUE { + if handle == INVALID_HANDLE_VALUE { Err(io::Error::last_os_error())?; } else if handle.is_null() { Err(io::Error::new( @@ -35,21 +43,18 @@ fn get_std_handle(fd: DWORD) -> Result { Ok(handle) } -#[macro_export] -macro_rules! check { - ($funcall:expr) => {{ - let rc = unsafe { $funcall }; - if rc == 0 { - Err(io::Error::last_os_error())?; - } - rc - }}; +fn check(rc: BOOL) -> Result<()> { + if rc == FALSE { + Err(io::Error::last_os_error())? + } else { + Ok(()) + } } fn get_win_size(handle: HANDLE) -> (usize, usize) { let mut info = unsafe { mem::zeroed() }; match unsafe { wincon::GetConsoleScreenBufferInfo(handle, &mut info) } { - 0 => (80, 24), + FALSE => (80, 24), _ => ( info.dwSize.X as usize, (1 + info.srWindow.Bottom - info.srWindow.Top) as usize, @@ -59,10 +64,15 @@ fn get_win_size(handle: HANDLE) -> (usize, usize) { fn get_console_mode(handle: HANDLE) -> Result { let mut original_mode = 0; - check!(consoleapi::GetConsoleMode(handle, &mut original_mode)); + check(unsafe { consoleapi::GetConsoleMode(handle, &mut original_mode) })?; Ok(original_mode) } +type ConsoleKeyMap = (); +#[cfg(not(test))] +pub type KeyMap = ConsoleKeyMap; + +#[must_use = "You must restore default mode (disable_raw_mode)"] #[cfg(not(test))] pub type Mode = ConsoleMode; @@ -77,15 +87,11 @@ pub struct ConsoleMode { impl RawMode for ConsoleMode { /// Disable RAW mode for the terminal. fn disable_raw_mode(&self) -> Result<()> { - check!(consoleapi::SetConsoleMode( - self.stdin_handle, - self.original_stdin_mode, - )); + check(unsafe { consoleapi::SetConsoleMode(self.stdin_handle, self.original_stdin_mode) })?; if let Some(original_stdstream_mode) = self.original_stdstream_mode { - check!(consoleapi::SetConsoleMode( - self.stdstream_handle, - original_stdstream_mode, - )); + check(unsafe { + consoleapi::SetConsoleMode(self.stdstream_handle, original_stdstream_mode) + })?; } Ok(()) } @@ -104,7 +110,7 @@ impl ConsoleRawReader { } impl RawReader for ConsoleRawReader { - fn next_key(&mut self, _: bool) -> Result { + fn next_key(&mut self, _: bool) -> Result { use std::char::decode_utf16; use winapi::um::wincon::{ LEFT_ALT_PRESSED, LEFT_CTRL_PRESSED, RIGHT_ALT_PRESSED, RIGHT_CTRL_PRESSED, @@ -116,15 +122,10 @@ impl RawReader for ConsoleRawReader { let mut surrogate = 0; loop { // TODO GetNumberOfConsoleInputEvents - check!(consoleapi::ReadConsoleInputW( - self.handle, - &mut rec, - 1 as DWORD, - &mut count, - )); + check(unsafe { consoleapi::ReadConsoleInputW(self.handle, &mut rec, 1, &mut count) })?; if rec.EventType == wincon::WINDOW_BUFFER_SIZE_EVENT { - SIGWINCH.store(true, atomic::Ordering::SeqCst); + SIGWINCH.store(true, Ordering::SeqCst); debug!(target: "rustyline", "SIGWINCH"); return Err(error::ReadlineError::WindowResize); // sigwinch + // err => err @@ -142,76 +143,69 @@ impl RawReader for ConsoleRawReader { 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 && !alt_gr; - let shift = key_event.dwControlKeyState & SHIFT_PRESSED != 0; + let mut mods = M::NONE; + if !alt_gr + && key_event.dwControlKeyState & (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED) != 0 + { + mods |= M::CTRL; + } + if !alt_gr && key_event.dwControlKeyState & (LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED) != 0 + { + mods |= M::ALT; + } + if key_event.dwControlKeyState & SHIFT_PRESSED != 0 { + mods |= M::SHIFT; + } let utf16 = unsafe { *key_event.uChar.UnicodeChar() }; - if utf16 == 0 { - match i32::from(key_event.wVirtualKeyCode) { - winuser::VK_LEFT => { - return Ok(if ctrl { - KeyPress::ControlLeft - } else if shift { - KeyPress::ShiftLeft - } else { - KeyPress::Left - }); - } - winuser::VK_RIGHT => { - return Ok(if ctrl { - KeyPress::ControlRight - } else if shift { - KeyPress::ShiftRight - } else { - KeyPress::Right - }); + let key_code = match i32::from(key_event.wVirtualKeyCode) { + winuser::VK_LEFT => K::Left, + winuser::VK_RIGHT => K::Right, + winuser::VK_UP => K::Up, + winuser::VK_DOWN => K::Down, + winuser::VK_DELETE => K::Delete, + winuser::VK_HOME => K::Home, + winuser::VK_END => K::End, + winuser::VK_PRIOR => K::PageUp, + winuser::VK_NEXT => K::PageDown, + winuser::VK_INSERT => K::Insert, + winuser::VK_F1 => K::F(1), + winuser::VK_F2 => K::F(2), + winuser::VK_F3 => K::F(3), + winuser::VK_F4 => K::F(4), + winuser::VK_F5 => K::F(5), + winuser::VK_F6 => K::F(6), + winuser::VK_F7 => K::F(7), + winuser::VK_F8 => K::F(8), + winuser::VK_F9 => K::F(9), + winuser::VK_F10 => K::F(10), + winuser::VK_F11 => K::F(11), + winuser::VK_F12 => K::F(12), + winuser::VK_BACK => K::Backspace, // vs Ctrl-h + winuser::VK_RETURN => K::Enter, // vs Ctrl-m + winuser::VK_ESCAPE => K::Esc, + winuser::VK_TAB => { + if mods.contains(M::SHIFT) { + mods.remove(M::SHIFT); + K::BackTab + } else { + K::Tab // vs Ctrl-i } - winuser::VK_UP => { - return Ok(if ctrl { - KeyPress::ControlUp - } else if shift { - KeyPress::ShiftUp - } else { - KeyPress::Up - }); - } - winuser::VK_DOWN => { - return Ok(if ctrl { - KeyPress::ControlDown - } else if shift { - KeyPress::ShiftDown - } else { - KeyPress::Down - }); + } + _ => { + if utf16 == 0 { + continue; + } else { + K::UnknownEscSeq } - 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, - }; + } + }; + let key = if key_code != K::UnknownEscSeq { + KeyEvent(key_code, mods) } else if utf16 == 27 { - return Ok(KeyPress::Esc); + KeyEvent(K::Esc, mods) // FIXME dead code ? } else { - if utf16 >= 0xD800 && utf16 < 0xDC00 { + if (0xD800..0xDC00).contains(&utf16) { surrogate = utf16; continue; } @@ -226,23 +220,19 @@ impl RawReader for ConsoleRawReader { return Err(error::ReadlineError::Eof); }; let c = rc?; - if meta { - return Ok(KeyPress::Meta(c)); - } else { - let mut key = keys::char_to_key_press(c); - if key == KeyPress::Tab && shift { - key = KeyPress::BackTab; - } else if key == KeyPress::Char(' ') && ctrl { - key = KeyPress::Ctrl(' '); - } - return Ok(key); - } - } + KeyEvent::new(c, mods) + }; + debug!(target: "rustyline", "wVirtualKeyCode: {:#x}, utf16: {:#x}, dwControlKeyState: {:#x} => key: {:?}", key_event.wVirtualKeyCode, utf16, key_event.dwControlKeyState, key); + return Ok(key); } } fn read_pasted_text(&mut self) -> Result { - unimplemented!() + Ok(clipboard_win::get_clipboard_string()?) + } + + fn find_binding(&self, _: &KeyEvent) -> Option { + None } } @@ -274,30 +264,80 @@ impl ConsoleRenderer { } } - fn get_console_screen_buffer_info(&self) -> Result { + fn get_console_screen_buffer_info(&self) -> Result { let mut info = unsafe { mem::zeroed() }; - check!(wincon::GetConsoleScreenBufferInfo(self.handle, &mut info)); + check(unsafe { wincon::GetConsoleScreenBufferInfo(self.handle, &mut info) })?; Ok(info) } - fn set_console_cursor_position(&mut self, pos: wincon::COORD) -> Result<()> { - check!(wincon::SetConsoleCursorPosition(self.handle, pos)); - Ok(()) + fn set_console_cursor_position(&mut self, pos: COORD) -> Result<()> { + check(unsafe { wincon::SetConsoleCursorPosition(self.handle, pos) }) } - fn clear(&mut self, length: DWORD, pos: wincon::COORD) -> Result<()> { + fn clear(&mut self, length: DWORD, pos: COORD, attr: WORD) -> Result<()> { let mut _count = 0; - check!(wincon::FillConsoleOutputCharacterA( - self.handle, - ' ' as CHAR, - length, - pos, - &mut _count, - )); - Ok(()) + check(unsafe { + wincon::FillConsoleOutputCharacterA(self.handle, ' ' as CHAR, length, pos, &mut _count) + })?; + check(unsafe { + wincon::FillConsoleOutputAttribute(self.handle, attr, length, pos, &mut _count) + }) + } + + fn set_cursor_visible(&mut self, visible: BOOL) -> Result<()> { + set_cursor_visible(self.handle, visible) + } + + // You can't have both ENABLE_WRAP_AT_EOL_OUTPUT and + // ENABLE_VIRTUAL_TERMINAL_PROCESSING. So we need to wrap manually. + fn wrap_at_eol(&mut self, s: &str, col: &mut usize) { + let mut esc_seq = 0; + for c in s.graphemes(true) { + if c == "\n" { + col = 0; + *col = 0; + } else { + let cw = width(c, &mut esc_seq); + *col += cw; + if *col > self.cols { + self.buffer.push('\n'); + *col = cw; + } + } + self.buffer.push_str(c); + } + if *col == self.cols { + self.buffer.push('\n'); + *col = 0; + } + } + + // position at the start of the prompt, clear to end of previous input + fn clear_old_rows(&mut self, info: &CONSOLE_SCREEN_BUFFER_INFO, layout: &Layout) -> Result<()> { + let current_row = layout.cursor.row; + let old_rows = layout.end.row; + let mut coord = info.dwCursorPosition; + coord.X = 0; + coord.Y -= current_row as i16; + self.set_console_cursor_position(coord)?; + self.clear( + (info.dwSize.X * (old_rows as i16 + 1)) as DWORD, + coord, + info.wAttributes, + ) } } +fn set_cursor_visible(handle: HANDLE, visible: BOOL) -> Result<()> { + let mut info = unsafe { mem::zeroed() }; + check(unsafe { wincon::GetConsoleCursorInfo(handle, &mut info) })?; + if info.bVisible == visible { + return Ok(()); + } + info.bVisible = visible; + check(unsafe { wincon::SetConsoleCursorInfo(handle, &info) }) +} + impl Renderer for ConsoleRenderer { type Reader = ConsoleRawReader; @@ -318,49 +358,37 @@ impl Renderer for ConsoleRenderer { fn refresh_line( &mut self, - prompt: &str, + prompt: &Prompt, line: &LineBuffer, hint: Option<&str>, old_layout: &Layout, new_layout: &Layout, highlighter: Option<&dyn Highlighter>, ) -> Result<()> { - let default_prompt = new_layout.default_prompt; let cursor = new_layout.cursor; let end_pos = new_layout.end; - let current_row = old_layout.cursor.row; - let old_rows = old_layout.end.row; self.buffer.clear(); - if let Some(highlighter) = highlighter { - // TODO handle ansi escape code (SetConsoleTextAttribute) - // append the prompt - self.buffer - .push_str(&highlighter.highlight_prompt(prompt, default_prompt)); - // append the input line - self.buffer - .push_str(&highlighter.highlight(line, line.pos())); - } else { - // append the prompt - self.buffer.push_str(prompt); - // append the input line - self.buffer.push_str(line); - } + let mut col = 0; + add_prompt_and_highlight(|s| { self.wrap_at_eol(s, &mut col); }, + highlighter, line, prompt); + // append hint if let Some(hint) = hint { if let Some(highlighter) = highlighter { - self.buffer.push_str(&highlighter.highlight_hint(hint)); + self.wrap_at_eol(&highlighter.highlight_hint(hint), &mut col); } else { self.buffer.push_str(hint); } } - // position at the start of the prompt, clear to end of previous input let info = self.get_console_screen_buffer_info()?; - let mut coord = info.dwCursorPosition; - coord.X = 0; - coord.Y -= current_row as i16; - self.set_console_cursor_position(coord)?; - self.clear((info.dwSize.X * (old_rows as i16 + 1)) as DWORD, coord)?; + self.set_cursor_visible(FALSE)?; // just to avoid flickering + let handle = self.handle; + scopeguard::defer! { + let _ = set_cursor_visible(handle, TRUE); + } + // position at the start of the prompt, clear to end of previous input + self.clear_old_rows(&info, old_layout)?; // display prompt, input line and hint self.write_and_flush(self.buffer.as_bytes())?; @@ -388,17 +416,16 @@ impl Renderer for ConsoleRenderer { } /// Characters with 2 column width are correctly handled (not split). - fn calculate_position(&self, s: &str, orig: Position) -> Position { + fn calculate_position(&self, s: &str, orig: Position, left_margin: usize) + -> Position + { let mut pos = orig; - for c in s.chars() { - let cw = if c == '\n' { - pos.col = 0; + for c in s.graphemes(true) { + if c == "\n" { + pos.col = left_margin; pos.row += 1; - None } else { - c.width() - }; - if let Some(cw) = cw { + let cw = c.width(); pos.col += cw; if pos.col > self.cols { pos.row += 1; @@ -427,14 +454,16 @@ impl Renderer for ConsoleRenderer { /// Clear the screen. Used to handle ctrl+l fn clear_screen(&mut self) -> Result<()> { let info = self.get_console_screen_buffer_info()?; - let coord = wincon::COORD { X: 0, Y: 0 }; - check!(wincon::SetConsoleCursorPosition(self.handle, coord)); + let coord = COORD { X: 0, Y: 0 }; + check(unsafe { wincon::SetConsoleCursorPosition(self.handle, coord) })?; let n = info.dwSize.X as DWORD * info.dwSize.Y as DWORD; - self.clear(n, coord) + self.clear(n, coord, info.wAttributes) } fn sigwinch(&self) -> bool { - SIGWINCH.compare_and_swap(true, false, atomic::Ordering::SeqCst) + SIGWINCH + .compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst) + .unwrap_or(false) } /// Try to get the number of columns in the current terminal, @@ -468,11 +497,19 @@ impl Renderer for ConsoleRenderer { debug!(target: "rustyline", "initial cursor location: {:?}, {:?}", info.dwCursorPosition.X, info.dwCursorPosition.Y); info.dwCursorPosition.X = 0; info.dwCursorPosition.Y += 1; - self.set_console_cursor_position(info.dwCursorPosition) + let res = self.set_console_cursor_position(info.dwCursorPosition); + if let Err(error::ReadlineError::Io(ref e)) = res { + if e.raw_os_error() == Some(winerror::ERROR_INVALID_PARAMETER as i32) { + warn!(target: "rustyline", "invalid cursor position: ({:?}, {:?}) in ({:?}, {:?})", info.dwCursorPosition.X, info.dwCursorPosition.Y, info.dwSize.X, info.dwSize.Y); + println!(); + return Ok(()); + } + } + res } } -static SIGWINCH: atomic::AtomicBool = atomic::AtomicBool::new(false); +static SIGWINCH: AtomicBool = AtomicBool::new(false); #[cfg(not(test))] pub type Terminal = Console; @@ -501,6 +538,7 @@ impl Console { } impl Term for Console { + type KeyMap = ConsoleKeyMap; type Mode = ConsoleMode; type Reader = ConsoleRawReader; type Writer = ConsoleRenderer; @@ -510,6 +548,7 @@ impl Term for Console { stream_type: OutputStreamType, _tab_stop: usize, bell_style: BellStyle, + _enable_bracketed_paste: bool, ) -> Console { use std::ptr; let stdin_handle = get_std_handle(STDIN_FILENO); @@ -564,7 +603,7 @@ impl Term for Console { // } /// Enable RAW mode for the terminal. - fn enable_raw_mode(&mut self) -> Result { + fn enable_raw_mode(&mut self) -> Result<(ConsoleMode, ConsoleKeyMap)> { if !self.stdin_isatty { Err(io::Error::new( io::ErrorKind::Other, @@ -582,35 +621,55 @@ impl Term for Console { raw |= wincon::ENABLE_INSERT_MODE; raw |= wincon::ENABLE_QUICK_EDIT_MODE; raw |= wincon::ENABLE_WINDOW_INPUT; - check!(consoleapi::SetConsoleMode(self.stdin_handle, raw)); + check(unsafe { consoleapi::SetConsoleMode(self.stdin_handle, raw) })?; let original_stdstream_mode = if self.stdstream_isatty { let original_stdstream_mode = get_console_mode(self.stdstream_handle)?; + + let mut mode = original_stdstream_mode; + if mode & wincon::ENABLE_WRAP_AT_EOL_OUTPUT == 0 { + mode |= wincon::ENABLE_WRAP_AT_EOL_OUTPUT; + debug!(target: "rustyline", "activate ENABLE_WRAP_AT_EOL_OUTPUT"); + unsafe { + assert_ne!(consoleapi::SetConsoleMode(self.stdstream_handle, mode), 0); + } + } // To enable ANSI colors (Windows 10 only): // https://docs.microsoft.com/en-us/windows/console/setconsolemode - if original_stdstream_mode & wincon::ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0 { - let raw = original_stdstream_mode | wincon::ENABLE_VIRTUAL_TERMINAL_PROCESSING; + self.ansi_colors_supported = mode & wincon::ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0; + if self.ansi_colors_supported { + if self.color_mode == ColorMode::Disabled { + mode &= !wincon::ENABLE_VIRTUAL_TERMINAL_PROCESSING; + debug!(target: "rustyline", "deactivate ENABLE_VIRTUAL_TERMINAL_PROCESSING"); + unsafe { + assert_ne!(consoleapi::SetConsoleMode(self.stdstream_handle, mode), 0); + } + } else { + debug!(target: "rustyline", "ANSI colors already enabled"); + } + } else if self.color_mode != ColorMode::Disabled { + mode |= wincon::ENABLE_VIRTUAL_TERMINAL_PROCESSING; self.ansi_colors_supported = - unsafe { consoleapi::SetConsoleMode(self.stdstream_handle, raw) != 0 }; + unsafe { consoleapi::SetConsoleMode(self.stdstream_handle, mode) != 0 }; debug!(target: "rustyline", "ansi_colors_supported: {}", self.ansi_colors_supported); - } else { - debug!(target: "rustyline", "ANSI colors already enabled"); - self.ansi_colors_supported = true; } Some(original_stdstream_mode) } else { None }; - Ok(ConsoleMode { - original_stdin_mode, - stdin_handle: self.stdin_handle, - original_stdstream_mode, - stdstream_handle: self.stdstream_handle, - }) + Ok(( + ConsoleMode { + original_stdin_mode, + stdin_handle: self.stdin_handle, + original_stdstream_mode, + stdstream_handle: self.stdstream_handle, + }, + (), + )) } - fn create_reader(&self, _: &Config) -> Result { + fn create_reader(&self, _: &Config, _: ConsoleKeyMap) -> Result { ConsoleRawReader::create() } diff --git a/src/validate.rs b/src/validate.rs new file mode 100644 index 0000000000..b29c682a54 --- /dev/null +++ b/src/validate.rs @@ -0,0 +1,147 @@ +//! Input validation API (Multi-line editing) + +use crate::keymap::Invoke; +use crate::Result; + +/// Input validation result +#[non_exhaustive] +pub enum ValidationResult { + /// Incomplete input + Incomplete, + /// Validation fails with an optional error message. User must fix the + /// input. + Invalid(Option), + /// Validation succeeds with an optional message + Valid(Option), +} + +impl ValidationResult { + pub(crate) fn is_valid(&self) -> bool { + matches!(self, ValidationResult::Valid(_)) + } + + pub(crate) fn has_message(&self) -> bool { + matches!( + self, + ValidationResult::Valid(Some(_)) | ValidationResult::Invalid(Some(_)) + ) + } +} + +/// Give access to user input. +pub struct ValidationContext<'i> { + i: &'i mut dyn Invoke, +} + +impl<'i> ValidationContext<'i> { + pub(crate) fn new(i: &'i mut dyn Invoke) -> Self { + ValidationContext { i } + } + + /// Returns user input. + pub fn input(&self) -> &str { + self.i.input() + } + + // TODO + //fn invoke(&mut self, cmd: Cmd) -> Result { + // self.i.invoke(cmd) + //} +} + +/// This trait provides an extension interface for determining whether +/// the current input buffer is valid. Rustyline uses the method +/// provided by this trait to decide whether hitting the enter key +/// will end the current editing session and return the current line +/// buffer to the caller of `Editor::readline` or variants. +pub trait Validator { + /// Takes the currently edited `input` and returns a + /// `ValidationResult` indicating whether it is valid or not along + /// with an option message to display about the result. The most + /// common validity check to implement is probably whether the + /// input is complete or not, for instance ensuring that all + /// delimiters are fully balanced. + /// + /// If you implement more complex validation checks it's probably + /// a good idea to also implement a `Hinter` to provide feedback + /// about what is invalid. + /// + /// For auto-correction like a missing closing quote or to reject invalid + /// char while typing, the input will be mutable (TODO). + fn validate(&self, ctx: &mut ValidationContext) -> Result { + let _ = ctx; + Ok(ValidationResult::Valid(None)) + } + + /// Configure whether validation is performed while typing or only + /// when user presses the Enter key. + /// + /// Default is `false`. + /// + /// This feature is not yet implemented, so this function is currently a + /// no-op + fn validate_while_typing(&self) -> bool { + false + } +} + +impl Validator for () {} + +impl<'v, V: ?Sized + Validator> Validator for &'v V { + fn validate(&self, ctx: &mut ValidationContext) -> Result { + (**self).validate(ctx) + } + + fn validate_while_typing(&self) -> bool { + (**self).validate_while_typing() + } +} + +/// Simple matching bracket validator. +#[derive(Default)] +pub struct MatchingBracketValidator { + _priv: (), +} + +impl MatchingBracketValidator { + /// Constructor + pub fn new() -> Self { + Self { _priv: () } + } +} + +impl Validator for MatchingBracketValidator { + fn validate(&self, ctx: &mut ValidationContext) -> Result { + Ok(validate_brackets(ctx.input())) + } +} + +fn validate_brackets(input: &str) -> ValidationResult { + let mut stack = vec![]; + for c in input.chars() { + match c { + '(' | '[' | '{' => stack.push(c), + ')' | ']' | '}' => match (stack.pop(), c) { + (Some('('), ')') | (Some('['), ']') | (Some('{'), '}') => {} + (Some(wanted), _) => { + return ValidationResult::Invalid(Some(format!( + "Mismatched brackets: {:?} is not properly closed", + wanted + ))) + } + (None, c) => { + return ValidationResult::Invalid(Some(format!( + "Mismatched brackets: {:?} is unpaired", + c + ))) + } + }, + _ => {} + } + } + if stack.is_empty() { + ValidationResult::Valid(None) + } else { + ValidationResult::Incomplete + } +}