rustyline-6.0.0/.github/workflows/rust.yml010064400007650000024000000007761360441142000171140ustar0000000000000000name: Rust on: push: branches: - master pull_request: branches: - master jobs: build: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, windows-latest] steps: - uses: actions/checkout@v1 - uses: actions-rs/toolchain@v1 with: default: true profile: minimal toolchain: stable override: true - name: Build run: cargo build --verbose - name: Run tests run: cargo test --verbose rustyline-6.0.0/.gitignore010064400007650000024000000002411346010623700137610ustar0000000000000000# Compiled files *.o *.so *.rlib *.dll # Executables *.exe # Generated by Cargo /target/ Cargo.lock # vim swap file *.swp # default history file history.txt rustyline-6.0.0/BUGS.md010064400007650000024000000020011360304376400130530ustar0000000000000000Know issues ## Document / Syntax 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. 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`. See [lex_document](https://python-prompt-toolkit.readthedocs.io/en/master/pages/advanced_topics/rendering_flow.html#the-rendering-flow). ## Repaint / Refresh Currently, performance is poor because, most of the time, we refresh the whole line (and prompt). We would like to transform events on prompt/line/hint into partial repaint. See `termwiz` design (`Surface`). ## Action / Command We would like to support user defined actions that interact nicely with undo manager and kill-ring. To do so, we need to refactor current key event dispatch. See `replxx` design (`ACTION_RESULT`, `action_trait_t`). rustyline-6.0.0/Cargo.toml.orig010064400007650000024000000024441360441303700146660ustar0000000000000000[package] name = "rustyline" version = "6.0.0" authors = ["Katsu Kawakami "] edition = "2018" description = "Rustyline, a readline implementation based on Antirez's Linenoise" documentation = "http://docs.rs/rustyline" repository = "https://github.com/kkawakam/rustyline" readme = "README.md" keywords = ["readline"] license = "MIT" categories = ["command-line-interface"] [badges] github-actions = { repository = "kkawakam/rustyline", workflow = "Rust" } maintenance = { status = "actively-developed" } [workspace] members = ["rustyline-derive"] [dependencies] cfg-if = "0.1" dirs = { version = "2.0", optional = true } libc = "0.2" log = "0.4" unicode-width = "0.1" unicode-segmentation = "1.0" memchr = "2.0" [target.'cfg(unix)'.dependencies] nix = "0.14" utf8parse = "0.1" [target.'cfg(windows)'.dependencies] winapi = { version = "0.3", features = ["consoleapi", "handleapi", "minwindef", "processenv", "winbase", "wincon", "winuser"] } [dev-dependencies] env_logger = "0.7" tempdir = "0.3" assert_matches = "1.2" rustyline-derive = { version = "0.3.0", path = "rustyline-derive" } [features] default = ["with-dirs"] with-dirs = ["dirs"] [package.metadata.docs.rs] features = ["with-dirs"] all-features = false no-default-features = true default-target = "x86_64-unknown-linux-gnu" rustyline-6.0.0/Cargo.toml0000644000000037441360441330500112000ustar00# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies # # If you believe there's an error in this file please file an # issue against the rust-lang/cargo repository. If you're # editing this file be aware that the upstream Cargo.toml # will likely look very different (and much more reasonable) [package] edition = "2018" name = "rustyline" version = "6.0.0" authors = ["Katsu Kawakami "] description = "Rustyline, a readline implementation based on Antirez's Linenoise" documentation = "http://docs.rs/rustyline" readme = "README.md" keywords = ["readline"] categories = ["command-line-interface"] license = "MIT" repository = "https://github.com/kkawakam/rustyline" [package.metadata.docs.rs] all-features = false default-target = "x86_64-unknown-linux-gnu" features = ["with-dirs"] no-default-features = true [dependencies.cfg-if] version = "0.1" [dependencies.dirs] version = "2.0" optional = true [dependencies.libc] version = "0.2" [dependencies.log] version = "0.4" [dependencies.memchr] version = "2.0" [dependencies.unicode-segmentation] version = "1.0" [dependencies.unicode-width] version = "0.1" [dev-dependencies.assert_matches] version = "1.2" [dev-dependencies.env_logger] version = "0.7" [dev-dependencies.rustyline-derive] version = "0.3.0" [dev-dependencies.tempdir] version = "0.3" [features] default = ["with-dirs"] with-dirs = ["dirs"] [target."cfg(unix)".dependencies.nix] version = "0.14" [target."cfg(unix)".dependencies.utf8parse] version = "0.1" [target."cfg(windows)".dependencies.winapi] version = "0.3" features = ["consoleapi", "handleapi", "minwindef", "processenv", "winbase", "wincon", "winuser"] [badges.github-actions] repository = "kkawakam/rustyline" workflow = "Rust" [badges.maintenance] status = "actively-developed" rustyline-6.0.0/examples/example.rs010064400007650000024000000065211360441142000156100ustar0000000000000000use 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::hint::{Hinter, HistoryHinter}; use rustyline::{Cmd, CompletionType, Config, Context, EditMode, Editor, KeyPress}; use rustyline_derive::{Helper, Validator}; #[derive(Helper, Validator)] struct MyHelper { completer: FilenameCompleter, highlighter: MatchingBracketHighlighter, hinter: HistoryHinter, colored_prompt: String, } impl Completer for MyHelper { type Candidate = Pair; fn complete( &self, line: &str, pos: usize, ctx: &Context<'_>, ) -> Result<(usize, Vec), ReadlineError> { self.completer.complete(line, pos, ctx) } } impl Hinter for MyHelper { fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option { self.hinter.hint(line, pos, ctx) } } impl Highlighter for MyHelper { fn highlight_prompt<'b, 's: 'b, 'p: 'b>( &'s self, prompt: &'p str, default: bool, ) -> Cow<'b, str> { if default { Borrowed(&self.colored_prompt) } else { Borrowed(prompt) } } fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { Owned("\x1b[1m".to_owned() + hint + "\x1b[m") } fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> { self.highlighter.highlight(line, pos) } fn highlight_char(&self, line: &str, pos: usize) -> bool { self.highlighter.highlight_char(line, pos) } } // To debug rustyline: // RUST_LOG=rustyline=debug cargo run --example example 2> debug.log fn main() -> rustyline::Result<()> { env_logger::init(); let config = Config::builder() .history_ignore_space(true) .completion_type(CompletionType::List) .edit_mode(EditMode::Emacs) .output_stream(OutputStreamType::Stdout) .build(); let h = MyHelper { completer: FilenameCompleter::new(), highlighter: MatchingBracketHighlighter::new(), hinter: HistoryHinter {}, colored_prompt: "".to_owned(), }; 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); if rl.load_history("history.txt").is_err() { println!("No previous history."); } let mut count = 1; loop { let p = format!("{}> ", count); rl.helper_mut().expect("No helper").colored_prompt = format!("\x1b[1;32m{}\x1b[0m", p); let readline = rl.readline(&p); match readline { Ok(line) => { rl.add_history_entry(line.as_str()); println!("Line: {}", line); } Err(ReadlineError::Interrupted) => { println!("CTRL-C"); break; } Err(ReadlineError::Eof) => { println!("CTRL-D"); break; } Err(err) => { println!("Error: {:?}", err); break; } } count += 1; } rl.save_history("history.txt") } rustyline-6.0.0/examples/input_validation.rs010064400007650000024000000017221360441142000175240ustar0000000000000000use rustyline::error::ReadlineError; use rustyline::validate::{ValidationContext, ValidationResult, Validator}; use rustyline::Editor; 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 { let input = ctx.input(); if !input.starts_with("SELECT") { return Ok(ValidationResult::Invalid(Some( " --< Expect: SELECT stmt".to_owned(), ))); } else if !input.ends_with(';') { return Ok(ValidationResult::Incomplete); } Ok(ValidationResult::Valid(None)) } } fn main() -> rustyline::Result<()> { let h = InputValidator {}; let mut rl = Editor::new(); rl.set_helper(Some(h)); let input = rl.readline("> ")?; println!("Input: {}", input); Ok(()) } rustyline-6.0.0/examples/read_password.rs010064400007650000024000000024511360441142000170100ustar0000000000000000use std::borrow::Cow::{self, Borrowed, Owned}; use rustyline::config::Configurer; use rustyline::highlight::Highlighter; use rustyline::{ColorMode, Editor}; use rustyline_derive::{Completer, Helper, Hinter, Validator}; #[derive(Completer, Helper, Hinter, Validator)] struct MaskingHighlighter { masking: bool, } impl Highlighter for MaskingHighlighter { fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> { use unicode_width::UnicodeWidthStr; if self.masking { Owned("*".repeat(line.width())) } else { Borrowed(line) } } fn highlight_char(&self, _line: &str, _pos: usize) -> bool { self.masking } } fn main() -> rustyline::Result<()> { println!("This is just a hack. Reading passwords securely requires more than that."); let h = MaskingHighlighter { masking: false }; let mut rl = Editor::new(); rl.set_helper(Some(h)); let username = rl.readline("Username:")?; println!("Username: {}", username); rl.helper_mut().expect("No helper").masking = true; rl.set_color_mode(ColorMode::Forced); // force masking rl.set_auto_add_history(false); // make sure password is not added to history let passwd = rl.readline("Password:")?; println!("Secret: {}", passwd); Ok(()) } rustyline-6.0.0/LICENSE010064400007650000024000000021161346010623700130010ustar0000000000000000The MIT License (MIT) Copyright (c) 2015 Katsu Kawakami & Rustyline authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. rustyline-6.0.0/README.md010064400007650000024000000244771360441303700132700ustar0000000000000000# RustyLine [![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) [![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) **Supported Platforms** * Unix (tested on FreeBSD, Linux and macOS) * Windows * cmd.exe * Powershell **Note**: * Powershell ISE is not supported, check [issue #56](https://github.com/kkawakam/rustyline/issues/56) * Mintty (Cygwin/MinGW) is not supported ## Example ```rust extern crate rustyline; use rustyline::error::ReadlineError; use rustyline::Editor; fn main() { // `()` can be used when no completer is required let mut rl = Editor::<()>::new(); if rl.load_history("history.txt").is_err() { println!("No previous history."); } loop { let readline = rl.readline(">> "); match readline { Ok(line) => { rl.add_history_entry(line.as_str()); println!("Line: {}", line); }, Err(ReadlineError::Interrupted) => { println!("CTRL-C"); break }, Err(ReadlineError::Eof) => { println!("CTRL-D"); break }, Err(err) => { println!("Error: {:?}", err); break } } } rl.save_history("history.txt").unwrap(); } ``` ## crates.io You can use this package in your project by adding the following to your `Cargo.toml`: ```toml [dependencies] rustyline = "6.0.0" ``` ## Features - Unicode (UTF-8) (linenoise supports only ASCII) - Word completion (linenoise supports only line completion) - Filename completion - History search ([Searching for Commands in the History](http://tiswww.case.edu/php/chet/readline/readline.html#SEC8)) - Kill ring ([Killing Commands](http://tiswww.case.edu/php/chet/readline/readline.html#IDX3)) - Multi line support (line wrapping) - Word commands - Hints ## Actions For all modes: Keystroke | Action --------- | ------ Home | Move cursor to the beginning of line End | Move cursor to end of line Left | Move cursor one character left Right | Move cursor one character right Ctrl-C | Interrupt/Cancel edition Ctrl-D, Del | (if line is *not* empty) Delete character under cursor Ctrl-D | (if line *is* empty) End of File Ctrl-J, Ctrl-M, Enter | Finish the line entry Ctrl-R | Reverse Search history (Ctrl-S forward, Ctrl-G cancel) Ctrl-T | Transpose previous character with current character Ctrl-U | Delete from start of line to cursor Ctrl-V | Insert any special character without performing its associated action (#65) Ctrl-W | Delete word leading up to cursor (using white space as a word boundary) Ctrl-Y | Paste from Yank buffer Ctrl-Z | Suspend (Unix only) Ctrl-_ | Undo ### Emacs mode (default mode) Keystroke | Action --------- | ------ Ctrl-A, Home | Move cursor to the beginning of line Ctrl-B, Left | Move cursor one character left Ctrl-E, End | Move cursor to end of line Ctrl-F, Right| Move cursor one character right Ctrl-H, Backspace | Delete character before cursor Ctrl-I, Tab | Next completion Ctrl-K | Delete from cursor to end of line Ctrl-L | Clear screen Ctrl-N, Down | Next match from history Ctrl-P, Up | Previous match from history Ctrl-X Ctrl-U | Undo Ctrl-Y | Paste from Yank buffer (Meta-Y to paste next yank instead) Meta-< | Move to first entry in history Meta-> | Move to last entry in history Meta-B, Alt-Left | Move cursor to previous word Meta-C | Capitalize the current word Meta-D | Delete forwards one word Meta-F, Alt-Right | Move cursor to next word Meta-L | Lower-case the next word Meta-T | Transpose words Meta-U | Upper-case the next word Meta-Y | See Ctrl-Y Meta-Backspace | Kill from the start of the current word, or, if between words, to the start of the previous word Meta-0, 1, ..., - | Specify the digit to the argument. `–` starts a negative argument. [Readline Emacs Editing Mode Cheat Sheet](http://www.catonmat.net/download/readline-emacs-editing-mode-cheat-sheet.pdf) ### vi command mode Keystroke | Action --------- | ------ $, End | Move cursor to end of line . | Redo the last text modification ; | Redo the last character finding command , | Redo the last character finding command in opposite direction 0, Home | Move cursor to the beginning of line ^ | Move to the first non-blank character of line a | Insert after cursor A | Insert at the end of line b | Move one word or token left B | Move one non-blank word left c | Change text of a movement command C | Change text to the end of line (equivalent to c$) d | Delete text of a movement command D, Ctrl-K | Delete to the end of the line e | Move to the end of the current word E | Move to the end of the current non-blank word f | Move right to the next occurrence of `char` F | Move left to the previous occurrence of `char` h, Ctrl-H, Backspace | Move one character left l, Space | Move one character right Ctrl-L | Clear screen i | Insert before cursor I | Insert at the beginning of line +, j, Ctrl-N | Move forward one command in history -, k, Ctrl-P | Move backward one command in history p | Insert the yanked text at the cursor (paste) P | Insert the yanked text before the cursor r | Replaces a single character under the cursor (without leaving command mode) s | Delete a single character under the cursor and enter input mode S | Change current line (equivalent to 0c$) t | Move right to the next occurrence of `char`, then one char backward T | Move left to the previous occurrence of `char`, then one char forward u | Undo w | Move one word or token right W | Move one non-blank word right x | Delete a single character under the cursor X | Delete a character before the cursor y | Yank a movement into buffer (copy) ### vi insert mode Keystroke | Action --------- | ------ Ctrl-H, Backspace | Delete character before cursor Ctrl-I, Tab | Next completion Esc | Switch to command mode [Readline vi Editing Mode Cheat Sheet](http://www.catonmat.net/download/bash-vi-editing-mode-cheat-sheet.pdf) [Terminal codes (ANSI/VT100)](http://wiki.bash-hackers.org/scripting/terminalcodes) ## Wine ```sh $ cargo run --example example --target 'x86_64-pc-windows-gnu' ... Error: Io(Error { repr: Os { code: 6, message: "Invalid handle." } }) $ wineconsole --backend=curses target/x86_64-pc-windows-gnu/debug/examples/example.exe ... ``` ## Terminal checks ```sh $ # current settings of all terminal attributes: $ stty -a $ # key bindings: $ bind -p ``` ## Similar projects Library | Lang | OS | Term | Unicode | History | Completion | Keymap | Kill Ring | Undo | Colors | Hint/Auto suggest | -------- | ---- | -- | ---- | ------- | ------- | ---------- | ------- | --------- | ---- | ------ | ----------------- | [go-prompt][] | Go | Ux/win | ANSI | Yes | Yes | any | Emacs/prog | No | No | Yes | Yes | [Haskeline][] | Haskell | Ux/Win | Any | Yes | Yes | any | Emacs/vi/conf | Yes | Yes | ? | ? | [linefeed][] | Rust | Ux/Win | Any | | Yes | any | Emacs/conf | Yes | No | ? | No | [linenoise][] | C | Ux | ANSI | No | Yes | only line | Emacs | No | No | Ux | Yes | [linenoise-ng][] | C | Ux/Win | ANSI | Yes | Yes | only line | Emacs | Yes | No | ? | ? | [Liner][] | Rust | Ux | ANSI | | No inc search | only word | Emacs/vi/prog | No | Yes | Ux | History based | [prompt_toolkit][] | Python | Ux/Win | ANSI | Yes | Yes | any | Emacs/vi/conf | Yes | Yes | Ux/Win | Yes | [rb-readline][] | Ruby | Ux/Win | ANSI | Yes | Yes | only word | Emacs/vi/conf | Yes | Yes | ? | No | [replxx][] | C/C++ | Ux/Win | ANSI | Yes | Yes | only line | Emacs | Yes | No | Ux/Win | Yes | Rustyline | Rust | Ux/Win | ANSI | Yes | Yes | any | Emacs/vi/bind | Yes | Yes | Ux/Win 10+ | Yes | [termwiz][] | Rust | Ux/Win | Any | ? | Yes | any | Emacs | No | No | Ux/Win | No | [go-prompt]: https://github.com/c-bata/go-prompt [Haskeline]: https://github.com/judah/haskeline [linefeed]: https://github.com/murarth/linefeed [linenoise]: https://github.com/antirez/linenoise [linenoise-ng]: https://github.com/arangodb/linenoise-ng [Liner]: https://github.com/redox-os/liner [prompt_toolkit]: https://github.com/jonathanslenders/python-prompt-toolkit [rb-readline]: https://github.com/ConnorAtherton/rb-readline [replxx]: https://github.com/AmokHuginnsson/replxx [termwiz]: https://github.com/wez/wezterm/tree/master/termwiz ## Multi line support This is a very simple feature that simply causes lines that are longer than the current terminal width to be displayed on the next visual 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 sequence and enter a mode where hitting the return key will cause a literal newline to be added to the input buffer. The way to achieve multi-line editing is to implement the `Validator` trait. rustyline-6.0.0/rustfmt.toml010064400007650000024000000001421346010623700143720ustar0000000000000000wrap_comments = true format_strings = true error_on_unformatted = false reorder_impl_items = true rustyline-6.0.0/src/completion.rs010064400007650000024000000427001357476110200153110ustar0000000000000000//! Completion API use std::borrow::Cow::{self, Borrowed, Owned}; use std::fs; use std::path::{self, Path}; 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. fn display(&self) -> &str; /// Text to insert in line. fn replacement(&self) -> &str; } impl Candidate for String { fn display(&self) -> &str { self.as_str() } fn replacement(&self) -> &str { self.as_str() } } pub struct Pair { pub display: String, pub replacement: String, } impl Candidate for Pair { fn display(&self) -> &str { self.display.as_str() } fn replacement(&self) -> &str { self.replacement.as_str() } } /// To be called for tab-completion. pub trait Completer { type Candidate: Candidate; /// Takes the currently edited `line` with the cursor `pos`ition and /// returns the start position and the completion candidates for the /// partial word to be completed. /// /// ("ls /usr/loc", 11) => Ok((3, vec!["/usr/local/"])) fn complete( &self, line: &str, pos: usize, ctx: &Context<'_>, ) -> Result<(usize, Vec)> { let _ = (line, pos, ctx); Ok((0, Vec::with_capacity(0))) } /// Updates the edited `line` with the `elected` candidate. fn update(&self, line: &mut LineBuffer, start: usize, elected: &str) { let end = line.pos(); line.replace(start..end, elected) } } impl Completer for () { type Candidate = String; fn update(&self, _line: &mut LineBuffer, _start: usize, _elected: &str) { unreachable!() } } impl<'c, C: ?Sized + Completer> Completer for &'c C { type Candidate = C::Candidate; fn complete( &self, line: &str, pos: usize, ctx: &Context<'_>, ) -> Result<(usize, Vec)> { (**self).complete(line, pos, ctx) } fn update(&self, line: &mut LineBuffer, start: usize, elected: &str) { (**self).update(line, start, elected) } } macro_rules! box_completer { ($($id: ident)*) => { $( impl Completer for $id { type Candidate = C::Candidate; fn complete(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Result<(usize, Vec)> { (**self).complete(line, pos, ctx) } fn update(&self, line: &mut LineBuffer, start: usize, elected: &str) { (**self).update(line, start, elected) } } )* } } use std::rc::Rc; use std::sync::Arc; box_completer! { Box Rc Arc } /// A `Completer` for file and folder names. pub struct FilenameCompleter { break_chars: &'static [u8], double_quotes_special_chars: &'static [u8], } const DOUBLE_QUOTES_ESCAPE_CHAR: Option = Some('\\'); cfg_if::cfg_if! { if #[cfg(unix)] { // rl_basic_word_break_characters, rl_completer_word_break_characters const DEFAULT_BREAK_CHARS: [u8; 18] = [ b' ', b'\t', b'\n', b'"', b'\\', b'\'', b'`', b'@', b'$', b'>', b'<', b'=', b';', b'|', b'&', b'{', b'(', b'\0', ]; const ESCAPE_CHAR: Option = Some('\\'); // In double quotes, not all break_chars need to be escaped // https://www.gnu.org/software/bash/manual/html_node/Double-Quotes.html const DOUBLE_QUOTES_SPECIAL_CHARS: [u8; 4] = [b'"', b'$', b'\\', b'`']; } else if #[cfg(windows)] { // Remove \ to make file completion works on windows const DEFAULT_BREAK_CHARS: [u8; 17] = [ b' ', b'\t', b'\n', b'"', b'\'', b'`', b'@', b'$', b'>', b'<', b'=', b';', b'|', b'&', b'{', b'(', b'\0', ]; const ESCAPE_CHAR: Option = None; const DOUBLE_QUOTES_SPECIAL_CHARS: [u8; 1] = [b'"']; // TODO Validate: only '"' ? } else if #[cfg(target_arch = "wasm32")] { const DEFAULT_BREAK_CHARS: [u8; 0] = []; const ESCAPE_CHAR: Option = None; const DOUBLE_QUOTES_SPECIAL_CHARS: [u8; 0] = []; } } #[derive(Clone, Copy, Debug, PartialEq)] pub enum Quote { Double, Single, None, } impl FilenameCompleter { pub fn new() -> Self { Self { break_chars: &DEFAULT_BREAK_CHARS, double_quotes_special_chars: &DOUBLE_QUOTES_SPECIAL_CHARS, } } 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]) { let start = idx + 1; if quote == Quote::Double { ( start, unescape(&line[start..pos], DOUBLE_QUOTES_ESCAPE_CHAR), DOUBLE_QUOTES_ESCAPE_CHAR, &self.double_quotes_special_chars, quote, ) } else { ( start, Borrowed(&line[start..pos]), None, &self.break_chars, quote, ) } } else { let (start, path) = extract_word(line, pos, ESCAPE_CHAR, &self.break_chars); let path = unescape(path, ESCAPE_CHAR); (start, path, ESCAPE_CHAR, &self.break_chars, Quote::None) }; let matches = filename_complete(&path, esc_char, break_chars, quote)?; Ok((start, matches)) } } impl Default for FilenameCompleter { fn default() -> Self { Self::new() } } impl Completer for FilenameCompleter { type Candidate = Pair; fn complete(&self, line: &str, pos: usize, _ctx: &Context<'_>) -> Result<(usize, Vec)> { self.complete_path(line, pos) } } /// Remove escape char pub fn unescape(input: &str, esc_char: Option) -> Cow<'_, str> { let esc_char = if let Some(c) = esc_char { c } else { return Borrowed(input); }; if !input.chars().any(|c| c == esc_char) { return Borrowed(input); } let mut result = String::with_capacity(input.len()); let mut chars = input.chars(); while let Some(ch) = chars.next() { if ch == esc_char { if let Some(ch) = chars.next() { if cfg!(windows) && ch != '"' { // TODO Validate: only '"' ? result.push(esc_char); } result.push(ch); } else if cfg!(windows) { result.push(ch); } } else { result.push(ch); } } Owned(result) } /// Escape any `break_chars` in `input` string with `esc_char`. /// For example, '/User Information' becomes '/User\ Information' /// when space is a breaking char and '\\' the escape char. pub fn escape( mut input: String, esc_char: Option, break_chars: &[u8], quote: Quote, ) -> String { if quote == Quote::Single { return input; // no escape in single quotes } let n = input .bytes() .filter(|b| memchr(*b, break_chars).is_some()) .count(); if n == 0 { return input; // no need to escape } let esc_char = if let Some(c) = esc_char { c } else { if cfg!(windows) && quote == Quote::None { input.insert(0, '"'); // force double quote return input; } return input; }; let mut result = String::with_capacity(input.len() + n); for c in input.chars() { if c.is_ascii() && memchr(c as u8, break_chars).is_some() { result.push(esc_char); } result.push(c); } result } fn filename_complete( path: &str, esc_char: Option, break_chars: &[u8], quote: Quote, ) -> Result> { #[cfg(feature = "with-dirs")] use dirs::home_dir; use std::env::current_dir; let sep = path::MAIN_SEPARATOR; let (dir_name, file_name) = match path.rfind(sep) { Some(idx) => path.split_at(idx + sep.len_utf8()), None => ("", path), }; let dir_path = Path::new(dir_name); let dir = if dir_path.starts_with("~") { // ~[/...] #[cfg(feature = "with-dirs")] { if let Some(home) = home_dir() { match dir_path.strip_prefix("~") { Ok(rel_path) => home.join(rel_path), _ => home, } } else { dir_path.to_path_buf() } } #[cfg(not(feature = "with-dirs"))] { dir_path.to_path_buf() } } else if dir_path.is_relative() { // TODO ~user[/...] (https://crates.io/crates/users) if let Ok(cwd) = current_dir() { cwd.join(dir_path) } else { dir_path.to_path_buf() } } else { dir_path.to_path_buf() }; let mut entries: Vec = Vec::new(); // if dir doesn't exist, then don't offer any completions if !dir.exists() { return Ok(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 } } } } } Ok(entries) } /// Given a `line` and a cursor `pos`ition, /// try to find backward the start of a word. /// Return (0, `line[..pos]`) if no break char has been found. /// Return the word and its start position (idx, `line[idx..pos]`) otherwise. pub fn extract_word<'l>( line: &'l str, pos: usize, esc_char: Option, break_chars: &[u8], ) -> (usize, &'l str) { let line = &line[..pos]; if line.is_empty() { return (0, line); } let mut start = None; for (i, c) in line.char_indices().rev() { if let (Some(esc_char), true) = (esc_char, start.is_some()) { if esc_char == c { // escaped break char start = None; continue; } else { break; } } if c.is_ascii() && memchr(c as u8, break_chars).is_some() { start = Some(i + c.len_utf8()); if esc_char.is_none() { break; } // else maybe escaped... } } match start { Some(start) => (start, &line[start..]), None => (0, line), } } 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()); } let mut longest_common_prefix = 0; 'o: loop { for (i, c1) in candidates.iter().enumerate().take(candidates.len() - 1) { let b1 = c1.replacement().as_bytes(); let b2 = candidates[i + 1].replacement().as_bytes(); if b1.len() <= longest_common_prefix || b2.len() <= longest_common_prefix || b1[longest_common_prefix] != b2[longest_common_prefix] { break 'o; } } longest_common_prefix += 1; } let candidate = candidates[0].replacement(); while !candidate.is_char_boundary(longest_common_prefix) { longest_common_prefix -= 1; } if longest_common_prefix == 0 { return None; } Some(&candidate[0..longest_common_prefix]) } #[derive(PartialEq)] enum ScanMode { DoubleQuote, Escape, EscapeInDoubleQuote, Normal, SingleQuote, } /// try to find an unclosed single/double quote in `s`. /// Return `None` if no unclosed quote is found. /// Return the unclosed quote position and if it is a double quote. fn find_unclosed_quote(s: &str) -> Option<(usize, Quote)> { let char_indices = s.char_indices(); let mut mode = ScanMode::Normal; let mut quote_index = 0; for (index, char) in char_indices { match mode { ScanMode::DoubleQuote => { if char == '"' { mode = ScanMode::Normal; } else if char == '\\' { // both windows and unix support escape in double quote mode = ScanMode::EscapeInDoubleQuote; } } ScanMode::Escape => { mode = ScanMode::Normal; } ScanMode::EscapeInDoubleQuote => { mode = ScanMode::DoubleQuote; } ScanMode::Normal => { if char == '"' { mode = ScanMode::DoubleQuote; quote_index = index; } else if char == '\\' && cfg!(not(windows)) { mode = ScanMode::Escape; } else if char == '\'' && cfg!(not(windows)) { mode = ScanMode::SingleQuote; quote_index = index; } } ScanMode::SingleQuote => { if char == '\'' { mode = ScanMode::Normal; } // no escape in single quotes } }; } if ScanMode::DoubleQuote == mode || ScanMode::EscapeInDoubleQuote == mode { return Some((quote_index, Quote::Double)); } else if ScanMode::SingleQuote == mode { return Some((quote_index, Quote::Single)); } None } #[cfg(test)] mod tests { #[test] pub fn extract_word() { let break_chars: &[u8] = &super::DEFAULT_BREAK_CHARS; let line = "ls '/usr/local/b"; assert_eq!( (4, "/usr/local/b"), super::extract_word(line, line.len(), Some('\\'), &break_chars) ); let line = "ls /User\\ Information"; assert_eq!( (3, "/User\\ Information"), super::extract_word(line, line.len(), Some('\\'), &break_chars) ); } #[test] pub fn unescape() { use std::borrow::Cow::{self, Borrowed, Owned}; let input = "/usr/local/b"; assert_eq!(Borrowed(input), super::unescape(input, Some('\\'))); if cfg!(windows) { let input = "c:\\users\\All Users\\"; let result: Cow<'_, str> = Borrowed(input); assert_eq!(result, super::unescape(input, Some('\\'))); } else { let input = "/User\\ Information"; let result: Cow<'_, str> = Owned(String::from("/User Information")); assert_eq!(result, super::unescape(input, Some('\\'))); } } #[test] pub fn escape() { let break_chars: &[u8] = &super::DEFAULT_BREAK_CHARS; let input = String::from("/usr/local/b"); assert_eq!( input.clone(), super::escape(input, Some('\\'), &break_chars, super::Quote::None) ); let input = String::from("/User Information"); let result = String::from("/User\\ Information"); assert_eq!( result, super::escape(input, Some('\\'), &break_chars, super::Quote::None) ); } #[test] pub fn longest_common_prefix() { let mut candidates = vec![]; { let lcp = super::longest_common_prefix(&candidates); assert!(lcp.is_none()); } let s = "User"; let c1 = String::from(s); candidates.push(c1.clone()); { let lcp = super::longest_common_prefix(&candidates); assert_eq!(Some(s), lcp); } let c2 = String::from("Users"); candidates.push(c2.clone()); { let lcp = super::longest_common_prefix(&candidates); assert_eq!(Some(s), lcp); } let c3 = String::from(""); candidates.push(c3.clone()); { let lcp = super::longest_common_prefix(&candidates); assert!(lcp.is_none()); } let candidates = vec![String::from("fée"), String::from("fête")]; let lcp = super::longest_common_prefix(&candidates); assert_eq!(Some("f"), lcp); } #[test] pub fn find_unclosed_quote() { assert_eq!(None, super::find_unclosed_quote("ls /etc")); assert_eq!( Some((3, super::Quote::Double)), super::find_unclosed_quote("ls \"User Information") ); assert_eq!( None, super::find_unclosed_quote("ls \"/User Information\" /etc") ); assert_eq!( Some((0, super::Quote::Double)), super::find_unclosed_quote("\"c:\\users\\All Users\\") ) } } rustyline-6.0.0/src/config.rs010064400007650000024000000265631357476110200144160ustar0000000000000000//! Customize line editor use std::default::Default; /// User preferences #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Config { /// Maximum number of entries in History. max_history_size: usize, // history_max_entries history_duplicates: HistoryDuplicates, history_ignore_space: bool, completion_type: CompletionType, /// When listing completion alternatives, only display /// one screen of possibilities at a time. completion_prompt_limit: usize, /// Duration (milliseconds) Rustyline will wait for a character when /// reading an ambiguous key sequence. keyseq_timeout: i32, /// Emacs or Vi mode edit_mode: EditMode, /// If true, each nonblank line returned by `readline` will be /// automatically added to the history. auto_add_history: bool, /// Beep or Flash or nothing bell_style: BellStyle, /// if colors should be enabled. color_mode: ColorMode, /// Whether to use stdout or stderr output_stream: OutputStreamType, /// Horizontal space taken by a tab. tab_stop: usize, } impl Config { pub fn builder() -> Builder { Builder::new() } /// Tell the maximum length (i.e. number of entries) for the history. pub fn max_history_size(&self) -> usize { self.max_history_size } pub(crate) fn set_max_history_size(&mut self, max_size: usize) { self.max_history_size = max_size; } /// Tell if lines which match the previous history entry are saved or not /// in the history list. /// /// By default, they are ignored. pub fn history_duplicates(&self) -> HistoryDuplicates { self.history_duplicates } pub(crate) fn set_history_ignore_dups(&mut self, yes: bool) { self.history_duplicates = if yes { HistoryDuplicates::IgnoreConsecutive } else { HistoryDuplicates::AlwaysAdd }; } /// Tell if lines which begin with a space character are saved or not in /// the history list. /// /// By default, they are saved. pub fn history_ignore_space(&self) -> bool { self.history_ignore_space } pub(crate) fn set_history_ignore_space(&mut self, yes: bool) { self.history_ignore_space = yes; } pub fn completion_type(&self) -> CompletionType { self.completion_type } pub fn completion_prompt_limit(&self) -> usize { self.completion_prompt_limit } pub fn keyseq_timeout(&self) -> i32 { self.keyseq_timeout } pub fn edit_mode(&self) -> EditMode { self.edit_mode } /// Tell if lines are automatically added to the history. /// /// By default, they are not. pub fn auto_add_history(&self) -> bool { self.auto_add_history } /// Bell style: beep, flash or nothing. pub fn bell_style(&self) -> BellStyle { self.bell_style } /// Tell if colors should be enabled. /// /// By default, they are except if stdout is not a TTY. pub fn color_mode(&self) -> ColorMode { self.color_mode } pub(crate) fn set_color_mode(&mut self, color_mode: ColorMode) { self.color_mode = color_mode; } pub fn output_stream(&self) -> OutputStreamType { self.output_stream } pub(crate) fn set_output_stream(&mut self, stream: OutputStreamType) { self.output_stream = stream; } /// Horizontal space taken by a tab. pub fn tab_stop(&self) -> usize { self.tab_stop } pub(crate) fn set_tab_stop(&mut self, tab_stop: usize) { self.tab_stop = tab_stop; } } impl Default for Config { fn default() -> Self { Self { max_history_size: 100, history_duplicates: HistoryDuplicates::IgnoreConsecutive, history_ignore_space: false, completion_type: CompletionType::Circular, // TODO Validate completion_prompt_limit: 100, keyseq_timeout: -1, edit_mode: EditMode::Emacs, auto_add_history: false, bell_style: BellStyle::default(), color_mode: ColorMode::Enabled, output_stream: OutputStreamType::Stdout, tab_stop: 8, } } } /// Beep or flash or nothing #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum BellStyle { /// Beep Audible, /// Silent None, /// Flash screen (not supported) Visible, } /// `Audible` by default on unix (overriden by current Terminal settings). /// `None` on windows. impl Default for BellStyle { #[cfg(any(windows, target_arch = "wasm32"))] fn default() -> Self { BellStyle::None } #[cfg(unix)] fn default() -> Self { BellStyle::Audible } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum HistoryDuplicates { AlwaysAdd, /// a line will not be added to the history if it matches the previous entry IgnoreConsecutive, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum CompletionType { /// Complete the next full match (like in Vim by default) Circular, /// Complete till longest match. /// When more than one match, list all matches /// (like in Bash/Readline). List, } /// Style of editing / Standard keymaps #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum EditMode { Emacs, Vi, } /// Colorization mode #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ColorMode { Enabled, Forced, Disabled, } /// Should the editor use stdout or stderr // TODO console term::TermTarget #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum OutputStreamType { Stderr, Stdout, } /// Configuration builder #[derive(Debug, Default)] pub struct Builder { p: Config, } impl Builder { pub fn new() -> Self { Self { p: Config::default(), } } /// Set the maximum length for the history. pub fn max_history_size(mut self, max_size: usize) -> Self { self.set_max_history_size(max_size); self } /// Tell if lines which match the previous history entry are saved or not /// in the history list. /// /// By default, they are ignored. pub fn history_ignore_dups(mut self, yes: bool) -> Self { self.set_history_ignore_dups(yes); self } /// Tell if lines which begin with a space character are saved or not in /// the history list. /// /// By default, they are saved. pub fn history_ignore_space(mut self, yes: bool) -> Self { self.set_history_ignore_space(yes); self } /// Set `completion_type`. pub fn completion_type(mut self, completion_type: CompletionType) -> Self { self.set_completion_type(completion_type); self } /// The number of possible completions that determines when the user is /// asked whether the list of possibilities should be displayed. pub fn completion_prompt_limit(mut self, completion_prompt_limit: usize) -> Self { self.set_completion_prompt_limit(completion_prompt_limit); self } /// Timeout for ambiguous key sequences in milliseconds. /// Currently, it is used only to distinguish a single ESC from an ESC /// sequence. /// After seeing an ESC key, wait at most `keyseq_timeout_ms` for another /// byte. pub fn keyseq_timeout(mut self, keyseq_timeout_ms: i32) -> Self { self.set_keyseq_timeout(keyseq_timeout_ms); self } /// Choose between Emacs or Vi mode. pub fn edit_mode(mut self, edit_mode: EditMode) -> Self { self.set_edit_mode(edit_mode); self } /// Tell if lines are automatically added to the history. /// /// By default, they are not. pub fn auto_add_history(mut self, yes: bool) -> Self { self.set_auto_add_history(yes); self } /// Set bell style: beep, flash or nothing. pub fn bell_style(mut self, bell_style: BellStyle) -> Self { self.set_bell_style(bell_style); self } /// Forces colorization on or off. /// /// By default, colorization is on except if stdout is not a TTY. pub fn color_mode(mut self, color_mode: ColorMode) -> Self { self.set_color_mode(color_mode); self } /// Whether to use stdout or stderr. /// /// Be default, use stdout pub fn output_stream(mut self, stream: OutputStreamType) -> Self { self.set_output_stream(stream); self } /// Horizontal space taken by a tab. /// /// By default, `8` pub fn tab_stop(mut self, tab_stop: usize) -> Self { self.set_tab_stop(tab_stop); self } pub fn build(self) -> Config { self.p } } impl Configurer for Builder { fn config_mut(&mut self) -> &mut Config { &mut self.p } } pub trait Configurer { fn config_mut(&mut self) -> &mut Config; /// Set the maximum length for the history. fn set_max_history_size(&mut self, max_size: usize) { self.config_mut().set_max_history_size(max_size); } /// Tell if lines which match the previous history entry are saved or not /// in the history list. /// /// By default, they are ignored. fn set_history_ignore_dups(&mut self, yes: bool) { self.config_mut().set_history_ignore_dups(yes); } /// Tell if lines which begin with a space character are saved or not in /// the history list. /// /// By default, they are saved. fn set_history_ignore_space(&mut self, yes: bool) { self.config_mut().set_history_ignore_space(yes); } /// Set `completion_type`. fn set_completion_type(&mut self, completion_type: CompletionType) { self.config_mut().completion_type = completion_type; } /// The number of possible completions that determines when the user is /// asked whether the list of possibilities should be displayed. fn set_completion_prompt_limit(&mut self, completion_prompt_limit: usize) { self.config_mut().completion_prompt_limit = completion_prompt_limit; } /// Timeout for ambiguous key sequences in milliseconds. fn set_keyseq_timeout(&mut self, keyseq_timeout_ms: i32) { self.config_mut().keyseq_timeout = keyseq_timeout_ms; } /// Choose between Emacs or Vi mode. fn set_edit_mode(&mut self, edit_mode: EditMode) { self.config_mut().edit_mode = edit_mode; match edit_mode { EditMode::Emacs => self.set_keyseq_timeout(-1), // no timeout EditMode::Vi => self.set_keyseq_timeout(500), } } /// Tell if lines are automatically added to the history. /// /// By default, they are not. fn set_auto_add_history(&mut self, yes: bool) { self.config_mut().auto_add_history = yes; } /// Set bell style: beep, flash or nothing. fn set_bell_style(&mut self, bell_style: BellStyle) { self.config_mut().bell_style = bell_style; } /// Forces colorization on or off. /// /// By default, colorization is on except if stdout is not a TTY. fn set_color_mode(&mut self, color_mode: ColorMode) { self.config_mut().set_color_mode(color_mode); } /// Whether to use stdout or stderr /// /// By default, use stdout fn set_output_stream(&mut self, stream: OutputStreamType) { self.config_mut().set_output_stream(stream); } /// Horizontal space taken by a tab. /// /// By default, `8` fn set_tab_stop(&mut self, tab_stop: usize) { self.config_mut().set_tab_stop(tab_stop); } } rustyline-6.0.0/src/edit.rs010064400007650000024000000543621360441142000140610ustar0000000000000000//! Command processor use log::debug; use std::cell::RefCell; use std::fmt; use std::rc::Rc; use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthChar; use super::{Context, Helper, Result}; use crate::highlight::Highlighter; use crate::history::Direction; use crate::keymap::{Anchor, At, CharSearch, Cmd, Movement, RepeatCount, Word}; 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}; /// 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 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 } enum Info<'m> { NoHint, Hint, Msg(Option<&'m str>), } impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { pub fn new( out: &'out mut ::Writer, prompt: &'prompt str, helper: Option<&'out H>, ctx: Context<'out>, ) -> State<'out, 'prompt, H> { let prompt_size = out.calculate_position(prompt, Position::default()); State { out, prompt, prompt_size, line: LineBuffer::with_capacity(MAX_LINE).can_growth(true), layout: Layout::default(), saved_line_for_history: LineBuffer::with_capacity(MAX_LINE).can_growth(true), byte_buffer: [0; 4], changes: Rc::new(RefCell::new(Changeset::new())), helper, ctx, hint: None, highlight_char: false, } } pub fn highlighter(&self) -> Option<&dyn Highlighter> { if self.out.colors_enabled() { self.helper.map(|h| h as &dyn Highlighter) } else { None } } pub fn next_cmd( &mut self, input_state: &mut InputState, rdr: &mut ::Reader, single_esc_abort: bool, ) -> Result { loop { 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.refresh_line()?; continue; } if let Ok(Cmd::Replace(_, _)) = rc { self.changes.borrow_mut().begin(); } return rc; } } pub fn backup(&mut self) { self.saved_line_for_history .update(self.line.as_str(), self.line.pos()); } pub fn restore(&mut self) { self.line.update( self.saved_line_for_history.as_str(), self.saved_line_for_history.pos(), ); } pub fn move_cursor(&mut self) -> Result<()> { // calculate the desired position of the cursor let cursor = self .out .calculate_position(&self.line[..self.line.pos()], self.prompt_size); if self.layout.cursor == cursor { return Ok(()); } if self.highlight_char() { let prompt_size = self.prompt_size; self.refresh(self.prompt, prompt_size, true, 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); debug_assert!(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<()> { let info = match info { Info::NoHint => None, Info::Hint => self.hint.as_ref().map(String::as_str), Info::Msg(msg) => msg, }; let highlighter = if self.out.colors_enabled() { self.helper.map(|h| h as &dyn Highlighter) } else { 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); debug!(target: "rustyline", "old layout: {:?}", self.layout); debug!(target: "rustyline", "new layout: {:?}", new_layout); self.out.refresh_line( prompt, &self.line, info, &self.layout, &new_layout, highlighter, )?; self.layout = new_layout; Ok(()) } 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; } else { self.hint = None } } fn highlight_char(&mut self) -> bool { if let Some(highlighter) = self.highlighter() { let highlight_char = highlighter.highlight_char(&self.line, self.line.pos()); if highlight_char { self.highlight_char = true; true } else if self.highlight_char { // previously highlighted => force a full refresh self.highlight_char = false; true } else { false } } else { false } } 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(); let validated = match result { ValidationResult::Incomplete => { self.edit_move_end()?; self.edit_insert('\n', 1)?; false } ValidationResult::Valid(msg) => { // Accept the line regardless of where the cursor is. self.edit_move_end()?; 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)?; } true } ValidationResult::Invalid(msg) => { if corrected || self.has_hint() || msg.is_some() { self.refresh_line_with_msg(msg)?; } false } }; Ok(validated) } else { Ok(true) } } } 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) } fn refresh_line_with_msg(&mut self, msg: Option) -> Result<()> { let prompt_size = self.prompt_size; self.hint = None; self.highlight_char(); self.refresh( self.prompt, prompt_size, true, Info::Msg(msg.as_ref().map(String::as_str)), ) } fn refresh_prompt_and_line(&mut self, prompt: &str) -> Result<()> { let prompt_size = self.out.calculate_position(prompt, Position::default()); self.hint(); self.highlight_char(); self.refresh(prompt, prompt_size, false, Info::Hint) } fn doing_insert(&mut self) { self.changes.borrow_mut().begin(); } fn done_inserting(&mut self) { self.changes.borrow_mut().end(); } fn last_insert(&self) -> Option { self.changes.borrow().last_insert() } fn is_cursor_at_end(&self) -> bool { self.line.pos() == self.line.len() } fn has_hint(&self) -> bool { self.hint.is_some() } } 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) .field("saved_line_for_history", &self.saved_line_for_history) .finish() } } impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { pub fn clear_screen(&mut self) -> Result<()> { self.out.clear_screen()?; self.layout.cursor = Position::default(); self.layout.end = Position::default(); Ok(()) } /// Insert the character `ch` at cursor current position. pub fn edit_insert(&mut self, ch: char, n: RepeatCount) -> Result<()> { if let Some(push) = self.line.insert(ch, n) { if push { let prompt_size = self.prompt_size; let no_previous_hint = self.hint.is_none(); self.hint(); let width = ch.width().unwrap_or(0); if n == 1 && width != 0 // Ctrl-V + \t or \n ... && self.layout.cursor.col + width < self.out.get_columns() && (self.hint.is_none() && no_previous_hint) // TODO refresh only current line && !self.highlight_char() { // 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) } } else { self.refresh_line() } } else { Ok(()) } } /// Replace a single (or n) character(s) under the cursor (Vi mode) pub fn edit_replace_char(&mut self, ch: char, n: RepeatCount) -> Result<()> { self.changes.borrow_mut().begin(); let succeed = if let Some(chars) = self.line.delete(n) { let count = chars.graphemes(true).count(); self.line.insert(ch, count); self.line.move_backward(1); true } else { false }; self.changes.borrow_mut().end(); if succeed { self.refresh_line() } else { Ok(()) } } /// Overwrite the character under the cursor (Vi mode) pub fn edit_overwrite_char(&mut self, ch: char) -> Result<()> { if let Some(end) = self.line.next_pos(1) { { let text = ch.encode_utf8(&mut self.byte_buffer); let start = self.line.pos(); self.line.replace(start..end, text); } self.refresh_line() } else { Ok(()) } } // Yank/paste `text` at current position. pub fn edit_yank( &mut self, input_state: &InputState, text: &str, anchor: Anchor, n: RepeatCount, ) -> Result<()> { if let Anchor::After = anchor { self.line.move_forward(1); } if self.line.yank(text, n).is_some() { if !input_state.is_emacs_mode() { self.line.move_backward(1); } self.refresh_line() } else { Ok(()) } } // Delete previously yanked text and yank/paste `text` at current position. pub fn edit_yank_pop(&mut self, yank_size: usize, text: &str) -> Result<()> { self.changes.borrow_mut().begin(); let result = if self.line.yank_pop(yank_size, text).is_some() { self.refresh_line() } else { Ok(()) }; self.changes.borrow_mut().end(); result } /// Move cursor on the left. pub fn edit_move_backward(&mut self, n: RepeatCount) -> Result<()> { if self.line.move_backward(n) { self.move_cursor() } else { Ok(()) } } /// Move cursor on the right. pub fn edit_move_forward(&mut self, n: RepeatCount) -> Result<()> { if self.line.move_forward(n) { self.move_cursor() } else { Ok(()) } } /// Move cursor to the start of the line. pub fn edit_move_home(&mut self) -> Result<()> { if self.line.move_home() { self.move_cursor() } else { Ok(()) } } /// Move cursor to the end of the line. pub fn edit_move_end(&mut self) -> Result<()> { if self.line.move_end() { self.move_cursor() } else { Ok(()) } } pub fn edit_kill(&mut self, mvt: &Movement) -> Result<()> { if self.line.kill(mvt) { self.refresh_line() } else { Ok(()) } } pub fn edit_insert_text(&mut self, text: &str) -> Result<()> { if text.is_empty() { return Ok(()); } let cursor = self.line.pos(); self.line.insert_str(cursor, text); 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(); let succeed = self.line.transpose_chars(); self.changes.borrow_mut().end(); if succeed { self.refresh_line() } else { Ok(()) } } pub fn edit_move_to_prev_word(&mut self, word_def: Word, n: RepeatCount) -> Result<()> { if self.line.move_to_prev_word(word_def, n) { self.move_cursor() } else { Ok(()) } } pub fn edit_move_to_next_word(&mut self, at: At, word_def: Word, n: RepeatCount) -> Result<()> { if self.line.move_to_next_word(at, word_def, n) { self.move_cursor() } else { Ok(()) } } pub fn edit_move_to(&mut self, cs: CharSearch, n: RepeatCount) -> Result<()> { if self.line.move_to(cs, n) { self.move_cursor() } else { Ok(()) } } pub fn edit_word(&mut self, a: WordAction) -> Result<()> { self.changes.borrow_mut().begin(); let succeed = self.line.edit_word(a); self.changes.borrow_mut().end(); if succeed { self.refresh_line() } else { Ok(()) } } pub fn edit_transpose_words(&mut self, n: RepeatCount) -> Result<()> { self.changes.borrow_mut().begin(); let succeed = self.line.transpose_words(n); self.changes.borrow_mut().end(); if succeed { self.refresh_line() } else { Ok(()) } } /// Substitute the currently edited line with the next or previous history /// entry. pub fn edit_history_next(&mut self, prev: bool) -> Result<()> { let history = self.ctx.history; if history.is_empty() { return Ok(()); } if self.ctx.history_index == history.len() { if prev { // Save the current edited line before overwriting it self.backup(); } else { return Ok(()); } } else if self.ctx.history_index == 0 && prev { return Ok(()); } if prev { self.ctx.history_index -= 1; } else { self.ctx.history_index += 1; } if self.ctx.history_index < history.len() { let buf = history.get(self.ctx.history_index).unwrap(); self.changes.borrow_mut().begin(); self.line.update(buf, buf.len()); self.changes.borrow_mut().end(); } else { // Restore current edited line self.restore(); } self.refresh_line() } // Non-incremental, anchored search pub fn edit_history_search(&mut self, dir: Direction) -> 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 { return self.out.beep(); } if dir == Direction::Reverse { self.ctx.history_index -= 1; } else { self.ctx.history_index += 1; } if let Some(history_index) = 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.changes.borrow_mut().begin(); self.line.update(buf, buf.len()); self.changes.borrow_mut().end(); self.refresh_line() } else { self.out.beep() } } /// Substitute the currently edited line with the first/last history entry. pub fn edit_history(&mut self, first: bool) -> Result<()> { let history = self.ctx.history; if history.is_empty() { return Ok(()); } if self.ctx.history_index == history.len() { if first { // Save the current edited line before overwriting it self.backup(); } else { return Ok(()); } } else if self.ctx.history_index == 0 && first { return Ok(()); } if first { self.ctx.history_index = 0; let buf = history.get(self.ctx.history_index).unwrap(); self.changes.borrow_mut().begin(); self.line.update(buf, buf.len()); self.changes.borrow_mut().end(); } else { self.ctx.history_index = history.len(); // Restore current edited line self.restore(); } self.refresh_line() } } #[cfg(test)] pub fn init_state<'out, H: Helper>( out: &'out mut ::Writer, line: &str, pos: usize, helper: Option<&'out H>, history: &'out crate::history::History, ) -> State<'out, 'static, H> { State { out, prompt: "", prompt_size: Position::default(), line: LineBuffer::init(line, pos, None), layout: Layout::default(), saved_line_for_history: LineBuffer::with_capacity(100), byte_buffer: [0; 4], changes: Rc::new(RefCell::new(Changeset::new())), helper, ctx: Context::new(history), hint: Some("hint".to_owned()), highlight_char: false, } } #[cfg(test)] mod test { use super::init_state; use crate::history::History; use crate::tty::Sink; #[test] fn edit_history_next() { let mut out = Sink::new(); let mut history = History::new(); history.add("line0"); history.add("line1"); let line = "current edited line"; let helper: Option<()> = None; let mut s = init_state(&mut out, line, 6, helper.as_ref(), &history); s.ctx.history_index = history.len(); for _ in 0..2 { s.edit_history_next(false).unwrap(); assert_eq!(line, s.line.as_str()); } s.edit_history_next(true).unwrap(); assert_eq!(line, s.saved_line_for_history.as_str()); assert_eq!(1, s.ctx.history_index); assert_eq!("line1", s.line.as_str()); for _ in 0..2 { s.edit_history_next(true).unwrap(); assert_eq!(line, s.saved_line_for_history.as_str()); assert_eq!(0, s.ctx.history_index); assert_eq!("line0", s.line.as_str()); } s.edit_history_next(false).unwrap(); assert_eq!(line, s.saved_line_for_history.as_str()); assert_eq!(1, s.ctx.history_index); assert_eq!("line1", s.line.as_str()); s.edit_history_next(false).unwrap(); // assert_eq!(line, s.saved_line_for_history); assert_eq!(2, s.ctx.history_index); assert_eq!(line, s.line.as_str()); } } rustyline-6.0.0/src/error.rs010064400007650000024000000050741347466344300143040ustar0000000000000000//! 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)] pub enum ReadlineError { /// I/O Error Io(io::Error), /// EOF (Ctrl-D) Eof, /// Ctrl-C Interrupted, /// Chars Error #[cfg(unix)] Utf8Error, /// Unix Error from syscall #[cfg(unix)] Errno(nix::Error), #[cfg(windows)] WindowResize, #[cfg(windows)] Decode(char::DecodeUtf16Error), } impl fmt::Display for ReadlineError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { ReadlineError::Io(ref err) => err.fmt(f), ReadlineError::Eof => write!(f, "EOF"), ReadlineError::Interrupted => write!(f, "Interrupted"), #[cfg(unix)] ReadlineError::Utf8Error => write!(f, "invalid utf-8: corrupt contents"), #[cfg(unix)] ReadlineError::Errno(ref err) => err.fmt(f), #[cfg(windows)] 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(), } } } impl From for ReadlineError { fn from(err: io::Error) -> Self { ReadlineError::Io(err) } } impl From for ReadlineError { fn from(kind: io::ErrorKind) -> Self { ReadlineError::Io(io::Error::from(kind)) } } #[cfg(unix)] impl From for ReadlineError { fn from(err: nix::Error) -> Self { ReadlineError::Errno(err) } } #[cfg(windows)] impl From for ReadlineError { fn from(err: char::DecodeUtf16Error) -> Self { ReadlineError::Decode(err) } } rustyline-6.0.0/src/highlight.rs010064400007650000024000000207411352702755400151140ustar0000000000000000//! Syntax highlighting use crate::config::CompletionType; use memchr::memchr; use std::borrow::Cow::{self, Borrowed, Owned}; use std::cell::Cell; /// Syntax highlighter with [ANSI color](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters). /// Rustyline will try to handle escape sequence for ANSI color on windows /// when not supported natively (windows <10). /// /// Currently, the highlighted version *must* have the same display width as /// the original input. pub trait Highlighter { /// Takes the currently edited `line` with the cursor `pos`ition and /// returns the highlighted version (with ANSI color). /// /// For example, you can implement /// [blink-matching-paren](https://www.gnu.org/software/bash/manual/html_node/Readline-Init-File-Syntax.html). fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> { let _ = pos; Borrowed(line) } /// Takes the `prompt` and /// returns the highlighted version (with ANSI color). fn highlight_prompt<'b, 's: 'b, 'p: 'b>( &'s self, prompt: &'p str, default: bool, ) -> Cow<'b, str> { let _ = default; Borrowed(prompt) } /// Takes the `hint` and /// returns the highlighted version (with ANSI color). fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { Borrowed(hint) } /// Takes the completion `candidate` and /// returns the highlighted version (with ANSI color). /// /// Currently, used only with `CompletionType::List`. fn highlight_candidate<'c>( &self, candidate: &'c str, completion: CompletionType, ) -> Cow<'c, str> { let _ = completion; Borrowed(candidate) } /// Tells if `line` needs to be highlighted when a specific char is typed or /// when cursor is moved under a specific char. /// /// Used to optimize refresh when a character is inserted or the cursor is /// moved. fn highlight_char(&self, line: &str, pos: usize) -> bool { let _ = (line, pos); false } } impl Highlighter for () {} impl<'r, H: ?Sized + Highlighter> Highlighter for &'r H { fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> { (**self).highlight(line, pos) } fn highlight_prompt<'b, 's: 'b, 'p: 'b>( &'s self, prompt: &'p str, default: bool, ) -> Cow<'b, str> { (**self).highlight_prompt(prompt, default) } fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { (**self).highlight_hint(hint) } fn highlight_candidate<'c>( &self, candidate: &'c str, completion: CompletionType, ) -> Cow<'c, str> { (**self).highlight_candidate(candidate, completion) } fn highlight_char(&self, line: &str, pos: usize) -> bool { (**self).highlight_char(line, pos) } } const OPENS: &[u8; 3] = b"{[("; const CLOSES: &[u8; 3] = b"}])"; #[derive(Default)] pub struct MatchingBracketHighlighter { bracket: Cell>, // memorize the character to search... } impl MatchingBracketHighlighter { pub fn new() -> Self { Self { bracket: Cell::new(None), } } } impl Highlighter for MatchingBracketHighlighter { fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> { if line.len() <= 1 { return Borrowed(line); } // highlight matching brace/bracket/parenthesis if it exists if let Some((bracket, pos)) = self.bracket.get() { if let Some((matching, idx)) = find_matching_bracket(line, pos, bracket) { let mut copy = line.to_owned(); copy.replace_range(idx..=idx, &format!("\x1b[1;34m{}\x1b[0m", matching as char)); return Owned(copy); } } Borrowed(line) } fn highlight_char(&self, line: &str, pos: usize) -> bool { // will highlight matching brace/bracket/parenthesis if it exists self.bracket.set(check_bracket(line, pos)); self.bracket.get().is_some() } } fn find_matching_bracket(line: &str, pos: usize, bracket: u8) -> Option<(u8, usize)> { let matching = matching_bracket(bracket); let mut idx; let mut unmatched = 1; if is_open_bracket(bracket) { // forward search idx = pos + 1; let bytes = &line.as_bytes()[idx..]; for b in bytes { if *b == matching { unmatched -= 1; if unmatched == 0 { debug_assert_eq!(matching, line.as_bytes()[idx]); return Some((matching, idx)); } } else if *b == bracket { unmatched += 1; } idx += 1; } debug_assert_eq!(idx, line.len()); } else { // backward search idx = pos; let bytes = &line.as_bytes()[..idx]; for b in bytes.iter().rev() { if *b == matching { unmatched -= 1; if unmatched == 0 { debug_assert_eq!(matching, line.as_bytes()[idx - 1]); return Some((matching, idx - 1)); } } else if *b == bracket { unmatched += 1; } idx -= 1; } debug_assert_eq!(idx, 0); } None } // check under or before the cursor fn check_bracket(line: &str, pos: usize) -> Option<(u8, usize)> { if line.is_empty() { return None; } let mut pos = pos; if pos >= line.len() { pos = line.len() - 1; // before cursor let b = line.as_bytes()[pos]; // previous byte if is_close_bracket(b) { Some((b, pos)) } else { None } } else { let mut under_cursor = true; loop { let b = line.as_bytes()[pos]; if is_close_bracket(b) { if pos == 0 { return None; } else { return Some((b, pos)); } } else if is_open_bracket(b) { if pos + 1 == line.len() { return None; } else { return Some((b, pos)); } } else if under_cursor && pos > 0 { under_cursor = false; pos -= 1; // or before cursor } else { return None; } } } } fn matching_bracket(bracket: u8) -> u8 { match bracket { b'{' => b'}', b'}' => b'{', b'[' => b']', b']' => b'[', b'(' => b')', b')' => b'(', b => b, } } fn is_open_bracket(bracket: u8) -> bool { memchr(bracket, OPENS).is_some() } fn is_close_bracket(bracket: u8) -> bool { memchr(bracket, CLOSES).is_some() } #[cfg(test)] mod tests { #[test] pub fn find_matching_bracket() { use super::find_matching_bracket; assert_eq!(find_matching_bracket("(...", 0, b'('), None); assert_eq!(find_matching_bracket("...)", 3, b')'), None); assert_eq!(find_matching_bracket("()..", 0, b'('), Some((b')', 1))); assert_eq!(find_matching_bracket("(..)", 0, b'('), Some((b')', 3))); assert_eq!(find_matching_bracket("..()", 3, b')'), Some((b'(', 2))); assert_eq!(find_matching_bracket("(..)", 3, b')'), Some((b'(', 0))); assert_eq!(find_matching_bracket("(())", 0, b'('), Some((b')', 3))); assert_eq!(find_matching_bracket("(())", 3, b')'), Some((b'(', 0))); } #[test] pub fn check_bracket() { use super::check_bracket; assert_eq!(check_bracket(")...", 0), None); assert_eq!(check_bracket("(...", 2), None); assert_eq!(check_bracket("...(", 3), None); assert_eq!(check_bracket("...(", 4), None); assert_eq!(check_bracket("..).", 4), None); assert_eq!(check_bracket("(...", 0), Some((b'(', 0))); assert_eq!(check_bracket("(...", 1), Some((b'(', 0))); assert_eq!(check_bracket("...)", 3), Some((b')', 3))); assert_eq!(check_bracket("...)", 4), Some((b')', 3))); } #[test] pub fn matching_bracket() { use super::matching_bracket; assert_eq!(matching_bracket(b'('), b')'); assert_eq!(matching_bracket(b')'), b'('); } #[test] pub fn is_open_bracket() { use super::is_close_bracket; use super::is_open_bracket; assert!(is_open_bracket(b'(')); assert!(is_close_bracket(b')')); } } rustyline-6.0.0/src/hint.rs010064400007650000024000000036771352702755400141200ustar0000000000000000//! Hints (suggestions at the right of the prompt as you type). use crate::history::Direction; use crate::Context; /// Hints provider pub trait Hinter { /// Takes the currently edited `line` with the cursor `pos`ition and /// returns the string that should be displayed or `None` /// if no hint is available for the text the user currently typed. // TODO Validate: called while editing line but not while moving cursor. fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option { let _ = (line, pos, ctx); None } } impl Hinter for () {} impl<'r, H: ?Sized + Hinter> Hinter for &'r H { fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option { (**self).hint(line, pos, ctx) } } pub struct HistoryHinter {} impl Hinter for HistoryHinter { fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option { if pos < line.len() { return None; } let start = if ctx.history_index() == ctx.history().len() { ctx.history_index().saturating_sub(1) } else { ctx.history_index() }; if let Some(history_index) = ctx.history .starts_with(&line[..pos], start, Direction::Reverse) { let entry = ctx.history.get(history_index); if let Some(entry) = entry { if entry == line || entry == &line[..pos] { return None; } } return entry.map(|s| s[pos..].to_owned()); } None } } #[cfg(test)] mod test { use super::{Hinter, HistoryHinter}; use crate::history::History; use crate::Context; #[test] pub fn empty_history() { let history = History::new(); let ctx = Context::new(&history); let hinter = HistoryHinter {}; let hint = hinter.hint("test", 4, &ctx); assert_eq!(None, hint); } } rustyline-6.0.0/src/history.rs010064400007650000024000000235351357476110200146460ustar0000000000000000//! History API #[cfg(unix)] use libc; use std::collections::vec_deque; use std::collections::VecDeque; use std::fs::File; use std::iter::DoubleEndedIterator; use std::ops::Index; use std::path::Path; use super::Result; use crate::config::{Config, HistoryDuplicates}; /// Search direction #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Direction { Forward, Reverse, } /// Current state of the history. #[derive(Default)] pub struct History { entries: VecDeque, max_len: usize, pub(crate) ignore_space: bool, pub(crate) ignore_dups: bool, } impl History { pub fn new() -> Self { Self::with_config(Config::default()) } 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, } } /// Return the history entry at position `index`, starting from 0. pub fn get(&self, index: usize) -> Option<&String> { self.entries.get(index) } /// Return the last history entry (i.e. previous command) pub fn last(&self) -> Option<&String> { self.entries.back() } /// Add a new entry in the history. pub fn add + Into>(&mut self, line: S) -> bool { if self.max_len == 0 { return false; } if line.as_ref().is_empty() || (self.ignore_space && line .as_ref() .chars() .next() .map_or(true, char::is_whitespace)) { return false; } if self.ignore_dups { if let Some(s) = self.entries.back() { if s == line.as_ref() { return false; } } } if self.entries.len() == self.max_len { self.entries.pop_front(); } self.entries.push_back(line.into()); true } /// Return the number of entries in the history. pub fn len(&self) -> usize { self.entries.len() } /// Return true if the history has no entry. pub fn is_empty(&self) -> bool { self.entries.is_empty() } /// Set the maximum length for the history. This function can be called even /// if there is already some history, the function will make sure to retain /// just the latest `len` elements if the new history length value is /// smaller than the amount of items already inside the history. /// /// Like [stifle_history](http://cnswww.cns.cwru. /// edu/php/chet/readline/history.html#IDX11). pub fn set_max_len(&mut self, len: usize) { self.max_len = len; if len == 0 { self.entries.clear(); return; } loop { if self.entries.len() <= len { break; } self.entries.pop_front(); } } /// 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() { return Ok(()); } let old_umask = umask(); let f = File::create(path); restore_umask(old_umask); let file = f?; fix_perm(&file); let mut wtr = BufWriter::new(file); for entry in &self.entries { wtr.write_all(entry.as_bytes())?; wtr.write_all(b"\n")?; } // https://github.com/rust-lang/rust/issues/32677#issuecomment-204833485 wtr.flush()?; 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<()> { 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 } Ok(()) } /// Clear history pub fn clear(&mut self) { self.entries.clear() } /// Search history (start position inclusive [0, len-1]). /// /// Return the absolute index of the nearest history entry that matches /// `term`. /// /// Return None if no entry contains `term` between [start, len -1] for /// forward search /// 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) } /// 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) } fn search_match(&self, term: &str, start: usize, dir: Direction, test: F) -> Option where F: Fn(&String) -> bool, { if term.is_empty() || start >= self.len() { return None; } match dir { Direction::Reverse => { let index = self .entries .iter() .rev() .skip(self.entries.len() - 1 - start) .position(test); index.map(|index| start - index) } Direction::Forward => { let index = self.entries.iter().skip(start).position(test); index.map(|index| index + start) } } } /// Return a forward iterator. pub fn iter(&self) -> Iter<'_> { Iter(self.entries.iter()) } } impl Index for History { type Output = String; fn index(&self, index: usize) -> &String { &self.entries[index] } } impl<'a> IntoIterator for &'a History { type IntoIter = Iter<'a>; type Item = &'a String; fn into_iter(self) -> Iter<'a> { self.iter() } } /// History iterator. pub struct Iter<'a>(vec_deque::Iter<'a, String>); impl<'a> Iterator for Iter<'a> { type Item = &'a String; fn next(&mut self) -> Option<&'a String> { self.0.next() } fn size_hint(&self) -> (usize, Option) { self.0.size_hint() } } impl<'a> DoubleEndedIterator for Iter<'a> { fn next_back(&mut self) -> Option<&'a String> { self.0.next_back() } } cfg_if::cfg_if! { if #[cfg(any(windows, target_arch = "wasm32"))] { fn umask() -> u16 { 0 } fn restore_umask(_: u16) {} fn fix_perm(_: &File) {} } else if #[cfg(unix)] { fn umask() -> libc::mode_t { unsafe { libc::umask(libc::S_IXUSR | libc::S_IRWXG | libc::S_IRWXO) } } fn restore_umask(old_umask: libc::mode_t) { unsafe { libc::umask(old_umask); } } fn fix_perm(file: &File) { use std::os::unix::io::AsRawFd; unsafe { libc::fchmod(file.as_raw_fd(), libc::S_IRUSR | libc::S_IWUSR); } } } } #[cfg(test)] mod tests { use super::{Direction, History}; use crate::config::Config; use std::path::Path; use tempdir; fn init() -> History { let mut history = History::new(); assert!(history.add("line1")); assert!(history.add("line2")); assert!(history.add("line3")); history } #[test] fn new() { let history = History::new(); assert_eq!(0, history.entries.len()); } #[test] fn add() { let config = Config::builder().history_ignore_space(true).build(); let mut history = History::with_config(config); assert_eq!(config.max_history_size(), history.max_len); assert!(history.add("line1")); assert!(history.add("line2")); assert!(!history.add("line2")); assert!(!history.add("")); assert!(!history.add(" line3")); } #[test] fn set_max_len() { let mut history = init(); history.set_max_len(1); assert_eq!(1, history.entries.len()); assert_eq!(Some(&"line3".to_owned()), history.last()); } #[test] fn save() { let mut history = init(); let td = tempdir::TempDir::new_in(&Path::new("."), "histo").unwrap(); let history_path = td.path().join(".history"); history.save(&history_path).unwrap(); history.load(&history_path).unwrap(); td.close().unwrap(); } #[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)); } #[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!(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)); } } rustyline-6.0.0/src/keymap.rs010064400007650000024000001040371360441142000144150ustar0000000000000000//! Bindings from keys to command for Emacs and Vi modes use std::collections::HashMap; use std::sync::{Arc, RwLock}; use log::debug; use super::Result; use crate::config::Config; use crate::config::EditMode; use crate::keys::KeyPress; use crate::tty::{RawReader, Term, Terminal}; /// The number of times one command should be repeated. pub type RepeatCount = usize; /// Commands // #[non_exhaustive] #[derive(Debug, Clone, PartialEq)] pub enum Cmd { /// abort Abort, // Miscellaneous Command /// accept-line AcceptLine, /// beginning-of-history BeginningOfHistory, /// capitalize-word CapitalizeWord, /// clear-screen ClearScreen, /// complete Complete, /// complete-backward CompleteBackward, /// complete-hint CompleteHint, /// downcase-word DowncaseWord, /// vi-eof-maybe EndOfFile, /// end-of-history EndOfHistory, /// forward-search-history ForwardSearchHistory, /// history-search-backward HistorySearchBackward, /// history-search-forward HistorySearchForward, Insert(RepeatCount, String), Interrupt, /// backward-delete-char, backward-kill-line, backward-kill-word /// delete-char, kill-line, kill-word, unix-line-discard, unix-word-rubout, /// vi-delete, vi-delete-to, vi-rubout Kill(Movement), /// backward-char, backward-word, beginning-of-line, end-of-line, /// forward-char, forward-word, vi-char-search, vi-end-word, vi-next-word, /// vi-prev-word Move(Movement), /// next-history NextHistory, Noop, /// vi-replace Overwrite(char), /// previous-history PreviousHistory, /// quoted-insert QuotedInsert, /// vi-change-char ReplaceChar(RepeatCount, char), /// vi-change-to, vi-substitute Replace(Movement, Option), /// reverse-search-history ReverseSearchHistory, /// self-insert SelfInsert(RepeatCount, char), Suspend, /// transpose-chars TransposeChars, /// transpose-words TransposeWords(RepeatCount), /// undo Undo(RepeatCount), Unknown, /// upcase-word UpcaseWord, /// vi-yank-to ViYankTo(Movement), /// yank, vi-put Yank(RepeatCount, Anchor), /// yank-pop YankPop, } impl Cmd { pub fn should_reset_kill_ring(&self) -> bool { #[allow(clippy::match_same_arms)] match *self { Cmd::Kill(Movement::BackwardChar(_)) | Cmd::Kill(Movement::ForwardChar(_)) => true, Cmd::ClearScreen | Cmd::Kill(_) | Cmd::Replace(_, _) | Cmd::Noop | Cmd::Suspend | Cmd::Yank(_, _) | Cmd::YankPop => false, _ => true, } } fn is_repeatable_change(&self) -> bool { match *self { Cmd::Insert(_, _) | Cmd::Kill(_) | Cmd::ReplaceChar(_, _) | Cmd::Replace(_, _) | Cmd::SelfInsert(_, _) | Cmd::ViYankTo(_) | Cmd::Yank(_, _) => true, Cmd::TransposeChars // TODO Validate | _ => false, } } fn is_repeatable(&self) -> bool { match *self { Cmd::Move(_) => true, _ => self.is_repeatable_change(), } } // Replay this command with a possible different `RepeatCount`. fn redo(&self, new: Option, wrt: &dyn Refresher) -> Self { match *self { Cmd::Insert(previous, ref text) => { Cmd::Insert(repeat_count(previous, new), text.clone()) } Cmd::Kill(ref mvt) => Cmd::Kill(mvt.redo(new)), Cmd::Move(ref mvt) => Cmd::Move(mvt.redo(new)), Cmd::ReplaceChar(previous, c) => Cmd::ReplaceChar(repeat_count(previous, new), c), Cmd::Replace(ref mvt, ref text) => { if text.is_none() { let last_insert = wrt.last_insert(); if let Movement::ForwardChar(0) = mvt { Cmd::Replace( Movement::ForwardChar(last_insert.as_ref().map_or(0, String::len)), last_insert, ) } else { Cmd::Replace(mvt.redo(new), last_insert) } } else { Cmd::Replace(mvt.redo(new), text.clone()) } } Cmd::SelfInsert(previous, c) => { // consecutive char inserts are repeatable not only the last one... if let Some(text) = wrt.last_insert() { Cmd::Insert(repeat_count(previous, new), text) } else { Cmd::SelfInsert(repeat_count(previous, new), c) } } // Cmd::TransposeChars => Cmd::TransposeChars, Cmd::ViYankTo(ref mvt) => Cmd::ViYankTo(mvt.redo(new)), Cmd::Yank(previous, anchor) => Cmd::Yank(repeat_count(previous, new), anchor), _ => unreachable!(), } } } fn repeat_count(previous: RepeatCount, new: Option) -> RepeatCount { match new { Some(n) => n, None => previous, } } /// Different word definitions #[derive(Debug, Clone, PartialEq, Copy)] pub enum Word { /// non-blanks characters Big, /// alphanumeric characters Emacs, /// alphanumeric (and '_') characters Vi, } /// Where to move with respect to word boundary #[derive(Debug, Clone, PartialEq, Copy)] pub enum At { Start, BeforeEnd, AfterEnd, } /// Where to paste (relative to cursor position) #[derive(Debug, Clone, PartialEq, Copy)] pub enum Anchor { After, Before, } /// Vi character search #[derive(Debug, Clone, PartialEq, Copy)] pub enum CharSearch { Forward(char), // until ForwardBefore(char), Backward(char), // until BackwardAfter(char), } impl CharSearch { fn opposite(self) -> Self { match self { CharSearch::Forward(c) => CharSearch::Backward(c), CharSearch::ForwardBefore(c) => CharSearch::BackwardAfter(c), CharSearch::Backward(c) => CharSearch::Forward(c), CharSearch::BackwardAfter(c) => CharSearch::ForwardBefore(c), } } } /// Where to move #[derive(Debug, Clone, PartialEq)] pub enum Movement { WholeLine, // not really a movement /// beginning-of-line BeginningOfLine, /// end-of-line EndOfLine, /// backward-word, vi-prev-word BackwardWord(RepeatCount, Word), // Backward until start of word /// forward-word, vi-end-word, vi-next-word ForwardWord(RepeatCount, At, Word), // Forward until start/end of word /// vi-char-search ViCharSearch(RepeatCount, CharSearch), /// vi-first-print ViFirstPrint, /// backward-char BackwardChar(RepeatCount), /// forward-char ForwardChar(RepeatCount), } impl Movement { // Replay this movement with a possible different `RepeatCount`. fn redo(&self, new: Option) -> Self { match *self { Movement::WholeLine => Movement::WholeLine, Movement::BeginningOfLine => Movement::BeginningOfLine, Movement::ViFirstPrint => Movement::ViFirstPrint, Movement::EndOfLine => Movement::EndOfLine, Movement::BackwardWord(previous, word) => { Movement::BackwardWord(repeat_count(previous, new), word) } Movement::ForwardWord(previous, at, word) => { Movement::ForwardWord(repeat_count(previous, new), at, word) } Movement::ViCharSearch(previous, char_search) => { Movement::ViCharSearch(repeat_count(previous, new), char_search) } Movement::BackwardChar(previous) => Movement::BackwardChar(repeat_count(previous, new)), Movement::ForwardChar(previous) => Movement::ForwardChar(repeat_count(previous, new)), } } } #[derive(PartialEq)] enum InputMode { /// Vi Command/Alternate Command, /// Insert/Input mode Insert, /// Overwrite mode Replace, } /// Transform key(s) to commands based on current input mode pub struct InputState { mode: EditMode, custom_bindings: Arc>>, 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; } 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<()>; /// Same as `refresh_line` but with a dynamic prompt. fn refresh_prompt_and_line(&mut self, prompt: &str) -> Result<()>; /// Vi only, switch to insert mode. fn doing_insert(&mut self); /// Vi only, switch to command mode. fn done_inserting(&mut self); /// Vi only, last text inserted. fn last_insert(&self) -> Option; /// Returns `true` if the cursor is currently at the end of the line. fn is_cursor_at_end(&self) -> bool; /// Returns `true` if there is a hint displayed. fn has_hint(&self) -> bool; } impl InputState { pub fn new(config: &Config, custom_bindings: Arc>>) -> Self { Self { mode: config.edit_mode(), custom_bindings, input_mode: InputMode::Insert, num_args: 0, last_cmd: Cmd::Noop, last_char_search: None, } } pub fn is_emacs_mode(&self) -> bool { self.mode == EditMode::Emacs } /// Parse user input into one command /// `single_esc_abort` is used in emacs mode on unix platform when a single /// esc key is expected to abort current action. pub fn next_cmd( &mut self, rdr: &mut ::Reader, wrt: &mut dyn Refresher, 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), } } fn emacs_digit_argument( &mut self, rdr: &mut R, wrt: &mut dyn Refresher, digit: char, ) -> Result { #[allow(clippy::cast_possible_truncation)] match digit { '0'..='9' => { self.num_args = digit.to_digit(10).unwrap() as i16; } '-' => { self.num_args = -1; } _ => unreachable!(), } loop { wrt.refresh_prompt_and_line(&format!("(arg: {}) ", self.num_args))?; let key = rdr.next_key(true)?; #[allow(clippy::cast_possible_truncation)] match key { KeyPress::Char(digit @ '0'..='9') | KeyPress::Meta(digit @ '0'..='9') => { if self.num_args == -1 { self.num_args *= digit.to_digit(10).unwrap() as i16; } else if self.num_args.abs() < 1000 { // shouldn't ever need more than 4 digits self.num_args = self .num_args .saturating_mul(10) .saturating_add(digit.to_digit(10).unwrap() as i16); } } KeyPress::Char('-') | KeyPress::Meta('-') => {} _ => { wrt.refresh_line()?; return Ok(key); } }; } } fn emacs( &mut self, rdr: &mut R, wrt: &mut dyn Refresher, single_esc_abort: bool, ) -> Result { let mut key = rdr.next_key(single_esc_abort)?; if let KeyPress::Meta(digit @ '-') = key { key = self.emacs_digit_argument(rdr, wrt, digit)?; } else if let KeyPress::Meta(digit @ '0'..='9') = 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 cmd = match key { KeyPress::Char(c) => { if positive { Cmd::SelfInsert(n, c) } else { Cmd::Unknown } } KeyPress::Ctrl('A') => Cmd::Move(Movement::BeginningOfLine), KeyPress::Ctrl('B') => { if positive { Cmd::Move(Movement::BackwardChar(n)) } else { Cmd::Move(Movement::ForwardChar(n)) } } KeyPress::Ctrl('E') => Cmd::Move(Movement::EndOfLine), KeyPress::Ctrl('F') => { if positive { Cmd::Move(Movement::ForwardChar(n)) } else { Cmd::Move(Movement::BackwardChar(n)) } } KeyPress::Ctrl('G') | KeyPress::Esc | KeyPress::Meta('\x07') => Cmd::Abort, KeyPress::Ctrl('H') | KeyPress::Backspace => { if positive { Cmd::Kill(Movement::BackwardChar(n)) } else { Cmd::Kill(Movement::ForwardChar(n)) } } KeyPress::BackTab => Cmd::CompleteBackward, KeyPress::Tab => { if positive { Cmd::Complete } else { Cmd::CompleteBackward } } // 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) } else { Cmd::Kill(Movement::BeginningOfLine) } } KeyPress::Ctrl('L') => Cmd::ClearScreen, KeyPress::Ctrl('N') => Cmd::NextHistory, KeyPress::Ctrl('P') => Cmd::PreviousHistory, KeyPress::Ctrl('X') => { let snd_key = rdr.next_key(true)?; match snd_key { KeyPress::Ctrl('G') | KeyPress::Esc => Cmd::Abort, KeyPress::Ctrl('U') => Cmd::Undo(n), _ => Cmd::Unknown, } } KeyPress::Meta('\x08') | KeyPress::Meta('\x7f') => { if positive { Cmd::Kill(Movement::BackwardWord(n, Word::Emacs)) } else { Cmd::Kill(Movement::ForwardWord(n, At::AfterEnd, Word::Emacs)) } } KeyPress::Meta('<') => Cmd::BeginningOfHistory, KeyPress::Meta('>') => Cmd::EndOfHistory, KeyPress::Meta('B') | KeyPress::Meta('b') => { if positive { Cmd::Move(Movement::BackwardWord(n, Word::Emacs)) } else { Cmd::Move(Movement::ForwardWord(n, At::AfterEnd, Word::Emacs)) } } KeyPress::Meta('C') | KeyPress::Meta('c') => Cmd::CapitalizeWord, KeyPress::Meta('D') | KeyPress::Meta('d') => { if positive { Cmd::Kill(Movement::ForwardWord(n, At::AfterEnd, Word::Emacs)) } else { Cmd::Kill(Movement::BackwardWord(n, Word::Emacs)) } } KeyPress::Meta('F') | KeyPress::Meta('f') => { if positive { Cmd::Move(Movement::ForwardWord(n, At::AfterEnd, Word::Emacs)) } else { Cmd::Move(Movement::BackwardWord(n, Word::Emacs)) } } KeyPress::Meta('L') | KeyPress::Meta('l') => Cmd::DowncaseWord, KeyPress::Meta('T') | KeyPress::Meta('t') => Cmd::TransposeWords(n), KeyPress::Meta('U') | KeyPress::Meta('u') => Cmd::UpcaseWord, KeyPress::Meta('Y') | KeyPress::Meta('y') => Cmd::YankPop, _ => self.common(rdr, key, n, positive)?, }; debug!(target: "rustyline", "Emacs command: {:?}", cmd); Ok(cmd) } #[allow(clippy::cast_possible_truncation)] fn vi_arg_digit( &mut self, rdr: &mut R, wrt: &mut dyn Refresher, digit: char, ) -> 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 self.num_args.abs() < 1000 { // shouldn't ever need more than 4 digits self.num_args = self .num_args .saturating_mul(10) .saturating_add(digit.to_digit(10).unwrap() as i16); } } else { wrt.refresh_line()?; return Ok(key); }; } } 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 { 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) } } else { cmd.clone() }); } } let cmd = match key { KeyPress::Char('$') | KeyPress::End => Cmd::Move(Movement::EndOfLine), KeyPress::Char('.') => { // vi-redo (repeat last command) if no_num_args { self.last_cmd.redo(None, wrt) } else { self.last_cmd.redo(Some(n), wrt) } }, // TODO KeyPress::Char('%') => Cmd::???, Move to the corresponding opening/closing bracket KeyPress::Char('0') => Cmd::Move(Movement::BeginningOfLine), KeyPress::Char('^') => Cmd::Move(Movement::ViFirstPrint), KeyPress::Char('a') => { // vi-append-mode self.input_mode = InputMode::Insert; wrt.doing_insert(); Cmd::Move(Movement::ForwardChar(n)) } KeyPress::Char('A') => { // vi-append-eol self.input_mode = InputMode::Insert; wrt.doing_insert(); Cmd::Move(Movement::EndOfLine) } KeyPress::Char('b') => Cmd::Move(Movement::BackwardWord(n, Word::Vi)), // vi-prev-word KeyPress::Char('B') => Cmd::Move(Movement::BackwardWord(n, Word::Big)), KeyPress::Char('c') => { self.input_mode = InputMode::Insert; match self.vi_cmd_motion(rdr, wrt, key, n)? { Some(mvt) => Cmd::Replace(mvt, None), None => Cmd::Unknown, } } KeyPress::Char('C') => { self.input_mode = InputMode::Insert; Cmd::Replace(Movement::EndOfLine, None) } KeyPress::Char('d') => { match self.vi_cmd_motion(rdr, wrt, key, n)? { Some(mvt) => Cmd::Kill(mvt), None => Cmd::Unknown, } } KeyPress::Char('D') | KeyPress::Ctrl('K') => Cmd::Kill(Movement::EndOfLine), KeyPress::Char('e') => Cmd::Move(Movement::ForwardWord(n, At::BeforeEnd, Word::Vi)), KeyPress::Char('E') => Cmd::Move(Movement::ForwardWord(n, At::BeforeEnd, Word::Big)), KeyPress::Char('i') => { // vi-insertion-mode self.input_mode = InputMode::Insert; wrt.doing_insert(); Cmd::Noop } KeyPress::Char('I') => { // vi-insert-beg self.input_mode = InputMode::Insert; wrt.doing_insert(); Cmd::Move(Movement::BeginningOfLine) } KeyPress::Char(c) if c == 'f' || c == 'F' || c == 't' || c == 'T' => { // vi-char-search let cs = self.vi_char_search(rdr, c)?; match cs { Some(cs) => Cmd::Move(Movement::ViCharSearch(n, cs)), None => Cmd::Unknown, } } KeyPress::Char(';') => { match self.last_char_search { Some(cs) => Cmd::Move(Movement::ViCharSearch(n, cs)), None => Cmd::Noop, } } KeyPress::Char(',') => { match self.last_char_search { Some(ref cs) => Cmd::Move(Movement::ViCharSearch(n, cs.opposite())), None => Cmd::Noop, } } // TODO KeyPress::Char('G') => Cmd::???, Move to the history line n KeyPress::Char('p') => Cmd::Yank(n, Anchor::After), // vi-put KeyPress::Char('P') => Cmd::Yank(n, Anchor::Before), // vi-put KeyPress::Char('r') => { // vi-replace-char: let ch = rdr.next_key(false)?; match ch { KeyPress::Char(c) => Cmd::ReplaceChar(n, c), KeyPress::Esc => Cmd::Noop, _ => Cmd::Unknown, } } KeyPress::Char('R') => { // vi-replace-mode (overwrite-mode) self.input_mode = InputMode::Replace; Cmd::Replace(Movement::ForwardChar(0), None) } KeyPress::Char('s') => { // vi-substitute-char: self.input_mode = InputMode::Insert; Cmd::Replace(Movement::ForwardChar(n), None) } KeyPress::Char('S') => { // vi-substitute-line: self.input_mode = InputMode::Insert; Cmd::Replace(Movement::WholeLine, None) } KeyPress::Char('u') => Cmd::Undo(n), // KeyPress::Char('U') => Cmd::???, // revert-line KeyPress::Char('w') => Cmd::Move(Movement::ForwardWord(n, At::Start, Word::Vi)), // vi-next-word KeyPress::Char('W') => Cmd::Move(Movement::ForwardWord(n, At::Start, Word::Big)), // vi-next-word KeyPress::Char('x') => Cmd::Kill(Movement::ForwardChar(n)), // vi-delete: TODO move backward if eol KeyPress::Char('X') => Cmd::Kill(Movement::BackwardChar(n)), // vi-rubout KeyPress::Char('y') => { match self.vi_cmd_motion(rdr, wrt, key, n)? { Some(mvt) => Cmd::ViYankTo(mvt), None => Cmd::Unknown, } } // KeyPress::Char('Y') => Cmd::???, // vi-yank-to KeyPress::Char('h') | KeyPress::Ctrl('H') | KeyPress::Backspace => Cmd::Move(Movement::BackwardChar(n)), KeyPress::Ctrl('G') => Cmd::Abort, KeyPress::Char('l') | KeyPress::Char(' ') => Cmd::Move(Movement::ForwardChar(n)), KeyPress::Ctrl('L') => Cmd::ClearScreen, KeyPress::Char('+') | KeyPress::Char('j') | // TODO: move to the start of the line. KeyPress::Ctrl('N') => Cmd::NextHistory, KeyPress::Char('-') | KeyPress::Char('k') | // TODO: move to the start of the line. KeyPress::Ctrl('P') => Cmd::PreviousHistory, KeyPress::Ctrl('R') => { self.input_mode = InputMode::Insert; // TODO Validate Cmd::ReverseSearchHistory } KeyPress::Ctrl('S') => { self.input_mode = InputMode::Insert; // TODO Validate Cmd::ForwardSearchHistory } KeyPress::Esc => Cmd::Noop, _ => self.common(rdr, key, n, true)?, }; debug!(target: "rustyline", "Vi command: {:?}", cmd); if cmd.is_repeatable_change() { self.last_cmd = cmd.clone(); } 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() }); } } let cmd = match key { KeyPress::Char(c) => { if self.input_mode == InputMode::Replace { Cmd::Overwrite(c) } else { Cmd::SelfInsert(1, c) } } KeyPress::Ctrl('H') | KeyPress::Backspace => Cmd::Kill(Movement::BackwardChar(1)), KeyPress::BackTab => Cmd::CompleteBackward, KeyPress::Tab => 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 => { // 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)?, }; debug!(target: "rustyline", "Vi insert: {:?}", cmd); if cmd.is_repeatable_change() { if let (Cmd::Replace(_, _), Cmd::SelfInsert(_, _)) = (&self.last_cmd, &cmd) { // replacing... } else if let (Cmd::SelfInsert(_, _), Cmd::SelfInsert(_, _)) = (&self.last_cmd, &cmd) { // inserting... } else { self.last_cmd = cmd.clone(); } } Ok(cmd) } fn vi_cmd_motion( &mut self, rdr: &mut R, wrt: &mut dyn Refresher, key: KeyPress, n: RepeatCount, ) -> Result> { let mut mvt = rdr.next_key(false)?; if mvt == key { return Ok(Some(Movement::WholeLine)); } let mut n = n; if let KeyPress::Char(digit @ '1'..='9') = mvt { // vi-arg-digit mvt = self.vi_arg_digit(rdr, wrt, digit)?; n = self.vi_num_args().saturating_mul(n); } Ok(match mvt { KeyPress::Char('$') => Some(Movement::EndOfLine), KeyPress::Char('0') => Some(Movement::BeginningOfLine), KeyPress::Char('^') => Some(Movement::ViFirstPrint), KeyPress::Char('b') => Some(Movement::BackwardWord(n, Word::Vi)), KeyPress::Char('B') => Some(Movement::BackwardWord(n, Word::Big)), KeyPress::Char('e') => Some(Movement::ForwardWord(n, At::AfterEnd, Word::Vi)), KeyPress::Char('E') => Some(Movement::ForwardWord(n, At::AfterEnd, Word::Big)), KeyPress::Char(c) if c == 'f' || c == 'F' || c == 't' || c == 'T' => { let cs = self.vi_char_search(rdr, c)?; match cs { Some(cs) => Some(Movement::ViCharSearch(n, cs)), None => None, } } KeyPress::Char(';') => match self.last_char_search { Some(cs) => Some(Movement::ViCharSearch(n, cs)), None => None, }, KeyPress::Char(',') => match self.last_char_search { Some(ref cs) => Some(Movement::ViCharSearch(n, cs.opposite())), None => None, }, KeyPress::Char('h') | KeyPress::Ctrl('H') | KeyPress::Backspace => { Some(Movement::BackwardChar(n)) } KeyPress::Char('l') | KeyPress::Char(' ') => Some(Movement::ForwardChar(n)), KeyPress::Char('w') => { // 'cw' is 'ce' if key == KeyPress::Char('c') { Some(Movement::ForwardWord(n, At::AfterEnd, Word::Vi)) } else { Some(Movement::ForwardWord(n, At::Start, Word::Vi)) } } KeyPress::Char('W') => { // 'cW' is 'cE' if key == KeyPress::Char('c') { Some(Movement::ForwardWord(n, At::AfterEnd, Word::Big)) } else { Some(Movement::ForwardWord(n, At::Start, Word::Big)) } } _ => None, }) } fn vi_char_search( &mut self, rdr: &mut R, cmd: char, ) -> Result> { let ch = rdr.next_key(false)?; Ok(match ch { KeyPress::Char(ch) => { let cs = match cmd { 'f' => CharSearch::Forward(ch), 't' => CharSearch::ForwardBefore(ch), 'F' => CharSearch::Backward(ch), 'T' => CharSearch::BackwardAfter(ch), _ => unreachable!(), }; self.last_char_search = Some(cs); Some(cs) } _ => None, }) } fn common( &mut self, rdr: &mut R, key: KeyPress, 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)) } else { Cmd::Move(Movement::BackwardChar(n)) } } KeyPress::Ctrl('J') | KeyPress::Enter => Cmd::AcceptLine, KeyPress::Down => Cmd::NextHistory, KeyPress::Up => Cmd::PreviousHistory, KeyPress::Ctrl('R') => Cmd::ReverseSearchHistory, KeyPress::Ctrl('S') => Cmd::ForwardSearchHistory, // most terminals override Ctrl+S to suspend execution KeyPress::Ctrl('T') => Cmd::TransposeChars, KeyPress::Ctrl('U') => { if positive { Cmd::Kill(Movement::BeginningOfLine) } else { Cmd::Kill(Movement::EndOfLine) } }, KeyPress::Ctrl('Q') | // most terminals override Ctrl+Q to resume execution KeyPress::Ctrl('V') => Cmd::QuotedInsert, KeyPress::Ctrl('W') => { if positive { Cmd::Kill(Movement::BackwardWord(n, Word::Big)) } else { Cmd::Kill(Movement::ForwardWord(n, At::AfterEnd, Word::Big)) } } KeyPress::Ctrl('Y') => { if positive { Cmd::Yank(n, Anchor::Before) } else { Cmd::Unknown // TODO Validate } } KeyPress::Ctrl('Z') => Cmd::Suspend, KeyPress::Ctrl('_') => Cmd::Undo(n), KeyPress::UnknownEscSeq => Cmd::Noop, KeyPress::BracketedPasteStart => { let paste = rdr.read_pasted_text()?; Cmd::Insert(1, paste) }, _ => Cmd::Unknown, }) } fn num_args(&mut self) -> i16 { let num_args = match self.num_args { 0 => 1, _ => self.num_args, }; self.num_args = 0; num_args } #[allow(clippy::cast_sign_loss)] fn emacs_num_args(&mut self) -> (RepeatCount, bool) { let num_args = self.num_args(); if num_args < 0 { if let (n, false) = num_args.overflowing_abs() { (n as RepeatCount, false) } else { (RepeatCount::max_value(), false) } } else { (num_args as RepeatCount, true) } } #[allow(clippy::cast_sign_loss)] fn vi_num_args(&mut self) -> RepeatCount { let num_args = self.num_args(); if num_args < 0 { unreachable!() } else { num_args.abs() as RepeatCount } } } rustyline-6.0.0/src/keys.rs010064400007650000024000000043771357476110200141230ustar0000000000000000//! Key constants // #[non_exhaustive] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum KeyPress { UnknownEscSeq, Backspace, // Ctrl('H') BackTab, BracketedPasteStart, BracketedPasteEnd, Char(char), ControlDown, ControlLeft, ControlRight, ControlUp, Ctrl(char), Delete, Down, End, Enter, // Ctrl('M') Esc, // Ctrl('[') F(u8), Home, Insert, Left, Meta(char), Null, PageDown, PageUp, Right, ShiftDown, ShiftLeft, ShiftRight, ShiftUp, Tab, // Ctrl('I') 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, } } #[cfg(test)] mod tests { use super::{char_to_key_press, KeyPress}; #[test] fn char_to_key() { assert_eq!(KeyPress::Esc, char_to_key_press('\x1b')); } } rustyline-6.0.0/src/kill_ring.rs010064400007650000024000000154621346010623700151130ustar0000000000000000//! Kill Ring management use crate::line_buffer::{DeleteListener, Direction}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum Action { Kill, Yank(usize), Other, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Mode { Append, Prepend, } pub struct KillRing { slots: Vec, // where we are in the kill ring index: usize, // whether or not the last command was a kill or a yank last_action: Action, killing: bool, } impl KillRing { /// Create a new kill-ring of the given `size`. pub fn new(size: usize) -> Self { Self { slots: Vec::with_capacity(size), index: 0, last_action: Action::Other, killing: false, } } /// Reset `last_action` state. pub fn reset(&mut self) { self.last_action = Action::Other; } /// Add `text` to the kill-ring. pub fn kill(&mut self, text: &str, dir: Mode) { if let Action::Kill = self.last_action { if self.slots.capacity() == 0 { // disabled return; } match dir { Mode::Append => self.slots[self.index].push_str(text), Mode::Prepend => self.slots[self.index].insert_str(0, text), }; } else { self.last_action = Action::Kill; if self.slots.capacity() == 0 { // disabled return; } if self.index == self.slots.capacity() - 1 { // full self.index = 0; } else if !self.slots.is_empty() { self.index += 1; } if self.index == self.slots.len() { self.slots.push(String::from(text)) } else { self.slots[self.index] = String::from(text); } } } /// Yank previously killed text. /// Return `None` when kill-ring is empty. pub fn yank(&mut self) -> Option<&String> { if self.slots.is_empty() { None } else { self.last_action = Action::Yank(self.slots[self.index].len()); Some(&self.slots[self.index]) } } /// Yank killed text stored in previous slot. /// Return `None` when the previous command was not a yank. pub fn yank_pop(&mut self) -> Option<(usize, &String)> { match self.last_action { Action::Yank(yank_size) => { if self.slots.is_empty() { return None; } if self.index == 0 { self.index = self.slots.len() - 1; } else { self.index -= 1; } self.last_action = Action::Yank(self.slots[self.index].len()); Some((yank_size, &self.slots[self.index])) } _ => None, } } } impl DeleteListener for KillRing { fn start_killing(&mut self) { self.killing = true; } fn delete(&mut self, _: usize, string: &str, dir: Direction) { if !self.killing { return; } let mode = match dir { Direction::Forward => Mode::Append, Direction::Backward => Mode::Prepend, }; self.kill(string, mode); } fn stop_killing(&mut self) { self.killing = false; } } #[cfg(test)] mod tests { use super::{Action, KillRing, Mode}; #[test] fn disabled() { let mut kill_ring = KillRing::new(0); kill_ring.kill("text", Mode::Append); assert!(kill_ring.slots.is_empty()); assert_eq!(0, kill_ring.index); assert_eq!(Action::Kill, kill_ring.last_action); assert_eq!(None, kill_ring.yank()); assert_eq!(Action::Kill, kill_ring.last_action); } #[test] fn one_kill() { let mut kill_ring = KillRing::new(2); kill_ring.kill("word1", Mode::Append); assert_eq!(0, kill_ring.index); assert_eq!(1, kill_ring.slots.len()); assert_eq!("word1", kill_ring.slots[0]); assert_eq!(Action::Kill, kill_ring.last_action); } #[test] fn kill_append() { let mut kill_ring = KillRing::new(2); kill_ring.kill("word1", Mode::Append); kill_ring.kill(" word2", Mode::Append); assert_eq!(0, kill_ring.index); assert_eq!(1, kill_ring.slots.len()); assert_eq!("word1 word2", kill_ring.slots[0]); assert_eq!(Action::Kill, kill_ring.last_action); } #[test] fn kill_backward() { let mut kill_ring = KillRing::new(2); kill_ring.kill("word1", Mode::Prepend); kill_ring.kill("word2 ", Mode::Prepend); assert_eq!(0, kill_ring.index); assert_eq!(1, kill_ring.slots.len()); assert_eq!("word2 word1", kill_ring.slots[0]); assert_eq!(Action::Kill, kill_ring.last_action); } #[test] fn kill_other_kill() { let mut kill_ring = KillRing::new(2); kill_ring.kill("word1", Mode::Append); kill_ring.reset(); kill_ring.kill("word2", Mode::Append); assert_eq!(1, kill_ring.index); assert_eq!(2, kill_ring.slots.len()); assert_eq!("word1", kill_ring.slots[0]); assert_eq!("word2", kill_ring.slots[1]); assert_eq!(Action::Kill, kill_ring.last_action); } #[test] fn many_kill() { let mut kill_ring = KillRing::new(2); kill_ring.kill("word1", Mode::Append); kill_ring.reset(); kill_ring.kill("word2", Mode::Append); kill_ring.reset(); kill_ring.kill("word3", Mode::Append); kill_ring.reset(); kill_ring.kill("word4", Mode::Append); assert_eq!(1, kill_ring.index); assert_eq!(2, kill_ring.slots.len()); assert_eq!("word3", kill_ring.slots[0]); assert_eq!("word4", kill_ring.slots[1]); assert_eq!(Action::Kill, kill_ring.last_action); } #[test] fn yank() { let mut kill_ring = KillRing::new(2); kill_ring.kill("word1", Mode::Append); kill_ring.reset(); kill_ring.kill("word2", Mode::Append); assert_eq!(Some(&"word2".to_owned()), kill_ring.yank()); assert_eq!(Action::Yank(5), kill_ring.last_action); assert_eq!(Some(&"word2".to_owned()), kill_ring.yank()); assert_eq!(Action::Yank(5), kill_ring.last_action); } #[test] fn yank_pop() { let mut kill_ring = KillRing::new(2); kill_ring.kill("word1", Mode::Append); kill_ring.reset(); kill_ring.kill("longword2", Mode::Append); assert_eq!(None, kill_ring.yank_pop()); kill_ring.yank(); assert_eq!(Some((9, &"word1".to_owned())), kill_ring.yank_pop()); assert_eq!(Some((5, &"longword2".to_owned())), kill_ring.yank_pop()); assert_eq!(Some((9, &"word1".to_owned())), kill_ring.yank_pop()); } } rustyline-6.0.0/src/layout.rs010064400007650000024000000015141353737232000144520ustar0000000000000000use std::cmp::{Ord, Ordering, PartialOrd}; #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] pub struct Position { pub col: usize, pub row: usize, } impl PartialOrd for Position { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for Position { fn cmp(&self, other: &Self) -> Ordering { match self.row.cmp(&other.row) { Ordering::Equal => self.col.cmp(&other.col), o => o, } } } #[derive(Debug, Default)] pub struct Layout { /// Prompt Unicode/visible width and height pub prompt_size: Position, pub default_prompt: bool, /// Cursor position (relative to the start of the prompt) pub cursor: Position, /// Number of rows used so far (from start of prompt to end of input) pub end: Position, } rustyline-6.0.0/src/lib.rs010064400007650000024000000724651360441142000137060ustar0000000000000000//! Readline for Rust //! //! This implementation is based on [Antirez's //! Linenoise](https://github.com/antirez/linenoise) //! //! # Example //! //! Usage //! //! ``` //! let mut rl = rustyline::Editor::<()>::new(); //! let readline = rl.readline(">> "); //! match readline { //! Ok(line) => println!("Line: {:?}", line), //! Err(_) => println!("No input"), //! } //! ``` // #![feature(non_exhaustive)] pub mod completion; pub mod config; mod edit; pub mod error; pub mod highlight; pub mod hint; pub mod history; mod keymap; mod keys; mod kill_ring; mod layout; pub mod line_buffer; mod tty; mod undo; pub mod validate; use std::collections::HashMap; use std::fmt; use std::io::{self, Write}; use std::path::Path; use std::result; use std::sync::{Arc, Mutex, RwLock}; use log::debug; use unicode_width::UnicodeWidthStr; use crate::tty::{RawMode, Renderer, Term, Terminal}; use crate::completion::{longest_common_prefix, Candidate, Completer}; pub use crate::config::{ ColorMode, CompletionType, Config, EditMode, HistoryDuplicates, OutputStreamType, }; 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::keymap::{InputState, Refresher}; pub use crate::keys::KeyPress; use crate::kill_ring::{KillRing, Mode}; use crate::line_buffer::WordAction; use crate::validate::Validator; /// The error type for I/O and Linux Syscalls (Errno) pub type Result = result::Result; /// Completes the line/word fn complete_line( rdr: &mut ::Reader, s: &mut State<'_, '_, H>, input_state: &mut InputState, config: &Config, ) -> Result> { let completer = s.helper.unwrap(); // get a list of completions let (start, candidates) = completer.complete(&s.line, s.line.pos(), &s.ctx)?; // if no completions, we are done if candidates.is_empty() { s.out.beep()?; Ok(None) } else if CompletionType::Circular == config.completion_type() { let mark = s.changes.borrow_mut().begin(); // Save the current edited line before overwriting it let backup = s.line.as_str().to_owned(); let backup_pos = s.line.pos(); let mut cmd; let mut i = 0; loop { // Show completion or original buffer if i < candidates.len() { let candidate = candidates[i].replacement(); // TODO we can't highlight the line buffer directly /*let candidate = if let Some(highlighter) = s.highlighter { highlighter.highlight_candidate(candidate, CompletionType::Circular) } else { Borrowed(candidate) };*/ completer.update(&mut s.line, start, candidate); s.refresh_line()?; } else { // Restore current edited line s.line.update(&backup, backup_pos); s.refresh_line()?; } cmd = s.next_cmd(input_state, rdr, true)?; match cmd { Cmd::Complete => { i = (i + 1) % (candidates.len() + 1); // Circular if i == candidates.len() { s.out.beep()?; } } Cmd::CompleteBackward => { if i == 0 { i = candidates.len(); // Circular s.out.beep()?; } else { i = (i - 1) % (candidates.len() + 1); // Circular } } Cmd::Abort => { // Re-show original buffer if i < candidates.len() { s.line.update(&backup, backup_pos); s.refresh_line()?; } s.changes.borrow_mut().truncate(mark); return Ok(None); } _ => { s.changes.borrow_mut().end(); break; } } } Ok(Some(cmd)) } else if CompletionType::List == config.completion_type() { if let Some(lcp) = longest_common_prefix(&candidates) { // if we can extend the item, extend it if lcp.len() > s.line.pos() - start { completer.update(&mut s.line, start, lcp); s.refresh_line()?; } } // beep if ambiguous if candidates.len() > 1 { s.out.beep()?; } else { return Ok(None); } // we can't complete any further, wait for second tab let mut cmd = s.next_cmd(input_state, rdr, true)?; // if any character other than tab, pass it to the main loop if cmd != Cmd::Complete { return Ok(Some(cmd)); } // move cursor to EOL to avoid overwriting the command line let save_pos = s.line.pos(); s.edit_move_end()?; s.line.set_pos(save_pos); // we got a second tab, maybe show list of possible completions let show_completions = if candidates.len() > config.completion_prompt_limit() { let msg = format!("\nDisplay all {} possibilities? (y or n)", candidates.len()); s.out.write_and_flush(msg.as_bytes())?; s.layout.end.row += 1; while cmd != Cmd::SelfInsert(1, 'y') && cmd != Cmd::SelfInsert(1, 'Y') && cmd != Cmd::SelfInsert(1, 'n') && cmd != Cmd::SelfInsert(1, 'N') && cmd != Cmd::Kill(Movement::BackwardChar(1)) { cmd = s.next_cmd(input_state, rdr, false)?; } match cmd { Cmd::SelfInsert(1, 'y') | Cmd::SelfInsert(1, 'Y') => true, _ => false, } } else { true }; if show_completions { page_completions(rdr, s, input_state, &candidates) } else { s.refresh_line()?; Ok(None) } } else { Ok(None) } } /// Completes the current hint fn complete_hint_line(s: &mut State<'_, '_, H>) -> Result<()> { let hint = match s.hint.as_ref() { Some(hint) => hint, None => return Ok(()), }; s.line.move_end(); if s.line.yank(hint, 1).is_none() { s.out.beep()?; } s.refresh_line_with_msg(None)?; Ok(()) } fn page_completions( rdr: &mut ::Reader, s: &mut State<'_, '_, H>, input_state: &mut InputState, candidates: &[C], ) -> Result> { use std::cmp; let min_col_pad = 2; let cols = s.out.get_columns(); let max_width = cmp::min( cols, candidates .iter() .map(|s| s.display().width()) .max() .unwrap() + min_col_pad, ); let num_cols = cols / max_width; let mut pause_row = s.out.get_rows() - 1; let num_rows = (candidates.len() + num_cols - 1) / num_cols; let mut ab = String::new(); for row in 0..num_rows { if row == pause_row { s.out.write_and_flush(b"\n--More--")?; let mut cmd = Cmd::Noop; while cmd != Cmd::SelfInsert(1, 'y') && cmd != Cmd::SelfInsert(1, 'Y') && cmd != Cmd::SelfInsert(1, 'n') && cmd != Cmd::SelfInsert(1, 'N') && cmd != Cmd::SelfInsert(1, 'q') && cmd != Cmd::SelfInsert(1, 'Q') && cmd != Cmd::SelfInsert(1, ' ') && cmd != Cmd::Kill(Movement::BackwardChar(1)) && cmd != Cmd::AcceptLine { cmd = s.next_cmd(input_state, rdr, false)?; } match cmd { Cmd::SelfInsert(1, 'y') | Cmd::SelfInsert(1, 'Y') | Cmd::SelfInsert(1, ' ') => { pause_row += s.out.get_rows() - 1; } Cmd::AcceptLine => { pause_row += 1; } _ => break, } s.out.write_and_flush(b"\n")?; } else { s.out.write_and_flush(b"\n")?; } ab.clear(); for col in 0..num_cols { let i = (col * num_rows) + row; if i < candidates.len() { let candidate = &candidates[i].display(); let width = candidate.width(); if let Some(highlighter) = s.highlighter() { ab.push_str(&highlighter.highlight_candidate(candidate, CompletionType::List)); } else { ab.push_str(candidate); } if ((col + 1) * num_rows) + row < candidates.len() { for _ in width..max_width { ab.push(' '); } } } } s.out.write_and_flush(ab.as_bytes())?; } s.out.write_and_flush(b"\n")?; s.refresh_line()?; Ok(None) } /// Incremental search fn reverse_incremental_search( rdr: &mut ::Reader, s: &mut State<'_, '_, H>, input_state: &mut InputState, history: &History, ) -> Result> { if history.is_empty() { return Ok(None); } let mark = s.changes.borrow_mut().begin(); // Save the current edited line (and cursor position) before overwriting it let backup = s.line.as_str().to_owned(); let backup_pos = s.line.pos(); let mut search_buf = String::new(); let mut history_idx = history.len() - 1; let mut direction = Direction::Reverse; let mut success = true; let mut cmd; // Display the reverse-i-search prompt and process chars loop { let prompt = if success { format!("(reverse-i-search)`{}': ", search_buf) } else { format!("(failed reverse-i-search)`{}': ", search_buf) }; s.refresh_prompt_and_line(&prompt)?; cmd = s.next_cmd(input_state, rdr, true)?; if let Cmd::SelfInsert(_, c) = cmd { search_buf.push(c); } else { match cmd { Cmd::Kill(Movement::BackwardChar(_)) => { search_buf.pop(); continue; } Cmd::ReverseSearchHistory => { direction = Direction::Reverse; if history_idx > 0 { history_idx -= 1; } else { success = false; continue; } } Cmd::ForwardSearchHistory => { direction = Direction::Forward; if history_idx < history.len() - 1 { history_idx += 1; } else { success = false; continue; } } Cmd::Abort => { // Restore current edited line (before search) s.line.update(&backup, backup_pos); s.refresh_line()?; s.changes.borrow_mut().truncate(mark); return Ok(None); } Cmd::Move(_) => { s.refresh_line()?; // restore prompt break; } _ => break, } } 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); true } _ => false, }; } s.changes.borrow_mut().end(); Ok(Some(cmd)) } /// Handles reading and editing the readline buffer. /// It will also handle special inputs in an appropriate fashion /// (e.g., C-c will exit readline) fn readline_edit( prompt: &str, initial: Option<(&str, &str)>, editor: &mut Editor, original_mode: &tty::Mode, ) -> 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 input_state = InputState::new(&editor.config, Arc::clone(&editor.custom_bindings)); s.line.set_delete_listener(editor.kill_ring.clone()); s.line.set_change_listener(s.changes.clone()); if let Some((left, right)) = initial { s.line .update((left.to_owned() + right).as_ref(), left.len()); } let mut rdr = editor.term.create_reader(&editor.config)?; if editor.term.is_output_tty() { s.move_cursor_at_leftmost(&mut rdr)?; } s.refresh_line()?; loop { let rc = s.next_cmd(&mut input_state, &mut rdr, false); let mut cmd = rc?; if cmd.should_reset_kill_ring() { editor.reset_kill_ring(); } // autocomplete 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 { cmd = next; } else { continue; } } 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 = reverse_incremental_search(&mut rdr, &mut s, &mut input_state, &editor.history)?; if let Some(next) = next { cmd = next; } else { continue; } } 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()?; if s.validate()? { break; } continue; } 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. } } } if cfg!(windows) { let _ = original_mode; // silent warning } Ok(s.line.into_string()) } struct Guard<'m>(&'m tty::Mode); #[allow(unused_must_use)] impl Drop for Guard<'_> { fn drop(&mut self) { let Guard(mode) = *self; mode.disable_raw_mode(); } } /// Readline method that will enable RAW mode, call the `readline_edit()` /// method and disable raw mode fn readline_raw( prompt: &str, initial: Option<(&str, &str)>, editor: &mut Editor, ) -> Result { let original_mode = editor.term.enable_raw_mode()?; let guard = Guard(&original_mode); let user_input = readline_edit(prompt, initial, editor, &original_mode); if editor.config.auto_add_history() { if let Ok(ref line) = user_input { editor.add_history_entry(line.as_str()); } } drop(guard); // disable_raw_mode(original_mode)?; match editor.config.output_stream() { OutputStreamType::Stdout => writeln!(io::stdout())?, OutputStreamType::Stderr => writeln!(io::stderr())?, }; 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) } } /// Syntax specific helper. /// /// TODO Tokenizer/parser used for both completion, suggestion, highlighting. /// (parse current line once) pub trait Helper where Self: Completer + Hinter + Highlighter + Validator, { } impl Helper for () {} impl<'h, H: ?Sized + Helper> Helper for &'h H {} /// Completion/suggestion context pub struct Context<'h> { history: &'h History, history_index: usize, } impl<'h> Context<'h> { /// Constructor. Visible for testing. pub fn new(history: &'h History) -> Self { Context { history, history_index: history.len(), } } /// Return an immutable reference to the history object. pub fn history(&self) -> &History { &self.history } /// The history index we are currently editing pub fn history_index(&self) -> usize { self.history_index } } /// Line editor pub struct Editor { term: Terminal, history: History, helper: Option, kill_ring: Arc>, config: Config, custom_bindings: Arc>>, } #[allow(clippy::new_without_default)] impl Editor { /// Create an editor with the default configuration pub fn new() -> Self { Self::with_config(Config::default()) } /// Create an editor with a specific configuration. pub fn with_config(config: Config) -> Self { let term = Terminal::new( config.color_mode(), config.output_stream(), config.tab_stop(), config.bell_style(), ); Self { term, history: History::with_config(config), helper: None, kill_ring: Arc::new(Mutex::new(KillRing::new(60))), config, custom_bindings: Arc::new(RwLock::new(HashMap::new())), } } /// This method will read a line from STDIN and will display a `prompt`. /// /// It uses terminal-style interaction if `stdin` is connected to a /// terminal. /// Otherwise (e.g., if `stdin` is a pipe or the terminal is not supported), /// it uses file-style interaction. pub fn readline(&mut self, prompt: &str) -> Result { self.readline_with(prompt, None) } /// This function behaves in the exact same manner as `readline`, except /// that it pre-populates the input area. /// /// The text that resides in the input area is given as a 2-tuple. /// The string on the left of the tuple is what will appear to the left of /// the cursor and the string on the right is what will appear to the /// right of the cursor. pub fn readline_with_initial(&mut self, prompt: &str, initial: (&str, &str)) -> Result { self.readline_with(prompt, Some(initial)) } fn readline_with(&mut self, prompt: &str, initial: Option<(&str, &str)>) -> Result { if self.term.is_unsupported() { debug!(target: "rustyline", "unsupported terminal"); // Write prompt and flush it to stdout let mut stdout = io::stdout(); stdout.write_all(prompt.as_bytes())?; stdout.flush()?; readline_direct() } 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() } } /// Load the history from the specified file. pub fn load_history + ?Sized>(&mut self, path: &P) -> Result<()> { self.history.load(path) } /// Save the history in the specified file. pub fn save_history + ?Sized>(&self, path: &P) -> Result<()> { self.history.save(path) } /// Add a new entry in the history. pub fn add_history_entry + Into>(&mut self, line: S) -> bool { self.history.add(line) } /// Clear history. pub fn clear_history(&mut self) { self.history.clear() } /// Return a mutable reference to the history object. pub fn history_mut(&mut self) -> &mut History { &mut self.history } /// Return an immutable reference to the history object. pub fn history(&self) -> &History { &self.history } /// Register a callback function to be called for tab-completion /// or to show hints to the user at the right of the prompt. pub fn set_helper(&mut self, helper: Option) { self.helper = helper; } /// Return a mutable reference to the helper. pub fn helper_mut(&mut self) -> Option<&mut H> { self.helper.as_mut() } /// Return an immutable reference to the helper. pub fn helper(&self) -> Option<&H> { self.helper.as_ref() } /// Bind a sequence to a command. pub fn bind_sequence(&mut self, key_seq: KeyPress, cmd: Cmd) -> Option { if let Ok(mut bindings) = self.custom_bindings.write() { bindings.insert(key_seq, cmd) } else { None } } /// Remove a binding for the given sequence. pub fn unbind_sequence(&mut self, key_seq: KeyPress) -> Option { if let Ok(mut bindings) = self.custom_bindings.write() { bindings.remove(&key_seq) } else { None } } /// ``` /// let mut rl = rustyline::Editor::<()>::new(); /// for readline in rl.iter("> ") { /// match readline { /// Ok(line) => { /// println!("Line: {}", line); /// } /// Err(err) => { /// println!("Error: {:?}", err); /// break; /// } /// } /// } /// ``` pub fn iter<'a>(&'a mut self, prompt: &'a str) -> Iter<'_, H> { Iter { editor: self, prompt, } } fn reset_kill_ring(&self) { let mut kill_ring = self.kill_ring.lock().unwrap(); kill_ring.reset(); } /// If output stream is a tty, this function returns its width and height as /// a number of characters. pub fn dimensions(&mut self) -> Option<(usize, usize)> { if self.term.is_output_tty() { let out = self.term.create_writer(); Some((out.get_columns(), out.get_rows())) } else { None } } } impl config::Configurer for Editor { fn config_mut(&mut self) -> &mut Config { &mut self.config } fn set_max_history_size(&mut self, max_size: usize) { self.config_mut().set_max_history_size(max_size); self.history.set_max_len(max_size); } fn set_history_ignore_dups(&mut self, yes: bool) { self.config_mut().set_history_ignore_dups(yes); self.history.ignore_dups = yes; } fn set_history_ignore_space(&mut self, yes: bool) { self.config_mut().set_history_ignore_space(yes); self.history.ignore_space = yes; } fn set_color_mode(&mut self, color_mode: ColorMode) { self.config_mut().set_color_mode(color_mode); self.term.color_mode = color_mode; } } impl fmt::Debug for Editor { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Editor") .field("term", &self.term) .field("config", &self.config) .finish() } } /// Edited lines iterator pub struct Iter<'a, H: Helper> { editor: &'a mut Editor, prompt: &'a str, } impl<'a, H: Helper> Iterator for Iter<'a, H> { type Item = Result; fn next(&mut self) -> Option> { let readline = self.editor.readline(self.prompt); match readline { Ok(l) => Some(Ok(l)), Err(error::ReadlineError::Eof) => None, e @ Err(_) => Some(e), } } } #[cfg(test)] #[macro_use] extern crate assert_matches; #[cfg(test)] mod test; rustyline-6.0.0/src/line_buffer.rs010064400007650000024000001306131352702755400154250ustar0000000000000000//! Line buffer with current cursor position use crate::keymap::{At, CharSearch, Movement, RepeatCount, Word}; use std::cell::RefCell; use std::fmt; use std::iter; use std::ops::{Deref, Index, Range}; use std::rc::Rc; use std::string::Drain; use std::sync::{Arc, Mutex}; use unicode_segmentation::UnicodeSegmentation; /// Default maximum buffer size for the line read pub(crate) const MAX_LINE: usize = 4096; /// Word's case change #[derive(Clone, Copy)] pub enum WordAction { CAPITALIZE, LOWERCASE, UPPERCASE, } /// Delete (kill) direction #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum Direction { Forward, Backward, } impl Default for Direction { fn default() -> Self { Direction::Forward } } /// Listener to be notified when some text is deleted. pub(crate) trait DeleteListener { fn start_killing(&mut self); fn delete(&mut self, idx: usize, string: &str, dir: Direction); fn stop_killing(&mut self); } /// Listener to be notified when the line is modified. pub(crate) trait ChangeListener: DeleteListener { fn insert_char(&mut self, idx: usize, c: char); fn insert_str(&mut self, idx: usize, string: &str); fn replace(&mut self, idx: usize, old: &str, new: &str); } /// Represent the current input (text and cursor position). /// /// The methods do text manipulations or/and cursor movements. pub struct LineBuffer { buf: String, // Edited line buffer (rl_line_buffer) pos: usize, // Current cursor position (byte position) (rl_point) can_growth: bool, // Whether to allow dynamic growth dl: Option>>, cl: Option>>, } impl fmt::Debug for LineBuffer { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("LineBuffer") .field("buf", &self.buf) .field("pos", &self.pos) .finish() } } impl LineBuffer { /// Create a new line buffer with the given maximum `capacity`. pub fn with_capacity(capacity: usize) -> Self { Self { buf: String::with_capacity(capacity), pos: 0, can_growth: false, dl: None, cl: None, } } /// Set whether to allow dynamic allocation pub(crate) fn can_growth(mut self, can_growth: bool) -> Self { self.can_growth = can_growth; self } fn must_truncate(&self, new_len: usize) -> bool { !self.can_growth && new_len > self.buf.capacity() } #[cfg(test)] pub(crate) fn init( line: &str, pos: usize, cl: Option>>, ) -> Self { let mut lb = Self::with_capacity(MAX_LINE); assert!(lb.insert_str(0, line)); lb.set_pos(pos); lb.cl = cl; lb } pub(crate) fn set_delete_listener(&mut self, dl: Arc>) { self.dl = Some(dl); } pub(crate) fn set_change_listener(&mut self, dl: Rc>) { self.cl = Some(dl); } /// Extracts a string slice containing the entire buffer. pub fn as_str(&self) -> &str { &self.buf } /// Converts a buffer into a `String` without copying or allocating. pub fn into_string(self) -> String { self.buf } /// Current cursor position (byte position) pub fn pos(&self) -> usize { self.pos } /// Set cursor position (byte position) pub fn set_pos(&mut self, pos: usize) { assert!(pos <= self.buf.len()); self.pos = pos; } /// Returns the length of this buffer, in bytes. pub fn len(&self) -> usize { self.buf.len() } /// Returns `true` if this buffer has a length of zero. pub fn is_empty(&self) -> bool { self.buf.is_empty() } /// Set line content (`buf`) and cursor position (`pos`). pub fn update(&mut self, buf: &str, pos: usize) { assert!(pos <= buf.len()); let end = self.len(); self.drain(0..end, Direction::default()); let max = self.buf.capacity(); if self.must_truncate(buf.len()) { self.insert_str(0, &buf[..max]); if pos > max { self.pos = max; } else { self.pos = pos; } } else { self.insert_str(0, buf); self.pos = pos; } } /// Returns the character at current cursor position. pub(crate) fn grapheme_at_cursor(&self) -> Option<&str> { if self.pos == self.buf.len() { None } else { self.buf[self.pos..].graphemes(true).next() } } /// Returns the position of the character just after the current cursor /// position. pub fn next_pos(&self, n: RepeatCount) -> Option { if self.pos == self.buf.len() { return None; } self.buf[self.pos..] .grapheme_indices(true) .take(n) .last() .map(|(i, s)| i + self.pos + s.len()) } /// Returns the position of the character just before the current cursor /// position. fn prev_pos(&self, n: RepeatCount) -> Option { if self.pos == 0 { return None; } self.buf[..self.pos] .grapheme_indices(true) .rev() .take(n) .last() .map(|(i, _)| i) } /// Insert the character `ch` at current cursor position /// and advance cursor position accordingly. /// Return `None` when maximum buffer size has been reached, /// `true` when the character has been appended to the end of the line. pub fn insert(&mut self, ch: char, n: RepeatCount) -> Option { let shift = ch.len_utf8() * n; if self.must_truncate(self.buf.len() + shift) { return None; } let push = self.pos == self.buf.len(); if n == 1 { self.buf.insert(self.pos, ch); for cl in &self.cl { if let Ok(mut cl) = cl.try_borrow_mut() { cl.insert_char(self.pos, ch); } // Ok: while undoing, cl is borrowed. And we want to ignore // changes while undoing. } } else { let text = iter::repeat(ch).take(n).collect::(); let pos = self.pos; self.insert_str(pos, &text); } self.pos += shift; Some(push) } /// Yank/paste `text` at current position. /// Return `None` when maximum buffer size has been reached or is empty, /// `true` when the character has been appended to the end of the line. pub fn yank(&mut self, text: &str, n: RepeatCount) -> Option { let shift = text.len() * n; if text.is_empty() || self.must_truncate(self.buf.len() + shift) { return None; } let push = self.pos == self.buf.len(); let pos = self.pos; if n == 1 { self.insert_str(pos, text); } else { let text = iter::repeat(text).take(n).collect::(); self.insert_str(pos, &text); } self.pos += shift; Some(push) } /// Delete previously yanked text and yank/paste `text` at current position. pub fn yank_pop(&mut self, yank_size: usize, text: &str) -> Option { let end = self.pos; let start = end - yank_size; self.drain(start..end, Direction::default()); self.pos -= yank_size; self.yank(text, 1) } /// Move cursor on the left. pub fn move_backward(&mut self, n: RepeatCount) -> bool { match self.prev_pos(n) { Some(pos) => { self.pos = pos; true } None => false, } } /// Move cursor on the right. pub fn move_forward(&mut self, n: RepeatCount) -> bool { match self.next_pos(n) { Some(pos) => { self.pos = pos; true } None => false, } } /// Move cursor to the start of the line. pub fn move_home(&mut self) -> bool { if self.pos > 0 { self.pos = 0; true } else { false } } /// Move cursor to the end of the line. pub fn move_end(&mut self) -> bool { if self.pos == self.buf.len() { false } else { self.pos = self.buf.len(); true } } /// Delete the character at the right of the cursor without altering the /// cursor position. Basically this is what happens with the "Delete" /// keyboard key. /// Return the number of characters deleted. pub fn delete(&mut self, n: RepeatCount) -> Option { match self.next_pos(n) { Some(pos) => { let start = self.pos; let chars = self .drain(start..pos, Direction::Forward) .collect::(); Some(chars) } None => None, } } /// Delete the character at the left of the cursor. /// Basically that is what happens with the "Backspace" keyboard key. pub fn backspace(&mut self, n: RepeatCount) -> bool { match self.prev_pos(n) { Some(pos) => { let end = self.pos; self.drain(pos..end, Direction::Backward); self.pos = pos; true } None => false, } } /// 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.buf.len(); self.drain(start..end, Direction::Forward); true } else { false } } /// 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 end = self.pos; self.drain(0..end, Direction::Backward); self.pos = 0; true } else { false } } /// Exchange the char before cursor with the character at cursor. pub fn transpose_chars(&mut self) -> bool { if self.pos == 0 || self.buf.graphemes(true).count() < 2 { return false; } if self.pos == self.buf.len() { self.move_backward(1); } let chars = self.delete(1).unwrap(); self.move_backward(1); self.yank(&chars, 1); self.move_forward(1); true } /// Go left until start of word fn prev_word_pos(&self, pos: usize, word_def: Word, n: RepeatCount) -> Option { if pos == 0 { return None; } let mut sow = 0; let mut gis = self.buf[..pos].grapheme_indices(true).rev(); 'outer: for _ in 0..n { sow = 0; let mut gj = gis.next(); 'inner: loop { if let Some((j, y)) = gj { let gi = gis.next(); if let Some((_, x)) = gi { if is_start_of_word(word_def, x, y) { sow = j; break 'inner; } gj = gi; } else { break 'outer; } } else { break 'outer; } } } Some(sow) } /// Moves the cursor to the beginning of previous word. pub fn move_to_prev_word(&mut self, word_def: Word, n: RepeatCount) -> bool { if let Some(pos) = self.prev_word_pos(self.pos, word_def, n) { self.pos = pos; true } else { false } } /// Delete the previous word, maintaining the cursor at the start of the /// current word. pub fn delete_prev_word(&mut self, word_def: Word, n: RepeatCount) -> bool { if let Some(pos) = self.prev_word_pos(self.pos, word_def, n) { let end = self.pos; self.drain(pos..end, Direction::Backward); self.pos = pos; true } else { false } } fn next_word_pos(&self, pos: usize, at: At, word_def: Word, n: RepeatCount) -> Option { if pos == self.buf.len() { return None; } let mut wp = 0; let mut gis = self.buf[pos..].grapheme_indices(true); let mut gi = if at == At::BeforeEnd { // TODO Validate gis.next() } else { None }; 'outer: for _ in 0..n { wp = 0; gi = gis.next(); 'inner: loop { if let Some((i, x)) = gi { let gj = gis.next(); if let Some((j, y)) = gj { if at == At::Start && is_start_of_word(word_def, x, y) { wp = j; break 'inner; } else if at != At::Start && is_end_of_word(word_def, x, y) { if word_def == Word::Emacs || at == At::AfterEnd { wp = j; } else { wp = i; } break 'inner; } gi = gj; } else { break 'outer; } } else { break 'outer; } } } if wp == 0 { if word_def == Word::Emacs || at == At::AfterEnd { Some(self.buf.len()) } else { match gi { Some((i, _)) if i != 0 => Some(i + pos), _ => None, } } } else { Some(wp + pos) } } /// Moves the cursor to the end of next word. pub fn move_to_next_word(&mut self, at: At, word_def: Word, n: RepeatCount) -> bool { if let Some(pos) = self.next_word_pos(self.pos, at, word_def, n) { self.pos = pos; true } else { false } } fn search_char_pos(&self, cs: CharSearch, n: RepeatCount) -> Option { let mut shift = 0; let search_result = match cs { CharSearch::Backward(c) | CharSearch::BackwardAfter(c) => self.buf[..self.pos] .char_indices() .rev() .filter(|&(_, ch)| ch == c) .take(n) .last() .map(|(i, _)| i), CharSearch::Forward(c) | CharSearch::ForwardBefore(c) => { if let Some(cc) = self.grapheme_at_cursor() { shift = self.pos + cc.len(); if shift < self.buf.len() { self.buf[shift..] .char_indices() .filter(|&(_, ch)| ch == c) .take(n) .last() .map(|(i, _)| i) } else { None } } else { None } } }; 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 } } /// Move cursor to the matching character position. /// Return `true` when the search succeeds. pub fn move_to(&mut self, cs: CharSearch, n: RepeatCount) -> bool { if let Some(pos) = self.search_char_pos(cs, n) { self.pos = pos; true } else { false } } /// Kill from the cursor to the end of the current word, /// or, if between words, to the end of the next word. pub fn delete_word(&mut self, at: At, word_def: Word, n: RepeatCount) -> bool { if let Some(pos) = self.next_word_pos(self.pos, at, word_def, n) { let start = self.pos; self.drain(start..pos, Direction::Forward); true } else { false } } pub fn delete_to(&mut self, cs: CharSearch, n: RepeatCount) -> bool { let search_result = match cs { CharSearch::ForwardBefore(c) => self.search_char_pos(CharSearch::Forward(c), n), _ => self.search_char_pos(cs, n), }; if let Some(pos) = search_result { match cs { CharSearch::Backward(_) | CharSearch::BackwardAfter(_) => { let end = self.pos; self.pos = pos; self.drain(pos..end, Direction::Backward); } CharSearch::ForwardBefore(_) => { let start = self.pos; self.drain(start..pos, Direction::Forward); } CharSearch::Forward(c) => { let start = self.pos; self.drain(start..pos + c.len_utf8(), Direction::Forward); } }; true } else { false } } fn skip_whitespace(&self) -> Option { if self.pos == self.buf.len() { return None; } self.buf[self.pos..] .grapheme_indices(true) .filter_map(|(i, ch)| { if ch.chars().all(char::is_alphanumeric) { Some(i) } else { None } }) .next() .map(|i| i + self.pos) } /// Alter the next word. pub fn edit_word(&mut self, a: WordAction) -> bool { if let Some(start) = self.skip_whitespace() { if let Some(end) = self.next_word_pos(start, At::AfterEnd, Word::Emacs, 1) { if start == end { return false; } let word = self .drain(start..end, Direction::default()) .collect::(); let result = match a { 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(), }; self.insert_str(start, &result); self.pos = start + result.len(); return true; } } false } /// Transpose two words pub fn transpose_words(&mut self, n: RepeatCount) -> bool { let word_def = Word::Emacs; self.move_to_next_word(At::AfterEnd, word_def, n); let w2_end = self.pos; self.move_to_prev_word(word_def, 1); let w2_beg = self.pos; self.move_to_prev_word(word_def, n); let w1_beg = self.pos; self.move_to_next_word(At::AfterEnd, word_def, 1); let w1_end = self.pos; if w1_beg == w2_beg || w2_beg < w1_end { return false; } let w1 = self.buf[w1_beg..w1_end].to_owned(); let w2 = self .drain(w2_beg..w2_end, Direction::default()) .collect::(); self.insert_str(w2_beg, &w1); self.drain(w1_beg..w1_end, Direction::default()); self.insert_str(w1_beg, &w2); self.pos = w2_end; true } /// Replaces the content between [`start`..`end`] with `text` /// and positions the cursor to the end of text. pub fn replace(&mut self, range: Range, text: &str) { let start = range.start; for cl in &self.cl { if let Ok(mut cl) = cl.try_borrow_mut() { cl.replace(start, self.buf.index(range.clone()), text); } // Ok: while undoing, cl is borrowed. And we want to ignore // changes while undoing. } self.buf.drain(range); if start == self.buf.len() { self.buf.push_str(text); } else { self.buf.insert_str(start, text); } self.pos = start + text.len(); } /// Insert the `s`tring at the specified position. /// Return `true` if the text has been inserted at the end of the line. pub fn insert_str(&mut self, idx: usize, s: &str) -> bool { for cl in &self.cl { if let Ok(mut cl) = cl.try_borrow_mut() { cl.insert_str(idx, s); } // Ok: while undoing, cl is borrowed. And we want to ignore // changes while undoing. } if idx == self.buf.len() { self.buf.push_str(s); true } else { self.buf.insert_str(idx, s); false } } /// Remove the specified `range` in the line. pub fn delete_range(&mut self, range: Range) { self.set_pos(range.start); self.drain(range, Direction::default()); } fn drain(&mut self, range: Range, dir: Direction) -> Drain<'_> { for dl in &self.dl { let lock = dl.try_lock(); if let Ok(mut dl) = lock { dl.delete(range.start, &self.buf[range.start..range.end], dir); } } for cl in &self.cl { if let Ok(mut cl) = cl.try_borrow_mut() { cl.delete(range.start, &self.buf[range.start..range.end], dir); } // Ok: while undoing, cl is borrowed. And we want to ignore // changes while undoing. } self.buf.drain(range) } /// Return the content between current cursor position and `mvt` position. /// Return `None` when the buffer is empty or when the movement fails. pub fn copy(&self, mvt: &Movement) -> Option { if self.is_empty() { return None; } match *mvt { Movement::WholeLine => Some(self.buf.clone()), Movement::BeginningOfLine => { if self.pos == 0 { None } else { Some(self.buf[..self.pos].to_owned()) } } Movement::ViFirstPrint => { if self.pos == 0 { None } else if let Some(pos) = self.next_word_pos(0, At::Start, Word::Big, 1) { Some(self.buf[pos..self.pos].to_owned()) } else { None } } Movement::EndOfLine => { if self.pos == self.buf.len() { None } else { Some(self.buf[self.pos..].to_owned()) } } Movement::BackwardWord(n, word_def) => { if let Some(pos) = self.prev_word_pos(self.pos, word_def, n) { Some(self.buf[pos..self.pos].to_owned()) } else { None } } Movement::ForwardWord(n, at, word_def) => { if let Some(pos) = self.next_word_pos(self.pos, at, word_def, n) { Some(self.buf[self.pos..pos].to_owned()) } else { None } } Movement::ViCharSearch(n, cs) => { let search_result = match cs { CharSearch::ForwardBefore(c) => self.search_char_pos(CharSearch::Forward(c), n), _ => self.search_char_pos(cs, n), }; 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 } } Movement::BackwardChar(n) => { if let Some(pos) = self.prev_pos(n) { Some(self.buf[pos..self.pos].to_owned()) } else { None } } Movement::ForwardChar(n) => { if let Some(pos) = self.next_pos(n) { Some(self.buf[self.pos..pos].to_owned()) } else { None } } } } pub fn kill(&mut self, mvt: &Movement) -> bool { let notify = match *mvt { Movement::ForwardChar(_) | Movement::BackwardChar(_) => false, _ => true, }; if notify { if let Some(dl) = self.dl.as_ref() { let mut dl = dl.lock().unwrap(); dl.start_killing() } } let killed = match *mvt { Movement::ForwardChar(n) => { // Delete (forward) `n` characters at point. self.delete(n).is_some() } Movement::BackwardChar(n) => { // Delete `n` characters backward. self.backspace(n) } Movement::EndOfLine => { // Kill the text from point to the end of the line. self.kill_line() } Movement::WholeLine => { self.move_home(); self.kill_line() } Movement::BeginningOfLine => { // Kill backward from point to the beginning of the line. self.discard_line() } Movement::BackwardWord(n, word_def) => { // kill `n` words backward (until start of word) self.delete_prev_word(word_def, n) } Movement::ForwardWord(n, at, word_def) => { // kill `n` words forward (until start/end of word) self.delete_word(at, word_def, n) } Movement::ViCharSearch(n, cs) => self.delete_to(cs, n), Movement::ViFirstPrint => { false // TODO } }; if notify { if let Some(dl) = self.dl.as_ref() { let mut dl = dl.lock().unwrap(); dl.stop_killing() } } killed } } impl Deref for LineBuffer { type Target = str; fn deref(&self) -> &str { self.as_str() } } fn is_start_of_word(word_def: Word, previous: &str, grapheme: &str) -> bool { (!is_word_char(word_def, previous) && is_word_char(word_def, grapheme)) || (word_def == Word::Vi && !is_other_char(previous) && is_other_char(grapheme)) } fn is_end_of_word(word_def: Word, grapheme: &str, next: &str) -> bool { (!is_word_char(word_def, next) && is_word_char(word_def, grapheme)) || (word_def == Word::Vi && !is_other_char(next) && is_other_char(grapheme)) } fn is_word_char(word_def: Word, grapheme: &str) -> bool { match word_def { Word::Emacs => grapheme.chars().all(char::is_alphanumeric), Word::Vi => is_vi_word_char(grapheme), Word::Big => !grapheme.chars().any(char::is_whitespace), } } fn is_vi_word_char(grapheme: &str) -> bool { grapheme.chars().all(char::is_alphanumeric) || grapheme == "_" } fn is_other_char(grapheme: &str) -> bool { !(grapheme.chars().any(char::is_whitespace) || is_vi_word_char(grapheme)) } #[cfg(test)] mod test { use super::{ChangeListener, DeleteListener, Direction, LineBuffer, WordAction, MAX_LINE}; use crate::keymap::{At, CharSearch, Word}; use std::cell::RefCell; use std::rc::Rc; struct Listener { deleted_str: Option, } impl Listener { fn new() -> Rc> { let l = Listener { deleted_str: None }; Rc::new(RefCell::new(l)) } fn assert_deleted_str_eq(&self, expected: &str) { let actual = self.deleted_str.as_ref().expect("no deleted string"); assert_eq!(expected, actual) } } impl DeleteListener for Listener { fn start_killing(&mut self) {} fn delete(&mut self, _: usize, string: &str, _: Direction) { self.deleted_str = Some(string.to_owned()); } fn stop_killing(&mut self) {} } impl ChangeListener for Listener { fn insert_char(&mut self, _: usize, _: char) {} fn insert_str(&mut self, _: usize, _: &str) {} fn replace(&mut self, _: usize, _: &str, _: &str) {} } #[test] fn next_pos() { let s = LineBuffer::init("ö̲g̈", 0, None); assert_eq!(7, s.len()); let pos = s.next_pos(1); assert_eq!(Some(4), pos); let s = LineBuffer::init("ö̲g̈", 4, None); let pos = s.next_pos(1); assert_eq!(Some(7), pos); } #[test] fn prev_pos() { let s = LineBuffer::init("ö̲g̈", 4, None); assert_eq!(7, s.len()); let pos = s.prev_pos(1); assert_eq!(Some(0), pos); let s = LineBuffer::init("ö̲g̈", 7, None); let pos = s.prev_pos(1); assert_eq!(Some(4), pos); } #[test] fn insert() { let mut s = LineBuffer::with_capacity(MAX_LINE); let push = s.insert('α', 1).unwrap(); assert_eq!("α", s.buf); assert_eq!(2, s.pos); assert_eq!(true, push); let push = s.insert('ß', 1).unwrap(); assert_eq!("αß", s.buf); assert_eq!(4, s.pos); assert_eq!(true, push); s.pos = 0; let push = s.insert('γ', 1).unwrap(); assert_eq!("γαß", s.buf); assert_eq!(2, s.pos); assert_eq!(false, push); } #[test] fn yank_after() { let mut s = LineBuffer::init("αß", 2, None); s.move_forward(1); let ok = s.yank("γδε", 1); assert_eq!(Some(true), ok); assert_eq!("αßγδε", s.buf); assert_eq!(10, s.pos); } #[test] fn yank_before() { let mut s = LineBuffer::init("αε", 2, None); let ok = s.yank("ßγδ", 1); assert_eq!(Some(false), ok); assert_eq!("αßγδε", s.buf); assert_eq!(8, s.pos); } #[test] fn moves() { let mut s = LineBuffer::init("αß", 4, None); let ok = s.move_backward(1); assert_eq!("αß", s.buf); assert_eq!(2, s.pos); assert_eq!(true, ok); let ok = s.move_forward(1); assert_eq!("αß", s.buf); assert_eq!(4, s.pos); assert_eq!(true, ok); let ok = s.move_home(); assert_eq!("αß", s.buf); assert_eq!(0, s.pos); assert_eq!(true, ok); let ok = s.move_end(); assert_eq!("αß", s.buf); assert_eq!(4, s.pos); assert_eq!(true, ok); } #[test] fn move_grapheme() { let mut s = LineBuffer::init("ag̈", 4, None); assert_eq!(4, s.len()); let ok = s.move_backward(1); assert_eq!(true, ok); assert_eq!(1, s.pos); let ok = s.move_forward(1); assert_eq!(true, ok); assert_eq!(4, s.pos); } #[test] fn delete() { let cl = Listener::new(); let mut s = LineBuffer::init("αß", 2, Some(cl.clone())); let chars = s.delete(1); assert_eq!("α", s.buf); assert_eq!(2, s.pos); assert_eq!(Some("ß".to_owned()), chars); let ok = s.backspace(1); assert_eq!("", s.buf); assert_eq!(0, s.pos); assert_eq!(true, ok); cl.borrow().assert_deleted_str_eq("α"); } #[test] fn kill() { let cl = Listener::new(); let mut s = LineBuffer::init("αßγδε", 6, Some(cl.clone())); let ok = s.kill_line(); assert_eq!("αßγ", s.buf); assert_eq!(6, s.pos); assert_eq!(true, 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); cl.borrow().assert_deleted_str_eq("αß"); } #[test] fn transpose() { let mut s = LineBuffer::init("aßc", 1, None); let ok = s.transpose_chars(); assert_eq!("ßac", s.buf); assert_eq!(3, s.pos); assert_eq!(true, 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); 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); } #[test] fn move_to_prev_word() { let mut s = LineBuffer::init("a ß c", 6, None); // before 'c' let ok = s.move_to_prev_word(Word::Emacs, 1); assert_eq!("a ß c", s.buf); assert_eq!(2, s.pos); // before 'ß' assert!(true, ok); assert!(s.move_end()); // after 'c' assert_eq!(7, s.pos); let ok = s.move_to_prev_word(Word::Emacs, 1); assert!(true, ok); assert_eq!(6, s.pos); // before 'c' let ok = s.move_to_prev_word(Word::Emacs, 2); assert!(true, ok); assert_eq!(0, s.pos); } #[test] fn move_to_prev_vi_word() { let mut s = LineBuffer::init("alpha ,beta/rho; mu", 19, None); let ok = s.move_to_prev_word(Word::Vi, 1); assert!(ok); assert_eq!(17, s.pos); let ok = s.move_to_prev_word(Word::Vi, 1); assert!(ok); assert_eq!(15, s.pos); let ok = s.move_to_prev_word(Word::Vi, 1); assert!(ok); assert_eq!(12, s.pos); let ok = s.move_to_prev_word(Word::Vi, 1); assert!(ok); assert_eq!(11, s.pos); let ok = s.move_to_prev_word(Word::Vi, 1); assert!(ok); assert_eq!(7, s.pos); let ok = s.move_to_prev_word(Word::Vi, 1); assert!(ok); assert_eq!(6, s.pos); let ok = s.move_to_prev_word(Word::Vi, 1); assert!(ok); assert_eq!(0, s.pos); let ok = s.move_to_prev_word(Word::Vi, 1); assert!(!ok); } #[test] fn move_to_prev_big_word() { let mut s = LineBuffer::init("alpha ,beta/rho; mu", 19, None); let ok = s.move_to_prev_word(Word::Big, 1); assert!(ok); assert_eq!(17, s.pos); let ok = s.move_to_prev_word(Word::Big, 1); assert!(ok); assert_eq!(6, s.pos); let ok = s.move_to_prev_word(Word::Big, 1); assert!(ok); assert_eq!(0, s.pos); let ok = s.move_to_prev_word(Word::Big, 1); assert!(!ok); } #[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_eq!(6, s.pos); let mut s = LineBuffer::init("αßγδε", 2, None); let ok = s.move_to(CharSearch::Forward('ε'), 1); assert_eq!(true, ok); assert_eq!(8, s.pos); let mut s = LineBuffer::init("αßγδε", 2, None); let ok = s.move_to(CharSearch::Forward('ε'), 10); assert_eq!(true, ok); assert_eq!(8, s.pos); } #[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_eq!(4, s.pos); let mut s = LineBuffer::init("αßγδε", 8, None); let ok = s.move_to(CharSearch::Backward('ß'), 1); assert_eq!(true, ok); assert_eq!(2, s.pos); } #[test] fn delete_prev_word() { let cl = Listener::new(); let mut s = LineBuffer::init("a ß c", 6, Some(cl.clone())); let ok = s.delete_prev_word(Word::Big, 1); assert_eq!("a c", s.buf); assert_eq!(2, s.pos); assert_eq!(true, ok); cl.borrow().assert_deleted_str_eq("ß "); } #[test] fn move_to_next_word() { 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_eq!(4, s.pos); // after 'ß' let ok = s.move_to_next_word(At::AfterEnd, Word::Emacs, 1); assert_eq!(true, ok); assert_eq!(7, s.pos); // after 'c' s.move_home(); let ok = s.move_to_next_word(At::AfterEnd, Word::Emacs, 1); assert_eq!(true, ok); assert_eq!(1, s.pos); // after 'a' let ok = s.move_to_next_word(At::AfterEnd, Word::Emacs, 2); assert_eq!(true, ok); assert_eq!(7, s.pos); // after 'c' } #[test] fn move_to_end_of_word() { let mut s = LineBuffer::init("a ßeta c", 1, None); let ok = s.move_to_next_word(At::BeforeEnd, Word::Vi, 1); assert_eq!("a ßeta c", s.buf); assert_eq!(6, s.pos); assert_eq!(true, ok); } #[test] fn move_to_end_of_vi_word() { let mut s = LineBuffer::init("alpha ,beta/rho; mu", 0, None); let ok = s.move_to_next_word(At::BeforeEnd, Word::Vi, 1); assert!(ok); assert_eq!(4, s.pos); let ok = s.move_to_next_word(At::BeforeEnd, Word::Vi, 1); assert!(ok); assert_eq!(6, s.pos); let ok = s.move_to_next_word(At::BeforeEnd, Word::Vi, 1); assert!(ok); assert_eq!(10, s.pos); let ok = s.move_to_next_word(At::BeforeEnd, Word::Vi, 1); assert!(ok); assert_eq!(11, s.pos); let ok = s.move_to_next_word(At::BeforeEnd, Word::Vi, 1); assert!(ok); assert_eq!(14, s.pos); let ok = s.move_to_next_word(At::BeforeEnd, Word::Vi, 1); assert!(ok); assert_eq!(15, s.pos); let ok = s.move_to_next_word(At::BeforeEnd, Word::Vi, 1); assert!(ok); assert_eq!(18, s.pos); let ok = s.move_to_next_word(At::BeforeEnd, Word::Vi, 1); assert!(!ok); } #[test] fn move_to_end_of_big_word() { let mut s = LineBuffer::init("alpha ,beta/rho; mu", 0, None); let ok = s.move_to_next_word(At::BeforeEnd, Word::Big, 1); assert!(ok); assert_eq!(4, s.pos); let ok = s.move_to_next_word(At::BeforeEnd, Word::Big, 1); assert!(ok); assert_eq!(15, s.pos); let ok = s.move_to_next_word(At::BeforeEnd, Word::Big, 1); assert!(ok); assert_eq!(18, s.pos); let ok = s.move_to_next_word(At::BeforeEnd, Word::Big, 1); assert!(!ok); } #[test] fn move_to_start_of_word() { let mut s = LineBuffer::init("a ß c", 2, None); let ok = s.move_to_next_word(At::Start, Word::Emacs, 1); assert_eq!("a ß c", s.buf); assert_eq!(6, s.pos); assert_eq!(true, ok); } #[test] fn move_to_start_of_vi_word() { let mut s = LineBuffer::init("alpha ,beta/rho; mu", 0, None); let ok = s.move_to_next_word(At::Start, Word::Vi, 1); assert!(ok); assert_eq!(6, s.pos); let ok = s.move_to_next_word(At::Start, Word::Vi, 1); assert!(ok); assert_eq!(7, s.pos); let ok = s.move_to_next_word(At::Start, Word::Vi, 1); assert!(ok); assert_eq!(11, s.pos); let ok = s.move_to_next_word(At::Start, Word::Vi, 1); assert!(ok); assert_eq!(12, s.pos); let ok = s.move_to_next_word(At::Start, Word::Vi, 1); assert!(ok); assert_eq!(15, s.pos); let ok = s.move_to_next_word(At::Start, Word::Vi, 1); assert!(ok); assert_eq!(17, s.pos); let ok = s.move_to_next_word(At::Start, Word::Vi, 1); assert!(ok); assert_eq!(18, s.pos); let ok = s.move_to_next_word(At::Start, Word::Vi, 1); assert!(!ok); } #[test] fn move_to_start_of_big_word() { let mut s = LineBuffer::init("alpha ,beta/rho; mu", 0, None); let ok = s.move_to_next_word(At::Start, Word::Big, 1); assert!(ok); assert_eq!(6, s.pos); let ok = s.move_to_next_word(At::Start, Word::Big, 1); assert!(ok); assert_eq!(17, s.pos); let ok = s.move_to_next_word(At::Start, Word::Big, 1); assert!(ok); assert_eq!(18, s.pos); let ok = s.move_to_next_word(At::Start, Word::Big, 1); assert!(!ok); } #[test] fn delete_word() { let cl = Listener::new(); let mut s = LineBuffer::init("a ß c", 1, Some(cl.clone())); let ok = s.delete_word(At::AfterEnd, Word::Emacs, 1); assert_eq!("a c", s.buf); assert_eq!(1, s.pos); assert_eq!(true, 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); cl.borrow().assert_deleted_str_eq("test"); } #[test] fn delete_til_start_of_word() { let cl = Listener::new(); let mut s = LineBuffer::init("a ß c", 2, Some(cl.clone())); let ok = s.delete_word(At::Start, Word::Emacs, 1); assert_eq!("a c", s.buf); assert_eq!(2, s.pos); assert_eq!(true, ok); cl.borrow().assert_deleted_str_eq("ß "); } #[test] fn delete_to_forward() { let cl = Listener::new(); let mut s = LineBuffer::init("αßγδε", 2, Some(cl.clone())); let ok = s.delete_to(CharSearch::ForwardBefore('ε'), 1); assert_eq!(true, ok); cl.borrow().assert_deleted_str_eq("ßγδ"); assert_eq!("αε", s.buf); assert_eq!(2, s.pos); let mut s = LineBuffer::init("αßγδε", 2, Some(cl.clone())); let ok = s.delete_to(CharSearch::Forward('ε'), 1); assert_eq!(true, ok); cl.borrow().assert_deleted_str_eq("ßγδε"); assert_eq!("α", s.buf); assert_eq!(2, s.pos); } #[test] fn delete_to_backward() { let cl = Listener::new(); let mut s = LineBuffer::init("αßγδε", 8, Some(cl.clone())); let ok = s.delete_to(CharSearch::BackwardAfter('α'), 1); assert_eq!(true, ok); cl.borrow().assert_deleted_str_eq("ßγδ"); assert_eq!("αε", s.buf); assert_eq!(2, s.pos); let mut s = LineBuffer::init("αßγδε", 8, Some(cl.clone())); let ok = s.delete_to(CharSearch::Backward('ß'), 1); assert_eq!(true, ok); cl.borrow().assert_deleted_str_eq("ßγδ"); assert_eq!("αε", s.buf); assert_eq!(2, s.pos); } #[test] fn edit_word() { let mut s = LineBuffer::init("a ßeta c", 1, None); assert!(s.edit_word(WordAction::UPPERCASE)); assert_eq!("a SSETA c", s.buf); assert_eq!(7, s.pos); let mut s = LineBuffer::init("a ßetA c", 1, None); assert!(s.edit_word(WordAction::LOWERCASE)); assert_eq!("a ßeta c", s.buf); assert_eq!(7, s.pos); let mut s = LineBuffer::init("a ßETA c", 1, None); assert!(s.edit_word(WordAction::CAPITALIZE)); assert_eq!("a SSeta c", s.buf); assert_eq!(7, s.pos); let mut s = LineBuffer::init("test", 1, None); assert!(s.edit_word(WordAction::CAPITALIZE)); assert_eq!("tEst", s.buf); assert_eq!(4, s.pos); } #[test] fn transpose_words() { let mut s = LineBuffer::init("ßeta / δelta__", 15, None); assert!(s.transpose_words(1)); assert_eq!("δelta__ / ßeta", s.buf); assert_eq!(16, s.pos); let mut s = LineBuffer::init("ßeta / δelta", 14, None); assert!(s.transpose_words(1)); assert_eq!("δelta / ßeta", s.buf); assert_eq!(14, s.pos); let mut s = LineBuffer::init(" / δelta", 8, None); assert!(!s.transpose_words(1)); let mut s = LineBuffer::init("ßeta / __", 9, None); assert!(!s.transpose_words(1)); } } rustyline-6.0.0/src/test/common.rs010064400007650000024000000253421346010623700154060ustar0000000000000000///! Basic commands tests. use super::{assert_cursor, assert_line, assert_line_with_initial, init_editor}; use crate::config::EditMode; use crate::error::ReadlineError; use crate::keys::KeyPress; #[test] fn home_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_cursor( *mode, ("", ""), &[KeyPress::Home, KeyPress::Enter], ("", ""), ); assert_cursor( *mode, ("Hi", ""), &[KeyPress::Home, KeyPress::Enter], ("", "Hi"), ); if *mode == EditMode::Vi { // vi command mode assert_cursor( *mode, ("Hi", ""), &[KeyPress::Esc, KeyPress::Home, KeyPress::Enter], ("", "Hi"), ); } } } #[test] fn end_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_cursor(*mode, ("", ""), &[KeyPress::End, KeyPress::Enter], ("", "")); assert_cursor( *mode, ("H", "i"), &[KeyPress::End, KeyPress::Enter], ("Hi", ""), ); assert_cursor( *mode, ("", "Hi"), &[KeyPress::End, KeyPress::Enter], ("Hi", ""), ); if *mode == EditMode::Vi { // vi command mode assert_cursor( *mode, ("", "Hi"), &[KeyPress::Esc, KeyPress::End, KeyPress::Enter], ("Hi", ""), ); } } } #[test] fn left_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_cursor( *mode, ("Hi", ""), &[KeyPress::Left, KeyPress::Enter], ("H", "i"), ); assert_cursor( *mode, ("H", "i"), &[KeyPress::Left, KeyPress::Enter], ("", "Hi"), ); assert_cursor( *mode, ("", "Hi"), &[KeyPress::Left, KeyPress::Enter], ("", "Hi"), ); if *mode == EditMode::Vi { // vi command mode assert_cursor( *mode, ("Bye", ""), &[KeyPress::Esc, KeyPress::Left, KeyPress::Enter], ("B", "ye"), ); } } } #[test] fn right_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_cursor( *mode, ("", ""), &[KeyPress::Right, KeyPress::Enter], ("", ""), ); assert_cursor( *mode, ("", "Hi"), &[KeyPress::Right, KeyPress::Enter], ("H", "i"), ); assert_cursor( *mode, ("B", "ye"), &[KeyPress::Right, KeyPress::Enter], ("By", "e"), ); assert_cursor( *mode, ("H", "i"), &[KeyPress::Right, KeyPress::Enter], ("Hi", ""), ); if *mode == EditMode::Vi { // vi command mode assert_cursor( *mode, ("", "Hi"), &[KeyPress::Esc, KeyPress::Right, KeyPress::Enter], ("H", "i"), ); } } } #[test] fn enter_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_line(*mode, &[KeyPress::Enter], ""); assert_line(*mode, &[KeyPress::Char('a'), KeyPress::Enter], "a"); assert_line_with_initial(*mode, ("Hi", ""), &[KeyPress::Enter], "Hi"); assert_line_with_initial(*mode, ("", "Hi"), &[KeyPress::Enter], "Hi"); assert_line_with_initial(*mode, ("H", "i"), &[KeyPress::Enter], "Hi"); if *mode == EditMode::Vi { // vi command mode assert_line(*mode, &[KeyPress::Esc, KeyPress::Enter], ""); assert_line( *mode, &[KeyPress::Char('a'), KeyPress::Esc, KeyPress::Enter], "a", ); assert_line_with_initial(*mode, ("Hi", ""), &[KeyPress::Esc, KeyPress::Enter], "Hi"); assert_line_with_initial(*mode, ("", "Hi"), &[KeyPress::Esc, KeyPress::Enter], "Hi"); assert_line_with_initial(*mode, ("H", "i"), &[KeyPress::Esc, KeyPress::Enter], "Hi"); } } } #[test] fn newline_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_line(*mode, &[KeyPress::Ctrl('J')], ""); assert_line(*mode, &[KeyPress::Char('a'), KeyPress::Ctrl('J')], "a"); if *mode == EditMode::Vi { // vi command mode assert_line(*mode, &[KeyPress::Esc, KeyPress::Ctrl('J')], ""); assert_line( *mode, &[KeyPress::Char('a'), KeyPress::Esc, KeyPress::Ctrl('J')], "a", ); } } } #[test] fn eof_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { let mut editor = init_editor(*mode, &[KeyPress::Ctrl('D')]); let err = editor.readline(">>"); assert_matches!(err, Err(ReadlineError::Eof)); } assert_line( EditMode::Emacs, &[KeyPress::Char('a'), KeyPress::Ctrl('D'), KeyPress::Enter], "a", ); assert_line( EditMode::Vi, &[KeyPress::Char('a'), KeyPress::Ctrl('D')], "a", ); assert_line( EditMode::Vi, &[KeyPress::Char('a'), KeyPress::Esc, KeyPress::Ctrl('D')], "a", ); assert_line_with_initial( EditMode::Emacs, ("", "Hi"), &[KeyPress::Ctrl('D'), KeyPress::Enter], "i", ); assert_line_with_initial(EditMode::Vi, ("", "Hi"), &[KeyPress::Ctrl('D')], "Hi"); assert_line_with_initial( EditMode::Vi, ("", "Hi"), &[KeyPress::Esc, KeyPress::Ctrl('D')], "Hi", ); } #[test] fn interrupt_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { let mut editor = init_editor(*mode, &[KeyPress::Ctrl('C')]); let err = editor.readline(">>"); assert_matches!(err, Err(ReadlineError::Interrupted)); let mut editor = init_editor(*mode, &[KeyPress::Ctrl('C')]); let err = editor.readline_with_initial(">>", ("Hi", "")); assert_matches!(err, Err(ReadlineError::Interrupted)); if *mode == EditMode::Vi { // vi command mode let mut editor = init_editor(*mode, &[KeyPress::Esc, KeyPress::Ctrl('C')]); let err = editor.readline_with_initial(">>", ("Hi", "")); assert_matches!(err, Err(ReadlineError::Interrupted)); } } } #[test] fn delete_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_cursor( *mode, ("a", ""), &[KeyPress::Delete, KeyPress::Enter], ("a", ""), ); assert_cursor( *mode, ("", "a"), &[KeyPress::Delete, KeyPress::Enter], ("", ""), ); if *mode == EditMode::Vi { // vi command mode assert_cursor( *mode, ("", "a"), &[KeyPress::Esc, KeyPress::Delete, KeyPress::Enter], ("", ""), ); } } } #[test] fn ctrl_t() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_cursor( *mode, ("a", "b"), &[KeyPress::Ctrl('T'), KeyPress::Enter], ("ba", ""), ); assert_cursor( *mode, ("ab", "cd"), &[KeyPress::Ctrl('T'), KeyPress::Enter], ("acb", "d"), ); if *mode == EditMode::Vi { // vi command mode assert_cursor( *mode, ("ab", ""), &[KeyPress::Esc, KeyPress::Ctrl('T'), KeyPress::Enter], ("ba", ""), ); } } } #[test] fn ctrl_u() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_cursor( *mode, ("start of line ", "end"), &[KeyPress::Ctrl('U'), KeyPress::Enter], ("", "end"), ); assert_cursor( *mode, ("", "end"), &[KeyPress::Ctrl('U'), KeyPress::Enter], ("", "end"), ); if *mode == EditMode::Vi { // vi command mode assert_cursor( *mode, ("start of line ", "end"), &[KeyPress::Esc, KeyPress::Ctrl('U'), KeyPress::Enter], ("", " end"), ); } } } #[cfg(unix)] #[test] fn ctrl_v() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_cursor( *mode, ("", ""), &[KeyPress::Ctrl('V'), KeyPress::Char('\t'), KeyPress::Enter], ("\t", ""), ); if *mode == EditMode::Vi { // vi command mode assert_cursor( *mode, ("", ""), &[ KeyPress::Esc, KeyPress::Ctrl('V'), KeyPress::Char('\t'), KeyPress::Enter, ], ("\t", ""), ); } } } #[test] fn ctrl_w() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_cursor( *mode, ("Hello, ", "world"), &[KeyPress::Ctrl('W'), KeyPress::Enter], ("", "world"), ); assert_cursor( *mode, ("Hello, world.", ""), &[KeyPress::Ctrl('W'), KeyPress::Enter], ("Hello, ", ""), ); if *mode == EditMode::Vi { // vi command mode assert_cursor( *mode, ("Hello, world.", ""), &[KeyPress::Esc, KeyPress::Ctrl('W'), KeyPress::Enter], ("Hello, ", "."), ); } } } #[test] fn ctrl_y() { for mode in &[EditMode::Emacs /* FIXME, EditMode::Vi */] { assert_cursor( *mode, ("Hello, ", "world"), &[KeyPress::Ctrl('W'), KeyPress::Ctrl('Y'), KeyPress::Enter], ("Hello, ", "world"), ); } } #[test] fn ctrl__() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_cursor( *mode, ("Hello, ", "world"), &[KeyPress::Ctrl('W'), KeyPress::Ctrl('_'), KeyPress::Enter], ("Hello, ", "world"), ); if *mode == EditMode::Vi { // vi command mode assert_cursor( *mode, ("Hello, ", "world"), &[ KeyPress::Esc, KeyPress::Ctrl('W'), KeyPress::Ctrl('_'), KeyPress::Enter, ], ("Hello,", " world"), ); } } } rustyline-6.0.0/src/test/emacs.rs010064400007650000024000000173641355636653400152310ustar0000000000000000//! Emacs specific key bindings use super::{assert_cursor, assert_history}; use crate::config::EditMode; use crate::keys::KeyPress; #[test] fn ctrl_a() { assert_cursor( EditMode::Emacs, ("Hi", ""), &[KeyPress::Ctrl('A'), KeyPress::Enter], ("", "Hi"), ); } #[test] fn ctrl_e() { assert_cursor( EditMode::Emacs, ("", "Hi"), &[KeyPress::Ctrl('E'), KeyPress::Enter], ("Hi", ""), ); } #[test] fn ctrl_b() { assert_cursor( EditMode::Emacs, ("Hi", ""), &[KeyPress::Ctrl('B'), KeyPress::Enter], ("H", "i"), ); assert_cursor( EditMode::Emacs, ("Hi", ""), &[KeyPress::Meta('2'), KeyPress::Ctrl('B'), KeyPress::Enter], ("", "Hi"), ); assert_cursor( EditMode::Emacs, ("", "Hi"), &[ KeyPress::Meta('-'), KeyPress::Meta('2'), KeyPress::Ctrl('B'), KeyPress::Enter, ], ("Hi", ""), ); } #[test] fn ctrl_f() { assert_cursor( EditMode::Emacs, ("", "Hi"), &[KeyPress::Ctrl('F'), KeyPress::Enter], ("H", "i"), ); assert_cursor( EditMode::Emacs, ("", "Hi"), &[KeyPress::Meta('2'), KeyPress::Ctrl('F'), KeyPress::Enter], ("Hi", ""), ); assert_cursor( EditMode::Emacs, ("Hi", ""), &[ KeyPress::Meta('-'), KeyPress::Meta('2'), KeyPress::Ctrl('F'), KeyPress::Enter, ], ("", "Hi"), ); } #[test] fn ctrl_h() { assert_cursor( EditMode::Emacs, ("Hi", ""), &[KeyPress::Ctrl('H'), KeyPress::Enter], ("H", ""), ); assert_cursor( EditMode::Emacs, ("Hi", ""), &[KeyPress::Meta('2'), KeyPress::Ctrl('H'), KeyPress::Enter], ("", ""), ); assert_cursor( EditMode::Emacs, ("", "Hi"), &[ KeyPress::Meta('-'), KeyPress::Meta('2'), KeyPress::Ctrl('H'), KeyPress::Enter, ], ("", ""), ); } #[test] fn backspace() { assert_cursor( EditMode::Emacs, ("", ""), &[KeyPress::Backspace, KeyPress::Enter], ("", ""), ); assert_cursor( EditMode::Emacs, ("Hi", ""), &[KeyPress::Backspace, KeyPress::Enter], ("H", ""), ); assert_cursor( EditMode::Emacs, ("", "Hi"), &[KeyPress::Backspace, KeyPress::Enter], ("", "Hi"), ); } #[test] fn ctrl_k() { assert_cursor( EditMode::Emacs, ("Hi", ""), &[KeyPress::Ctrl('K'), KeyPress::Enter], ("Hi", ""), ); assert_cursor( EditMode::Emacs, ("", "Hi"), &[KeyPress::Ctrl('K'), KeyPress::Enter], ("", ""), ); assert_cursor( EditMode::Emacs, ("B", "ye"), &[KeyPress::Ctrl('K'), KeyPress::Enter], ("B", ""), ); } #[test] fn ctrl_n() { assert_history( EditMode::Emacs, &["line1", "line2"], &[ KeyPress::Ctrl('P'), KeyPress::Ctrl('P'), KeyPress::Ctrl('N'), KeyPress::Enter, ], "", ("line2", ""), ); } #[test] fn ctrl_p() { assert_history( EditMode::Emacs, &["line1"], &[KeyPress::Ctrl('P'), KeyPress::Enter], "", ("line1", ""), ); } #[test] fn ctrl_t() { /* FIXME assert_cursor( ("ab", "cd"), &[KeyPress::Meta('2'), KeyPress::Ctrl('T'), KeyPress::Enter], ("acdb", ""), );*/ } #[test] fn ctrl_x_ctrl_u() { assert_cursor( EditMode::Emacs, ("Hello, ", "world"), &[ KeyPress::Ctrl('W'), KeyPress::Ctrl('X'), KeyPress::Ctrl('U'), KeyPress::Enter, ], ("Hello, ", "world"), ); } #[test] fn meta_b() { assert_cursor( EditMode::Emacs, ("Hello, world!", ""), &[KeyPress::Meta('B'), KeyPress::Enter], ("Hello, ", "world!"), ); assert_cursor( EditMode::Emacs, ("Hello, world!", ""), &[KeyPress::Meta('2'), KeyPress::Meta('B'), KeyPress::Enter], ("", "Hello, world!"), ); assert_cursor( EditMode::Emacs, ("", "Hello, world!"), &[KeyPress::Meta('-'), KeyPress::Meta('B'), KeyPress::Enter], ("Hello", ", world!"), ); } #[test] fn meta_f() { assert_cursor( EditMode::Emacs, ("", "Hello, world!"), &[KeyPress::Meta('F'), KeyPress::Enter], ("Hello", ", world!"), ); assert_cursor( EditMode::Emacs, ("", "Hello, world!"), &[KeyPress::Meta('2'), KeyPress::Meta('F'), KeyPress::Enter], ("Hello, world", "!"), ); assert_cursor( EditMode::Emacs, ("Hello, world!", ""), &[KeyPress::Meta('-'), KeyPress::Meta('F'), KeyPress::Enter], ("Hello, ", "world!"), ); } #[test] fn meta_c() { assert_cursor( EditMode::Emacs, ("hi", ""), &[KeyPress::Meta('C'), KeyPress::Enter], ("hi", ""), ); assert_cursor( EditMode::Emacs, ("", "hi"), &[KeyPress::Meta('C'), KeyPress::Enter], ("Hi", ""), ); /* FIXME assert_cursor( ("", "hi test"), &[KeyPress::Meta('2'), KeyPress::Meta('C'), KeyPress::Enter], ("Hi Test", ""), );*/ } #[test] fn meta_l() { assert_cursor( EditMode::Emacs, ("Hi", ""), &[KeyPress::Meta('L'), KeyPress::Enter], ("Hi", ""), ); assert_cursor( EditMode::Emacs, ("", "HI"), &[KeyPress::Meta('L'), KeyPress::Enter], ("hi", ""), ); /* FIXME assert_cursor( ("", "HI TEST"), &[KeyPress::Meta('2'), KeyPress::Meta('L'), KeyPress::Enter], ("hi test", ""), );*/ } #[test] fn meta_u() { assert_cursor( EditMode::Emacs, ("hi", ""), &[KeyPress::Meta('U'), KeyPress::Enter], ("hi", ""), ); assert_cursor( EditMode::Emacs, ("", "hi"), &[KeyPress::Meta('U'), KeyPress::Enter], ("HI", ""), ); /* FIXME assert_cursor( ("", "hi test"), &[KeyPress::Meta('2'), KeyPress::Meta('U'), KeyPress::Enter], ("HI TEST", ""), );*/ } #[test] fn meta_d() { assert_cursor( EditMode::Emacs, ("Hello", ", world!"), &[KeyPress::Meta('D'), KeyPress::Enter], ("Hello", "!"), ); assert_cursor( EditMode::Emacs, ("Hello", ", world!"), &[KeyPress::Meta('2'), KeyPress::Meta('D'), KeyPress::Enter], ("Hello", ""), ); } #[test] fn meta_t() { assert_cursor( EditMode::Emacs, ("Hello", ", world!"), &[KeyPress::Meta('T'), KeyPress::Enter], ("world, Hello", "!"), ); /* FIXME assert_cursor( ("One Two", " Three Four"), &[KeyPress::Meta('T'), KeyPress::Enter], ("One Four Three Two", ""), );*/ } #[test] fn meta_y() { assert_cursor( EditMode::Emacs, ("Hello, world", "!"), &[ KeyPress::Ctrl('W'), KeyPress::Left, KeyPress::Ctrl('W'), KeyPress::Ctrl('Y'), KeyPress::Meta('Y'), KeyPress::Enter, ], ("world", " !"), ); } #[test] fn meta_backspace() { assert_cursor( EditMode::Emacs, ("Hello, wor", "ld!"), &[KeyPress::Meta('\x08'), KeyPress::Enter], ("Hello, ", "ld!"), ); } #[test] fn meta_digit() { assert_cursor( EditMode::Emacs, ("", ""), &[KeyPress::Meta('3'), KeyPress::Char('h'), KeyPress::Enter], ("hhh", ""), ); } rustyline-6.0.0/src/test/history.rs010064400007650000024000000133661357476110200156260ustar0000000000000000//! History related commands tests use super::assert_history; use crate::config::EditMode; use crate::keys::KeyPress; #[test] fn down_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_history( *mode, &["line1"], &[KeyPress::Down, KeyPress::Enter], "", ("", ""), ); assert_history( *mode, &["line1", "line2"], &[KeyPress::Up, KeyPress::Up, KeyPress::Down, KeyPress::Enter], "", ("line2", ""), ); assert_history( *mode, &["line1"], &[ KeyPress::Char('a'), KeyPress::Up, KeyPress::Down, // restore original line KeyPress::Enter, ], "", ("a", ""), ); assert_history( *mode, &["line1"], &[ KeyPress::Char('a'), KeyPress::Down, // noop KeyPress::Enter, ], "", ("a", ""), ); } } #[test] fn up_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_history(*mode, &[], &[KeyPress::Up, KeyPress::Enter], "", ("", "")); assert_history( *mode, &["line1"], &[KeyPress::Up, KeyPress::Enter], "", ("line1", ""), ); assert_history( *mode, &["line1", "line2"], &[KeyPress::Up, KeyPress::Up, KeyPress::Enter], "", ("line1", ""), ); } } #[test] fn ctrl_r() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_history( *mode, &[], &[KeyPress::Ctrl('R'), KeyPress::Char('o'), KeyPress::Enter], "", ("o", ""), ); assert_history( *mode, &["rustc", "cargo"], &[ KeyPress::Ctrl('R'), KeyPress::Char('o'), KeyPress::Right, // just to assert cursor pos KeyPress::Enter, ], "", ("cargo", ""), ); assert_history( *mode, &["rustc", "cargo"], &[ KeyPress::Ctrl('R'), KeyPress::Char('u'), KeyPress::Right, // just to assert cursor pos KeyPress::Enter, ], "", ("ru", "stc"), ); assert_history( *mode, &["rustc", "cargo"], &[ KeyPress::Ctrl('R'), KeyPress::Char('r'), KeyPress::Char('u'), KeyPress::Right, // just to assert cursor pos KeyPress::Enter, ], "", ("r", "ustc"), ); assert_history( *mode, &["rustc", "cargo"], &[ KeyPress::Ctrl('R'), KeyPress::Char('r'), KeyPress::Ctrl('R'), KeyPress::Right, // just to assert cursor pos KeyPress::Enter, ], "", ("r", "ustc"), ); assert_history( *mode, &["rustc", "cargo"], &[ KeyPress::Ctrl('R'), KeyPress::Char('r'), KeyPress::Char('z'), // no match KeyPress::Right, // just to assert cursor pos KeyPress::Enter, ], "", ("car", "go"), ); assert_history( EditMode::Emacs, &["rustc", "cargo"], &[ KeyPress::Char('a'), KeyPress::Ctrl('R'), KeyPress::Char('r'), KeyPress::Ctrl('G'), // abort (FIXME: doesn't work with vi mode) KeyPress::Enter, ], "", ("a", ""), ); } } #[test] fn ctrl_r_with_long_prompt() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_history( *mode, &["rustc", "cargo"], &[KeyPress::Ctrl('R'), KeyPress::Char('o'), KeyPress::Enter], ">>>>>>>>>>>>>>>>>>>>>>>>>>> ", ("cargo", ""), ); } } #[test] fn ctrl_s() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_history( *mode, &["rustc", "cargo"], &[ KeyPress::Ctrl('R'), KeyPress::Char('r'), KeyPress::Ctrl('R'), KeyPress::Ctrl('S'), KeyPress::Right, // just to assert cursor pos KeyPress::Enter, ], "", ("car", "go"), ); } } #[test] fn meta_lt() { assert_history( EditMode::Emacs, &[""], &[KeyPress::Meta('<'), KeyPress::Enter], "", ("", ""), ); assert_history( EditMode::Emacs, &["rustc", "cargo"], &[KeyPress::Meta('<'), KeyPress::Enter], "", ("rustc", ""), ); } #[test] fn meta_gt() { assert_history( EditMode::Emacs, &[""], &[KeyPress::Meta('>'), KeyPress::Enter], "", ("", ""), ); assert_history( EditMode::Emacs, &["rustc", "cargo"], &[KeyPress::Meta('<'), KeyPress::Meta('>'), KeyPress::Enter], "", ("", ""), ); assert_history( EditMode::Emacs, &["rustc", "cargo"], &[ KeyPress::Char('a'), KeyPress::Meta('<'), KeyPress::Meta('>'), // restore original line KeyPress::Enter, ], "", ("a", ""), ); } rustyline-6.0.0/src/test/mod.rs010064400007650000024000000077311360441142000146700ustar0000000000000000use std::collections::HashMap; use std::sync::{Arc, RwLock}; use std::vec::IntoIter; 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::tty::Sink; use crate::validate::Validator; use crate::{Context, Editor, Helper, Result}; mod common; mod emacs; mod history; mod vi_cmd; mod vi_insert; fn init_editor(mode: EditMode, keys: &[KeyPress]) -> Editor<()> { let config = Config::builder().edit_mode(mode).build(); let mut editor = Editor::<()>::with_config(config); editor.term.keys.extend(keys.iter().cloned()); editor } struct SimpleCompleter; impl Completer for SimpleCompleter { type Candidate = String; fn complete( &self, line: &str, _pos: usize, _ctx: &Context<'_>, ) -> Result<(usize, Vec)> { Ok((0, vec![line.to_owned() + "t"])) } } impl Helper for SimpleCompleter {} impl Hinter for SimpleCompleter {} impl Highlighter for SimpleCompleter {} impl Validator for SimpleCompleter {} #[test] fn complete_line() { let mut out = Sink::new(); let history = crate::history::History::new(); 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 cmd = super::complete_line(&mut rdr, &mut s, &mut input_state, &Config::default()).unwrap(); assert_eq!(Some(Cmd::AcceptLine), cmd); assert_eq!("rust", s.line.as_str()); assert_eq!(4, s.line.pos()); } // `keys`: keys to press // `expected_line`: line after enter key fn assert_line(mode: EditMode, keys: &[KeyPress], expected_line: &str) { let mut editor = init_editor(mode, keys); let actual_line = editor.readline(">>").unwrap(); assert_eq!(expected_line, actual_line); } // `initial`: line status before `keys` pressed: strings before and after cursor // `keys`: keys to press // `expected_line`: line after enter key fn assert_line_with_initial( mode: EditMode, initial: (&str, &str), keys: &[KeyPress], expected_line: &str, ) { let mut editor = init_editor(mode, keys); let actual_line = editor.readline_with_initial(">>", initial).unwrap(); assert_eq!(expected_line, actual_line); } // `initial`: line status before `keys` pressed: strings before and after cursor // `keys`: keys to press // `expected`: line status before enter key: strings before and after cursor fn assert_cursor(mode: EditMode, initial: (&str, &str), keys: &[KeyPress], expected: (&str, &str)) { let mut editor = init_editor(mode, keys); let actual_line = editor.readline_with_initial("", initial).unwrap(); assert_eq!(expected.0.to_owned() + expected.1, actual_line); assert_eq!(expected.0.len(), editor.term.cursor); } // `entries`: history entries before `keys` pressed // `keys`: keys to press // `expected`: line status before enter key: strings before and after cursor fn assert_history( mode: EditMode, entries: &[&str], keys: &[KeyPress], prompt: &str, expected: (&str, &str), ) { let mut editor = init_editor(mode, keys); for entry in entries { editor.history.add(*entry); } let actual_line = editor.readline(prompt).unwrap(); assert_eq!(expected.0.to_owned() + expected.1, actual_line); if prompt.is_empty() { assert_eq!(expected.0.len(), editor.term.cursor); } } #[test] fn unknown_esc_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_line(*mode, &[KeyPress::UnknownEscSeq, KeyPress::Enter], ""); } } #[test] fn test_send() { fn assert_send() {} assert_send::>(); } #[test] fn test_sync() { fn assert_sync() {} assert_sync::>(); } rustyline-6.0.0/src/test/vi_cmd.rs010064400007650000024000000301421355636653400153670ustar0000000000000000//! Vi command mode specific key bindings use super::{assert_cursor, assert_history}; use crate::config::EditMode; use crate::keys::KeyPress; #[test] fn dollar() { assert_cursor( EditMode::Vi, ("", "Hi"), &[KeyPress::Esc, KeyPress::Char('$'), KeyPress::Enter], ("Hi", ""), // FIXME ); } /*#[test] fn dot() { // TODO }*/ #[test] fn semi_colon() { assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[ KeyPress::Esc, KeyPress::Char('f'), KeyPress::Char('o'), KeyPress::Char(';'), KeyPress::Enter, ], ("Hello, w", "orld!"), ); } #[test] fn comma() { assert_cursor( EditMode::Vi, ("Hello, w", "orld!"), &[ KeyPress::Esc, KeyPress::Char('f'), KeyPress::Char('l'), KeyPress::Char(','), KeyPress::Enter, ], ("Hel", "lo, world!"), ); } #[test] fn zero() { assert_cursor( EditMode::Vi, ("Hi", ""), &[KeyPress::Esc, KeyPress::Char('0'), KeyPress::Enter], ("", "Hi"), ); } #[test] fn caret() { assert_cursor( EditMode::Vi, (" Hi", ""), &[KeyPress::Esc, KeyPress::Char('^'), KeyPress::Enter], (" ", "Hi"), ); } #[test] fn a() { assert_cursor( EditMode::Vi, ("B", "e"), &[ KeyPress::Esc, KeyPress::Char('a'), KeyPress::Char('y'), KeyPress::Enter, ], ("By", "e"), ); } #[test] fn uppercase_a() { assert_cursor( EditMode::Vi, ("", "By"), &[ KeyPress::Esc, KeyPress::Char('A'), KeyPress::Char('e'), KeyPress::Enter, ], ("Bye", ""), ); } #[test] fn b() { assert_cursor( EditMode::Vi, ("Hello, world!", ""), &[KeyPress::Esc, KeyPress::Char('b'), KeyPress::Enter], ("Hello, ", "world!"), ); assert_cursor( EditMode::Vi, ("Hello, world!", ""), &[ KeyPress::Esc, KeyPress::Char('2'), KeyPress::Char('b'), KeyPress::Enter, ], ("Hello", ", world!"), ); } #[test] fn uppercase_b() { assert_cursor( EditMode::Vi, ("Hello, world!", ""), &[KeyPress::Esc, KeyPress::Char('B'), KeyPress::Enter], ("Hello, ", "world!"), ); assert_cursor( EditMode::Vi, ("Hello, world!", ""), &[ KeyPress::Esc, KeyPress::Char('2'), KeyPress::Char('B'), KeyPress::Enter, ], ("", "Hello, world!"), ); } #[test] fn uppercase_c() { assert_cursor( EditMode::Vi, ("Hello, w", "orld!"), &[ KeyPress::Esc, KeyPress::Char('C'), KeyPress::Char('i'), KeyPress::Enter, ], ("Hello, i", ""), ); } #[test] fn ctrl_k() { for key in &[KeyPress::Char('D'), KeyPress::Ctrl('K')] { assert_cursor( EditMode::Vi, ("Hi", ""), &[KeyPress::Esc, *key, KeyPress::Enter], ("H", ""), ); assert_cursor( EditMode::Vi, ("", "Hi"), &[KeyPress::Esc, *key, KeyPress::Enter], ("", ""), ); assert_cursor( EditMode::Vi, ("By", "e"), &[KeyPress::Esc, *key, KeyPress::Enter], ("B", ""), ); } } #[test] fn e() { assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[KeyPress::Esc, KeyPress::Char('e'), KeyPress::Enter], ("Hell", "o, world!"), ); assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[ KeyPress::Esc, KeyPress::Char('2'), KeyPress::Char('e'), KeyPress::Enter, ], ("Hello, worl", "d!"), ); } #[test] fn uppercase_e() { assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[KeyPress::Esc, KeyPress::Char('E'), KeyPress::Enter], ("Hello", ", world!"), ); assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[ KeyPress::Esc, KeyPress::Char('2'), KeyPress::Char('E'), KeyPress::Enter, ], ("Hello, world", "!"), ); } #[test] fn f() { assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[ KeyPress::Esc, KeyPress::Char('f'), KeyPress::Char('r'), KeyPress::Enter, ], ("Hello, wo", "rld!"), ); assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[ KeyPress::Esc, KeyPress::Char('3'), KeyPress::Char('f'), KeyPress::Char('l'), KeyPress::Enter, ], ("Hello, wor", "ld!"), ); } #[test] fn uppercase_f() { assert_cursor( EditMode::Vi, ("Hello, world!", ""), &[ KeyPress::Esc, KeyPress::Char('F'), KeyPress::Char('r'), KeyPress::Enter, ], ("Hello, wo", "rld!"), ); assert_cursor( EditMode::Vi, ("Hello, world!", ""), &[ KeyPress::Esc, KeyPress::Char('3'), KeyPress::Char('F'), KeyPress::Char('l'), KeyPress::Enter, ], ("He", "llo, world!"), ); } #[test] fn i() { assert_cursor( EditMode::Vi, ("Be", ""), &[ KeyPress::Esc, KeyPress::Char('i'), KeyPress::Char('y'), KeyPress::Enter, ], ("By", "e"), ); } #[test] fn uppercase_i() { assert_cursor( EditMode::Vi, ("Be", ""), &[ KeyPress::Esc, KeyPress::Char('I'), KeyPress::Char('y'), KeyPress::Enter, ], ("y", "Be"), ); } #[test] fn u() { assert_cursor( EditMode::Vi, ("Hello, ", "world"), &[ KeyPress::Esc, KeyPress::Ctrl('W'), KeyPress::Char('u'), KeyPress::Enter, ], ("Hello,", " world"), ); } #[test] fn w() { assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[KeyPress::Esc, KeyPress::Char('w'), KeyPress::Enter], ("Hello", ", world!"), ); assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[ KeyPress::Esc, KeyPress::Char('2'), KeyPress::Char('w'), KeyPress::Enter, ], ("Hello, ", "world!"), ); } #[test] fn uppercase_w() { assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[KeyPress::Esc, KeyPress::Char('W'), KeyPress::Enter], ("Hello, ", "world!"), ); assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[ KeyPress::Esc, KeyPress::Char('2'), KeyPress::Char('W'), KeyPress::Enter, ], ("Hello, world", "!"), ); } #[test] fn x() { assert_cursor( EditMode::Vi, ("", "a"), &[KeyPress::Esc, KeyPress::Char('x'), KeyPress::Enter], ("", ""), ); } #[test] fn uppercase_x() { assert_cursor( EditMode::Vi, ("Hi", ""), &[KeyPress::Esc, KeyPress::Char('X'), KeyPress::Enter], ("", "i"), ); } #[test] fn h() { for key in &[ KeyPress::Char('h'), KeyPress::Ctrl('H'), KeyPress::Backspace, ] { assert_cursor( EditMode::Vi, ("Bye", ""), &[KeyPress::Esc, *key, KeyPress::Enter], ("B", "ye"), ); assert_cursor( EditMode::Vi, ("Bye", ""), &[KeyPress::Esc, KeyPress::Char('2'), *key, KeyPress::Enter], ("", "Bye"), ); } } #[test] fn l() { for key in &[KeyPress::Char('l'), KeyPress::Char(' ')] { assert_cursor( EditMode::Vi, ("", "Hi"), &[KeyPress::Esc, *key, KeyPress::Enter], ("H", "i"), ); assert_cursor( EditMode::Vi, ("", "Hi"), &[KeyPress::Esc, KeyPress::Char('2'), *key, KeyPress::Enter], ("Hi", ""), ); } } #[test] fn j() { for key in &[ KeyPress::Char('j'), KeyPress::Char('+'), KeyPress::Ctrl('N'), ] { assert_history( EditMode::Vi, &["line1", "line2"], &[ KeyPress::Esc, KeyPress::Ctrl('P'), KeyPress::Ctrl('P'), *key, KeyPress::Enter, ], "", ("line2", ""), ); } } #[test] fn k() { for key in &[ KeyPress::Char('k'), KeyPress::Char('-'), KeyPress::Ctrl('P'), ] { assert_history( EditMode::Vi, &["line1"], &[KeyPress::Esc, *key, KeyPress::Enter], "", ("line1", ""), ); } } #[test] fn p() { assert_cursor( EditMode::Vi, ("Hello, ", "world"), &[ KeyPress::Esc, KeyPress::Ctrl('W'), KeyPress::Char('p'), KeyPress::Enter, ], (" Hello", ",world"), ); } #[test] fn uppercase_p() { assert_cursor( EditMode::Vi, ("Hello, ", "world"), &[ KeyPress::Esc, KeyPress::Ctrl('W'), KeyPress::Char('P'), KeyPress::Enter, ], ("Hello", ", world"), ); } #[test] fn r() { assert_cursor( EditMode::Vi, ("Hi", ", world!"), &[ KeyPress::Esc, KeyPress::Char('r'), KeyPress::Char('o'), KeyPress::Enter, ], ("H", "o, world!"), ); assert_cursor( EditMode::Vi, ("He", "llo, world!"), &[ KeyPress::Esc, KeyPress::Char('4'), KeyPress::Char('r'), KeyPress::Char('i'), KeyPress::Enter, ], ("Hiii", "i, world!"), ); } #[test] fn s() { assert_cursor( EditMode::Vi, ("Hi", ", world!"), &[ KeyPress::Esc, KeyPress::Char('s'), KeyPress::Char('o'), KeyPress::Enter, ], ("Ho", ", world!"), ); assert_cursor( EditMode::Vi, ("He", "llo, world!"), &[ KeyPress::Esc, KeyPress::Char('4'), KeyPress::Char('s'), KeyPress::Char('i'), KeyPress::Enter, ], ("Hi", ", world!"), ); } #[test] fn uppercase_s() { assert_cursor( EditMode::Vi, ("Hello, ", "world"), &[KeyPress::Esc, KeyPress::Char('S'), KeyPress::Enter], ("", ""), ); } #[test] fn t() { assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[ KeyPress::Esc, KeyPress::Char('t'), KeyPress::Char('r'), KeyPress::Enter, ], ("Hello, w", "orld!"), ); assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[ KeyPress::Esc, KeyPress::Char('3'), KeyPress::Char('t'), KeyPress::Char('l'), KeyPress::Enter, ], ("Hello, wo", "rld!"), ); } #[test] fn uppercase_t() { assert_cursor( EditMode::Vi, ("Hello, world!", ""), &[ KeyPress::Esc, KeyPress::Char('T'), KeyPress::Char('r'), KeyPress::Enter, ], ("Hello, wor", "ld!"), ); assert_cursor( EditMode::Vi, ("Hello, world!", ""), &[ KeyPress::Esc, KeyPress::Char('3'), KeyPress::Char('T'), KeyPress::Char('l'), KeyPress::Enter, ], ("Hel", "lo, world!"), ); } rustyline-6.0.0/src/test/vi_insert.rs010064400007650000024000000020501346010623700161070ustar0000000000000000//! Vi insert mode specific key bindings use super::assert_cursor; use crate::config::EditMode; use crate::keys::KeyPress; #[test] fn insert_mode_by_default() { assert_cursor( EditMode::Vi, ("", ""), &[KeyPress::Char('a'), KeyPress::Enter], ("a", ""), ); } #[test] fn ctrl_h() { assert_cursor( EditMode::Vi, ("Hi", ""), &[KeyPress::Ctrl('H'), KeyPress::Enter], ("H", ""), ); } #[test] fn backspace() { assert_cursor( EditMode::Vi, ("", ""), &[KeyPress::Backspace, KeyPress::Enter], ("", ""), ); assert_cursor( EditMode::Vi, ("Hi", ""), &[KeyPress::Backspace, KeyPress::Enter], ("H", ""), ); assert_cursor( EditMode::Vi, ("", "Hi"), &[KeyPress::Backspace, KeyPress::Enter], ("", "Hi"), ); } #[test] fn esc() { assert_cursor( EditMode::Vi, ("", ""), &[KeyPress::Char('a'), KeyPress::Esc, KeyPress::Enter], ("", "a"), ); } rustyline-6.0.0/src/tty/mod.rs010064400007650000024000000121521357476110200145350ustar0000000000000000//! This module implements and describes common TTY methods & traits use crate::config::{BellStyle, ColorMode, Config, OutputStreamType}; use crate::highlight::Highlighter; use crate::keys::KeyPress; use crate::layout::{Layout, Position}; use crate::line_buffer::LineBuffer; use crate::Result; /// Terminal state pub trait RawMode: Sized { /// Disable RAW mode for the terminal. fn disable_raw_mode(&self) -> Result<()>; } /// Translate bytes read from stdin to keys. pub trait RawReader { /// Blocking read of key pressed. 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; } /// Display prompt, line and cursor in terminal output pub trait Renderer { type Reader: RawReader; fn move_cursor(&mut self, old: Position, new: Position) -> Result<()>; /// Display `prompt`, line and cursor in terminal output #[allow(clippy::too_many_arguments)] fn refresh_line( &mut self, prompt: &str, line: &LineBuffer, hint: Option<&str>, old_layout: &Layout, new_layout: &Layout, highlighter: Option<&dyn Highlighter>, ) -> Result<()>; /// Calculate the number of columns and rows used to display `s` on a /// `cols` width terminal starting at `orig`. fn calculate_position(&self, s: &str, orig: Position) -> Position; fn write_and_flush(&self, buf: &[u8]) -> Result<()>; /// Beep, used for completion when there is nothing to complete or when all /// the choices were already shown. fn beep(&mut self) -> Result<()>; /// Clear the screen. Used to handle ctrl+l fn clear_screen(&mut self) -> Result<()>; /// Check if a SIGWINCH signal has been received fn sigwinch(&self) -> bool; /// Update the number of columns/rows in the current terminal. fn update_size(&mut self); /// Get the number of columns in the current terminal. fn get_columns(&self) -> usize; /// Get the number of rows in the current terminal. fn get_rows(&self) -> usize; /// Check if output supports colors. fn colors_enabled(&self) -> bool; /// Make sure prompt is at the leftmost edge of the screen fn move_cursor_at_leftmost(&mut self, rdr: &mut Self::Reader) -> Result<()>; } impl<'a, R: Renderer + ?Sized> Renderer for &'a mut R { type Reader = R::Reader; fn move_cursor(&mut self, old: Position, new: Position) -> Result<()> { (**self).move_cursor(old, new) } fn refresh_line( &mut self, prompt: &str, line: &LineBuffer, hint: Option<&str>, old_layout: &Layout, new_layout: &Layout, highlighter: Option<&dyn Highlighter>, ) -> Result<()> { (**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 write_and_flush(&self, buf: &[u8]) -> Result<()> { (**self).write_and_flush(buf) } fn beep(&mut self) -> Result<()> { (**self).beep() } fn clear_screen(&mut self) -> Result<()> { (**self).clear_screen() } fn sigwinch(&self) -> bool { (**self).sigwinch() } fn update_size(&mut self) { (**self).update_size() } fn get_columns(&self) -> usize { (**self).get_columns() } fn get_rows(&self) -> usize { (**self).get_rows() } fn colors_enabled(&self) -> bool { (**self).colors_enabled() } fn move_cursor_at_leftmost(&mut self, rdr: &mut R::Reader) -> Result<()> { (**self).move_cursor_at_leftmost(rdr) } } /// Terminal contract pub trait Term { type Reader: RawReader; // rl_instream type Writer: Renderer; // rl_outstream type Mode: RawMode; fn new( color_mode: ColorMode, stream: OutputStreamType, tab_stop: usize, bell_style: BellStyle, ) -> Self; /// Check if current terminal can provide a rich line-editing user /// interface. fn is_unsupported(&self) -> bool; /// check if stdin is connected to a terminal. fn is_stdin_tty(&self) -> bool; /// 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; /// Create a RAW reader fn create_reader(&self, config: &Config) -> 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::*; } } rustyline-6.0.0/src/tty/test.rs010064400007650000024000000075261357476110200147460ustar0000000000000000//! Tests specific definitions use std::iter::IntoIterator; use std::slice::Iter; use std::vec::IntoIter; use super::{RawMode, RawReader, Renderer, Term}; use crate::config::{BellStyle, ColorMode, Config, OutputStreamType}; use crate::error::ReadlineError; use crate::highlight::Highlighter; use crate::keys::KeyPress; use crate::layout::{Layout, Position}; use crate::line_buffer::LineBuffer; use crate::Result; pub type Mode = (); impl RawMode for Mode { fn disable_raw_mode(&self) -> Result<()> { Ok(()) } } impl<'a> RawReader for Iter<'a, KeyPress> { fn next_key(&mut self, _: bool) -> Result { match self.next() { Some(key) => Ok(*key), None => Err(ReadlineError::Eof), } } #[cfg(unix)] fn next_char(&mut self) -> Result { unimplemented!(); } fn read_pasted_text(&mut self) -> Result { unimplemented!() } } impl RawReader for IntoIter { fn next_key(&mut self, _: bool) -> Result { match self.next() { Some(key) => Ok(key), None => Err(ReadlineError::Eof), } } #[cfg(unix)] fn next_char(&mut self) -> Result { match self.next() { Some(KeyPress::Char(c)) => Ok(c), None => Err(ReadlineError::Eof), _ => unimplemented!(), } } fn read_pasted_text(&mut self) -> Result { unimplemented!() } } pub struct Sink {} impl Sink { pub fn new() -> Sink { Sink {} } } impl Renderer for Sink { type Reader = IntoIter; fn move_cursor(&mut self, _: Position, _: Position) -> Result<()> { Ok(()) } fn refresh_line( &mut self, _prompt: &str, _line: &LineBuffer, _hint: Option<&str>, _old_layout: &Layout, _new_layout: &Layout, _highlighter: Option<&dyn Highlighter>, ) -> Result<()> { Ok(()) } fn calculate_position(&self, s: &str, orig: Position) -> Position { let mut pos = orig; pos.col += s.len(); pos } fn write_and_flush(&self, _: &[u8]) -> Result<()> { Ok(()) } fn beep(&mut self) -> Result<()> { Ok(()) } fn clear_screen(&mut self) -> Result<()> { Ok(()) } fn sigwinch(&self) -> bool { false } fn update_size(&mut self) {} fn get_columns(&self) -> usize { 80 } fn get_rows(&self) -> usize { 24 } fn colors_enabled(&self) -> bool { false } fn move_cursor_at_leftmost(&mut self, _: &mut IntoIter) -> Result<()> { Ok(()) } } pub type Terminal = DummyTerminal; #[derive(Clone, Debug)] pub struct DummyTerminal { pub keys: Vec, pub cursor: usize, // cursor position before last command pub color_mode: ColorMode, pub bell_style: BellStyle, } impl Term for DummyTerminal { type Mode = Mode; type Reader = IntoIter; type Writer = Sink; fn new( color_mode: ColorMode, _stream: OutputStreamType, _tab_stop: usize, bell_style: BellStyle, ) -> DummyTerminal { DummyTerminal { keys: Vec::new(), cursor: 0, color_mode, bell_style, } } // Init checks: fn is_unsupported(&self) -> bool { false } fn is_stdin_tty(&self) -> bool { true } fn is_output_tty(&self) -> bool { false } // Interactive loop: fn enable_raw_mode(&mut self) -> Result { Ok(()) } fn create_reader(&self, _: &Config) -> Result> { Ok(self.keys.clone().into_iter()) } fn create_writer(&self) -> Sink { Sink::new() } } #[cfg(unix)] pub fn suspend() -> Result<()> { Ok(()) } rustyline-6.0.0/src/tty/unix.rs010064400007650000024000000740411360441142000147330ustar0000000000000000//! Unix specific definitions use std; use std::cmp::Ordering; use std::io::{self, Read, Write}; use std::os::unix::io::{AsRawFd, RawFd}; use std::sync; use std::sync::atomic; 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 unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; use utf8parse::{Parser, Receiver}; use super::{RawMode, RawReader, Renderer, Term}; use crate::config::{BellStyle, ColorMode, Config, OutputStreamType}; use crate::error; use crate::highlight::Highlighter; use crate::keys::{self, KeyPress}; use crate::layout::{Layout, Position}; use crate::line_buffer::LineBuffer; use crate::Result; const STDIN_FILENO: RawFd = libc::STDIN_FILENO; /// Unsupported Terminals that don't support RAW mode const UNSUPPORTED_TERM: [&str; 3] = ["dumb", "cons25", "emacs"]; const BRACKETED_PASTE_ON: &[u8] = b"\x1b[?2004h"; const BRACKETED_PASTE_OFF: &[u8] = b"\x1b[?2004l"; impl AsRawFd for OutputStreamType { fn as_raw_fd(&self) -> RawFd { match self { OutputStreamType::Stdout => libc::STDOUT_FILENO, OutputStreamType::Stderr => libc::STDERR_FILENO, } } } nix::ioctl_read_bad!(win_size, libc::TIOCGWINSZ, libc::winsize); #[allow(clippy::identity_conversion)] fn get_win_size(fileno: &T) -> (usize, usize) { use std::mem::zeroed; 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 _ => (80, 24), } } } /// Check TERM environment variable to see if current term is in our /// unsupported list fn is_unsupported_term() -> bool { match std::env::var("TERM") { Ok(term) => { for iter in &UNSUPPORTED_TERM { if (*iter).eq_ignore_ascii_case(&term) { return true; } } false } Err(_) => false, } } /// Return whether or not STDIN, STDOUT or STDERR is a TTY fn is_a_tty(fd: RawFd) -> bool { unsafe { libc::isatty(fd) != 0 } } pub struct PosixMode { termios: termios::Termios, out: Option, } #[cfg(not(test))] pub type Mode = PosixMode; impl RawMode for PosixMode { /// Disable RAW mode for the terminal. fn disable_raw_mode(&self) -> Result<()> { termios::tcsetattr(STDIN_FILENO, SetArg::TCSADRAIN, &self.termios)?; // disable bracketed paste if let Some(out) = self.out { write_and_flush(out, BRACKETED_PASTE_OFF)?; } Ok(()) } } // Rust std::io::Stdin is buffered with no way to know if bytes are available. // So we use low-level stuff instead... struct StdinRaw {} impl Read for StdinRaw { fn read(&mut self, buf: &mut [u8]) -> io::Result { loop { let res = unsafe { libc::read( STDIN_FILENO, buf.as_mut_ptr() as *mut libc::c_void, buf.len() as libc::size_t, ) }; if res == -1 { let error = io::Error::last_os_error(); if error.kind() != io::ErrorKind::Interrupted || SIGWINCH.load(atomic::Ordering::Relaxed) { return Err(error); } } else { #[allow(clippy::cast_sign_loss)] return Ok(res as usize); } } } } /// Console input reader pub struct PosixRawReader { stdin: StdinRaw, timeout_ms: i32, buf: [u8; 1], parser: Parser, receiver: Utf8, } struct Utf8 { c: Option, valid: bool, } impl PosixRawReader { fn new(config: &Config) -> Result { Ok(Self { stdin: StdinRaw {}, timeout_ms: config.keyseq_timeout(), buf: [0; 1], parser: Parser::new(), receiver: Utf8 { c: None, valid: true, }, }) } /// Handle ESC sequences fn escape_sequence(&mut self) -> Result { // Read the next byte representing the escape sequence. let seq1 = self.next_char()?; if seq1 == '[' { // ESC [ sequences. (CSI) self.escape_csi() } else if seq1 == 'O' { // xterm // ESC O sequences. (SS3) self.escape_o() } else if seq1 == '\x1b' { // ESC ESC Ok(KeyPress::Esc) } else { // TODO ESC-R (r): Undo all changes made to this line. Ok(KeyPress::Meta(seq1)) } } /// Handle ESC [ 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) } _ => { // Extended escape, read additional byte. self.extended_escape(seq2) } } } else if seq2 == '[' { 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), _ => { debug!(target: "rustyline", "unsupported esc sequence: ESC [ [ {:?}", seq3); KeyPress::UnknownEscSeq } }) } 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, _ => { debug!(target: "rustyline", "unsupported esc sequence: ESC [ {:?}", seq2); KeyPress::UnknownEscSeq } }) } } /// Handle ESC [ escape sequences #[allow(clippy::cognitive_complexity)] 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 _ => { debug!(target: "rustyline", "unsupported esc sequence: ESC [ {} ~", seq2); KeyPress::UnknownEscSeq } }) } 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 _ => { debug!(target: "rustyline", "unsupported esc sequence: ESC [ {}{} ~", seq2, seq3); KeyPress::UnknownEscSeq } }) } else if seq4 == ';' { let seq5 = self.next_char()?; if seq5.is_digit(10) { let seq6 = self.next_char()?; if seq6.is_digit(10) { self.next_char()?; // 'R' expected } else if seq6 == 'R' { } else { debug!(target: "rustyline", "unsupported esc sequence: ESC [ {}{} ; {} {}", seq2, seq3, seq5, seq6); } } else { debug!(target: "rustyline", "unsupported esc sequence: ESC [ {}{} ; {:?}", seq2, seq3, seq5); } 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, _ => { debug!(target: "rustyline", "unsupported esc sequence: ESC [ {}{}{}~", seq2, seq3, seq4); KeyPress::UnknownEscSeq } }) } else { debug!(target: "rustyline", "unsupported esc sequence: ESC [ {}{}{} {}", seq2, seq3, seq4, seq5); Ok(KeyPress::UnknownEscSeq) } } else { debug!(target: "rustyline", "unsupported esc sequence: ESC [ {}{} {:?}", seq2, seq3, seq4); Ok(KeyPress::UnknownEscSeq) } } else if seq3 == ';' { let seq4 = self.next_char()?; if seq4.is_digit(10) { let seq5 = self.next_char()?; if seq5.is_digit(10) { self.next_char()?; // 'R' expected Ok(KeyPress::UnknownEscSeq) } 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, _ => { debug!(target: "rustyline", "unsupported esc sequence: ESC [ 1 ; {} {:?}", seq4, seq5); KeyPress::UnknownEscSeq } }) } else { debug!(target: "rustyline", "unsupported esc sequence: ESC [ {} ; {} {:?}", seq2, seq4, seq5); Ok(KeyPress::UnknownEscSeq) } } else { debug!(target: "rustyline", "unsupported esc sequence: ESC [ {} ; {:?}", seq2, seq4); Ok(KeyPress::UnknownEscSeq) } } else { Ok(match (seq2, seq3) { ('5', 'A') => KeyPress::ControlUp, ('5', 'B') => KeyPress::ControlDown, ('5', 'C') => KeyPress::ControlRight, ('5', 'D') => KeyPress::ControlLeft, _ => { debug!(target: "rustyline", "unsupported esc sequence: ESC [ {} {:?}", seq2, seq3); KeyPress::UnknownEscSeq } }) } } /// Handle ESC O 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 _ => { debug!(target: "rustyline", "unsupported esc sequence: ESC O {:?}", seq2); KeyPress::UnknownEscSeq } }) } 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) } } impl RawReader for PosixRawReader { 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 timeout_ms = if single_esc_abort && self.timeout_ms == -1 { 0 } else { self.timeout_ms }; match self.poll(timeout_ms) { Ok(n) if n == 0 => { // single escape } Ok(_) => { // escape sequence key = self.escape_sequence()? } // Err(ref e) if e.kind() == ErrorKind::Interrupted => continue, Err(e) => return Err(e.into()), } } debug!(target: "rustyline", "key: {:?}", key); Ok(key) } fn next_char(&mut self) -> Result { loop { let n = self.stdin.read(&mut self.buf)?; if n == 0 { return Err(error::ReadlineError::Eof); } let b = self.buf[0]; self.parser.advance(&mut self.receiver, b); if !self.receiver.valid { return Err(error::ReadlineError::Utf8Error); } else if let Some(c) = self.receiver.c.take() { return Ok(c); } } } fn read_pasted_text(&mut self) -> Result { let mut buffer = String::new(); loop { match self.next_char()? { '\x1b' => { let key = self.escape_sequence()?; if key == KeyPress::BracketedPasteEnd { break; } else { continue; // TODO validate } } c => buffer.push(c), }; } let buffer = buffer.replace("\r\n", "\n"); let buffer = buffer.replace("\r", "\n"); Ok(buffer) } } impl Receiver for Utf8 { /// Called whenever a code point is parsed successfully fn codepoint(&mut self, c: char) { self.c = Some(c); self.valid = true; } /// Called when an invalid_sequence is detected fn invalid_sequence(&mut self) { self.c = None; self.valid = false; } } /// Console output writer pub struct PosixRenderer { out: OutputStreamType, cols: usize, // Number of columns in terminal buffer: String, tab_stop: usize, colors_enabled: bool, bell_style: BellStyle, } impl PosixRenderer { fn new( out: OutputStreamType, tab_stop: usize, colors_enabled: bool, bell_style: BellStyle, ) -> Self { let (cols, _) = get_win_size(&out); Self { out, cols, buffer: String::with_capacity(1024), tab_stop, colors_enabled, bell_style, } } } impl Renderer for PosixRenderer { type Reader = PosixRawReader; fn move_cursor(&mut self, old: Position, new: Position) -> Result<()> { use std::fmt::Write; self.buffer.clear(); let row_ordering = new.row.cmp(&old.row); if row_ordering == Ordering::Greater { // move down let row_shift = new.row - old.row; if row_shift == 1 { self.buffer.push_str("\x1b[B"); } else { write!(self.buffer, "\x1b[{}B", row_shift).unwrap(); } } else if row_ordering == Ordering::Less { // move up let row_shift = old.row - new.row; if row_shift == 1 { self.buffer.push_str("\x1b[A"); } else { write!(self.buffer, "\x1b[{}A", row_shift).unwrap(); } } let col_ordering = new.col.cmp(&old.col); if col_ordering == Ordering::Greater { // move right let col_shift = new.col - old.col; if col_shift == 1 { self.buffer.push_str("\x1b[C"); } else { write!(self.buffer, "\x1b[{}C", col_shift).unwrap(); } } else if col_ordering == Ordering::Less { // move left let col_shift = old.col - new.col; if col_shift == 1 { self.buffer.push_str("\x1b[D"); } else { write!(self.buffer, "\x1b[{}D", col_shift).unwrap(); } } self.write_and_flush(self.buffer.as_bytes()) } fn refresh_line( &mut self, prompt: &str, line: &LineBuffer, hint: Option<&str>, old_layout: &Layout, new_layout: &Layout, highlighter: Option<&dyn Highlighter>, ) -> Result<()> { 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"); 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); } // display hint if let Some(hint) = hint { if let Some(highlighter) = highlighter { self.buffer.push_str(&highlighter.highlight_hint(hint)); } else { self.buffer.push_str(hint); } } // we have to generate our own newline on line wrap if end_pos.col == 0 && end_pos.row > 0 && !self.buffer.ends_with('\n') { self.buffer.push_str("\n"); } // position the cursor let new_cursor_row_movement = end_pos.row - cursor.row; // move the cursor up as required if new_cursor_row_movement > 0 { write!(self.buffer, "\x1b[{}A", new_cursor_row_movement).unwrap(); } // position the cursor within the line if cursor.col > 0 { write!(self.buffer, "\r\x1b[{}C", cursor.col).unwrap(); } else { self.buffer.push('\r'); } self.write_and_flush(self.buffer.as_bytes())?; Ok(()) } fn write_and_flush(&self, buf: &[u8]) -> Result<()> { write_and_flush(self.out, buf) } /// 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 { let mut pos = orig; let mut esc_seq = 0; for c in s.graphemes(true) { if c == "\n" { pos.row += 1; pos.col = 0; continue; } let cw = if c == "\t" { self.tab_stop - (pos.col % self.tab_stop) } else { width(c, &mut esc_seq) }; pos.col += cw; if pos.col > self.cols { pos.row += 1; pos.col = cw; } } if pos.col == self.cols { pos.col = 0; pos.row += 1; } pos } fn beep(&mut self) -> Result<()> { match self.bell_style { BellStyle::Audible => { io::stderr().write_all(b"\x07")?; io::stderr().flush()?; Ok(()) } _ => Ok(()), } } /// Clear the screen. Used to handle ctrl+l fn clear_screen(&mut self) -> Result<()> { self.write_and_flush(b"\x1b[H\x1b[2J") } /// Check if a SIGWINCH signal has been received fn sigwinch(&self) -> bool { SIGWINCH.compare_and_swap(true, false, atomic::Ordering::SeqCst) } /// Try to update the number of columns in the current terminal, fn update_size(&mut self) { let (cols, _) = get_win_size(&self.out); self.cols = cols; } fn get_columns(&self) -> usize { self.cols } /// Try to get the number of rows in the current terminal, /// or assume 24 if it fails. fn get_rows(&self) -> usize { let (_, rows) = get_win_size(&self.out); rows } fn colors_enabled(&self) -> bool { self.colors_enabled } fn move_cursor_at_leftmost(&mut self, rdr: &mut PosixRawReader) -> Result<()> { if rdr.poll(0)? != 0 { debug!(target: "rustyline", "cannot request cursor location"); return Ok(()); } /* Report cursor location */ self.write_and_flush(b"\x1b[6n")?; /* Read the response: ESC [ rows ; cols R */ if rdr.poll(100)? == 0 || rdr.next_char()? != '\x1b' || rdr.next_char()? != '[' || read_digits_until(rdr, ';')?.is_none() { warn!(target: "rustyline", "cannot read initial cursor location"); return Ok(()); } let col = read_digits_until(rdr, 'R')?; debug!(target: "rustyline", "initial cursor location: {:?}", col); if col.is_some() && 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 { match rdr.next_char()? { digit @ '0'..='9' => { num = num .saturating_mul(10) .saturating_add(digit.to_digit(10).unwrap()); continue; } c if c == sep => break, _ => return Ok(None), } } Ok(Some(num)) } static SIGWINCH_ONCE: sync::Once = sync::Once::new(); static SIGWINCH: atomic::AtomicBool = atomic::AtomicBool::new(false); fn install_sigwinch_handler() { SIGWINCH_ONCE.call_once(|| unsafe { let sigwinch = signal::SigAction::new( signal::SigHandler::Handler(sigwinch_handler), signal::SaFlags::empty(), signal::SigSet::empty(), ); let _ = signal::sigaction(signal::SIGWINCH, &sigwinch); }); } extern "C" fn sigwinch_handler(_: libc::c_int) { SIGWINCH.store(true, atomic::Ordering::SeqCst); debug!(target: "rustyline", "SIGWINCH"); } #[cfg(not(test))] pub type Terminal = PosixTerminal; #[derive(Clone, Debug)] pub struct PosixTerminal { unsupported: bool, stdin_isatty: bool, stdstream_isatty: bool, pub(crate) color_mode: ColorMode, stream_type: OutputStreamType, tab_stop: usize, bell_style: BellStyle, } impl PosixTerminal { fn colors_enabled(&self) -> bool { match self.color_mode { ColorMode::Enabled => self.stdstream_isatty, ColorMode::Forced => true, ColorMode::Disabled => false, } } } impl Term for PosixTerminal { type Mode = PosixMode; type Reader = PosixRawReader; type Writer = PosixRenderer; fn new( color_mode: ColorMode, stream_type: OutputStreamType, tab_stop: usize, bell_style: BellStyle, ) -> Self { let term = Self { unsupported: is_unsupported_term(), stdin_isatty: is_a_tty(STDIN_FILENO), stdstream_isatty: is_a_tty(stream_type.as_raw_fd()), color_mode, stream_type, tab_stop, bell_style, }; if !term.unsupported && term.stdin_isatty && term.stdstream_isatty { install_sigwinch_handler(); } term } // Init checks: /// Check if current terminal can provide a rich line-editing user /// interface. fn is_unsupported(&self) -> bool { self.unsupported } /// check if stdin is connected to a terminal. fn is_stdin_tty(&self) -> bool { self.stdin_isatty } fn is_output_tty(&self) -> bool { self.stdstream_isatty } // Interactive loop: fn enable_raw_mode(&mut self) -> Result { use nix::errno::Errno::ENOTTY; use nix::sys::termios::{ControlFlags, InputFlags, LocalFlags, SpecialCharacterIndices}; if !self.stdin_isatty { return Err(nix::Error::from_errno(ENOTTY).into()); } let original_mode = termios::tcgetattr(STDIN_FILENO)?; let mut raw = original_mode.clone(); // disable BREAK interrupt, CR to NL conversion on input, // input parity check, strip high bit (bit 8), output flow control raw.input_flags &= !(InputFlags::BRKINT | InputFlags::ICRNL | InputFlags::INPCK | InputFlags::ISTRIP | InputFlags::IXON); // we don't want raw output, it turns newlines into straight line feeds // disable all output processing // raw.c_oflag = raw.c_oflag & !(OutputFlags::OPOST); // character-size mark (8 bits) raw.control_flags |= ControlFlags::CS8; // 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 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) { debug!(target: "rustyline", "Cannot enable bracketed paste: {}", e); None } else { Some(self.stream_type) }; Ok(PosixMode { termios: original_mode, out, }) } /// Create a RAW reader fn create_reader(&self, config: &Config) -> Result { PosixRawReader::new(config) } fn create_writer(&self) -> PosixRenderer { PosixRenderer::new( self.stream_type, self.tab_stop, self.colors_enabled(), self.bell_style, ) } } #[cfg(not(test))] pub fn suspend() -> Result<()> { use nix::unistd::Pid; // suspend the whole process group signal::kill(Pid::from_raw(0), signal::SIGTSTP)?; Ok(()) } fn write_and_flush(out: OutputStreamType, buf: &[u8]) -> Result<()> { match out { OutputStreamType::Stdout => { io::stdout().write_all(buf)?; io::stdout().flush()?; } OutputStreamType::Stderr => { io::stderr().write_all(buf)?; io::stderr().flush()?; } } Ok(()) } #[cfg(test)] mod test { use super::{Position, PosixRenderer, PosixTerminal, Renderer}; use crate::config::{BellStyle, OutputStreamType}; #[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()); assert_eq!(3, pos.col); assert_eq!(0, pos.row); } #[test] fn test_unsupported_term() { ::std::env::set_var("TERM", "xterm"); assert_eq!(false, super::is_unsupported_term()); ::std::env::set_var("TERM", "dumb"); assert_eq!(true, super::is_unsupported_term()); } #[test] fn test_send() { fn assert_send() {} assert_send::(); } #[test] fn test_sync() { fn assert_sync() {} assert_sync::(); } } rustyline-6.0.0/src/tty/windows.rs010064400007650000024000000520361353737232000154540ustar0000000000000000//! Windows specific definitions 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 winapi::um::winnt::{CHAR, HANDLE}; use winapi::um::{consoleapi, handleapi, processenv, winbase, wincon, winuser}; use super::{RawMode, RawReader, Renderer, Term}; use crate::config::{BellStyle, ColorMode, Config, OutputStreamType}; use crate::error; use crate::highlight::Highlighter; use crate::keys::{self, KeyPress}; use crate::layout::{Layout, Position}; use crate::line_buffer::LineBuffer; use crate::Result; const STDIN_FILENO: DWORD = winbase::STD_INPUT_HANDLE; const STDOUT_FILENO: DWORD = winbase::STD_OUTPUT_HANDLE; 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 { Err(io::Error::last_os_error())?; } else if handle.is_null() { Err(io::Error::new( io::ErrorKind::Other, "no stdio handle available for this process", ))?; } Ok(handle) } #[macro_export] macro_rules! check { ($funcall:expr) => {{ let rc = unsafe { $funcall }; if rc == 0 { Err(io::Error::last_os_error())?; } rc }}; } fn get_win_size(handle: HANDLE) -> (usize, usize) { let mut info = unsafe { mem::zeroed() }; match unsafe { wincon::GetConsoleScreenBufferInfo(handle, &mut info) } { 0 => (80, 24), _ => ( info.dwSize.X as usize, (1 + info.srWindow.Bottom - info.srWindow.Top) as usize, ), // (info.srWindow.Right - info.srWindow.Left + 1) } } fn get_console_mode(handle: HANDLE) -> Result { let mut original_mode = 0; check!(consoleapi::GetConsoleMode(handle, &mut original_mode)); Ok(original_mode) } #[cfg(not(test))] pub type Mode = ConsoleMode; #[derive(Clone, Copy, Debug)] pub struct ConsoleMode { original_stdin_mode: DWORD, stdin_handle: HANDLE, original_stdstream_mode: Option, stdstream_handle: HANDLE, } 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, )); if let Some(original_stdstream_mode) = self.original_stdstream_mode { check!(consoleapi::SetConsoleMode( self.stdstream_handle, original_stdstream_mode, )); } Ok(()) } } /// Console input reader pub struct ConsoleRawReader { handle: HANDLE, } impl ConsoleRawReader { pub fn create() -> Result { let handle = get_std_handle(STDIN_FILENO)?; Ok(ConsoleRawReader { handle }) } } impl RawReader for ConsoleRawReader { 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, SHIFT_PRESSED, }; let mut rec: wincon::INPUT_RECORD = unsafe { mem::zeroed() }; let mut count = 0; let mut surrogate = 0; loop { // TODO GetNumberOfConsoleInputEvents check!(consoleapi::ReadConsoleInputW( self.handle, &mut rec, 1 as DWORD, &mut count, )); if rec.EventType == wincon::WINDOW_BUFFER_SIZE_EVENT { SIGWINCH.store(true, atomic::Ordering::SeqCst); debug!(target: "rustyline", "SIGWINCH"); return Err(error::ReadlineError::WindowResize); // sigwinch + // err => err // ignored } else if rec.EventType != wincon::KEY_EVENT { continue; } let key_event = unsafe { rec.Event.KeyEvent() }; // writeln!(io::stderr(), "key_event: {:?}", key_event).unwrap(); if key_event.bKeyDown == 0 && key_event.wVirtualKeyCode != winuser::VK_MENU as WORD { continue; } // key_event.wRepeatCount seems to be always set to 1 (maybe because we only // read one character at a time) let alt_gr = key_event.dwControlKeyState & (LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED) == (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 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 }); } 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 }); } winuser::VK_DELETE => return Ok(KeyPress::Delete), winuser::VK_HOME => return Ok(KeyPress::Home), winuser::VK_END => return Ok(KeyPress::End), winuser::VK_PRIOR => return Ok(KeyPress::PageUp), winuser::VK_NEXT => return Ok(KeyPress::PageDown), winuser::VK_INSERT => return Ok(KeyPress::Insert), winuser::VK_F1 => return Ok(KeyPress::F(1)), winuser::VK_F2 => return Ok(KeyPress::F(2)), winuser::VK_F3 => return Ok(KeyPress::F(3)), winuser::VK_F4 => return Ok(KeyPress::F(4)), winuser::VK_F5 => return Ok(KeyPress::F(5)), winuser::VK_F6 => return Ok(KeyPress::F(6)), winuser::VK_F7 => return Ok(KeyPress::F(7)), winuser::VK_F8 => return Ok(KeyPress::F(8)), winuser::VK_F9 => return Ok(KeyPress::F(9)), winuser::VK_F10 => return Ok(KeyPress::F(10)), winuser::VK_F11 => return Ok(KeyPress::F(11)), winuser::VK_F12 => return Ok(KeyPress::F(12)), // winuser::VK_BACK is correctly handled because the key_event.UnicodeChar is // also set. _ => continue, }; } else if utf16 == 27 { return Ok(KeyPress::Esc); } else { if utf16 >= 0xD800 && utf16 < 0xDC00 { surrogate = utf16; continue; } let orc = if surrogate == 0 { decode_utf16(Some(utf16)).next() } else { decode_utf16([surrogate, utf16].iter().cloned()).next() }; let rc = if let Some(rc) = orc { rc } else { 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); } } } } fn read_pasted_text(&mut self) -> Result { unimplemented!() } } pub struct ConsoleRenderer { out: OutputStreamType, handle: HANDLE, cols: usize, // Number of columns in terminal buffer: String, colors_enabled: bool, bell_style: BellStyle, } impl ConsoleRenderer { fn new( handle: HANDLE, out: OutputStreamType, colors_enabled: bool, bell_style: BellStyle, ) -> ConsoleRenderer { // Multi line editing is enabled by ENABLE_WRAP_AT_EOL_OUTPUT mode let (cols, _) = get_win_size(handle); ConsoleRenderer { out, handle, cols, buffer: String::with_capacity(1024), colors_enabled, bell_style, } } fn get_console_screen_buffer_info(&self) -> Result { let mut info = unsafe { mem::zeroed() }; check!(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 clear(&mut self, length: DWORD, pos: wincon::COORD) -> Result<()> { let mut _count = 0; check!(wincon::FillConsoleOutputCharacterA( self.handle, ' ' as CHAR, length, pos, &mut _count, )); Ok(()) } } impl Renderer for ConsoleRenderer { type Reader = ConsoleRawReader; fn move_cursor(&mut self, old: Position, new: Position) -> Result<()> { let mut cursor = self.get_console_screen_buffer_info()?.dwCursorPosition; if new.row > old.row { cursor.Y += (new.row - old.row) as i16; } else { cursor.Y -= (old.row - new.row) as i16; } if new.col > old.col { cursor.X += (new.col - old.col) as i16; } else { cursor.X -= (old.col - new.col) as i16; } self.set_console_cursor_position(cursor) } fn refresh_line( &mut self, prompt: &str, 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); } // append hint if let Some(hint) = hint { if let Some(highlighter) = highlighter { self.buffer.push_str(&highlighter.highlight_hint(hint)); } 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)?; // display prompt, input line and hint self.write_and_flush(self.buffer.as_bytes())?; // position the cursor let mut coord = self.get_console_screen_buffer_info()?.dwCursorPosition; coord.X = cursor.col as i16; coord.Y -= (end_pos.row - cursor.row) as i16; self.set_console_cursor_position(coord)?; Ok(()) } fn write_and_flush(&self, buf: &[u8]) -> Result<()> { match self.out { OutputStreamType::Stdout => { io::stdout().write_all(buf)?; io::stdout().flush()?; } OutputStreamType::Stderr => { io::stderr().write_all(buf)?; io::stderr().flush()?; } } Ok(()) } /// Characters with 2 column width are correctly handled (not split). fn calculate_position(&self, s: &str, orig: Position) -> Position { let mut pos = orig; for c in s.chars() { let cw = if c == '\n' { pos.col = 0; pos.row += 1; None } else { c.width() }; if let Some(cw) = cw { pos.col += cw; if pos.col > self.cols { pos.row += 1; pos.col = cw; } } } if pos.col == self.cols { pos.col = 0; pos.row += 1; } pos } fn beep(&mut self) -> Result<()> { match self.bell_style { BellStyle::Audible => { io::stderr().write_all(b"\x07")?; io::stderr().flush()?; Ok(()) } _ => Ok(()), } } /// 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 n = info.dwSize.X as DWORD * info.dwSize.Y as DWORD; self.clear(n, coord) } fn sigwinch(&self) -> bool { SIGWINCH.compare_and_swap(true, false, atomic::Ordering::SeqCst) } /// Try to get the number of columns in the current terminal, /// or assume 80 if it fails. fn update_size(&mut self) { let (cols, _) = get_win_size(self.handle); self.cols = cols; } fn get_columns(&self) -> usize { self.cols } /// Try to get the number of rows in the current terminal, /// or assume 24 if it fails. fn get_rows(&self) -> usize { let (_, rows) = get_win_size(self.handle); rows } fn colors_enabled(&self) -> bool { self.colors_enabled } fn move_cursor_at_leftmost(&mut self, _: &mut ConsoleRawReader) -> Result<()> { self.write_and_flush(b"")?; // we must do this otherwise the cursor position is not reported correctly let mut info = self.get_console_screen_buffer_info()?; if info.dwCursorPosition.X == 0 { return Ok(()); } 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) } } static SIGWINCH: atomic::AtomicBool = atomic::AtomicBool::new(false); #[cfg(not(test))] pub type Terminal = Console; #[derive(Clone, Debug)] pub struct Console { stdin_isatty: bool, stdin_handle: HANDLE, stdstream_isatty: bool, stdstream_handle: HANDLE, pub(crate) color_mode: ColorMode, ansi_colors_supported: bool, stream_type: OutputStreamType, bell_style: BellStyle, } impl Console { fn colors_enabled(&self) -> bool { // TODO ANSI Colors & Windows <10 match self.color_mode { ColorMode::Enabled => self.stdstream_isatty && self.ansi_colors_supported, ColorMode::Forced => true, ColorMode::Disabled => false, } } } impl Term for Console { type Mode = ConsoleMode; type Reader = ConsoleRawReader; type Writer = ConsoleRenderer; fn new( color_mode: ColorMode, stream_type: OutputStreamType, _tab_stop: usize, bell_style: BellStyle, ) -> Console { use std::ptr; let stdin_handle = get_std_handle(STDIN_FILENO); let stdin_isatty = match stdin_handle { Ok(handle) => { // If this function doesn't fail then fd is a TTY get_console_mode(handle).is_ok() } Err(_) => false, }; let stdstream_handle = get_std_handle(if stream_type == OutputStreamType::Stdout { STDOUT_FILENO } else { STDERR_FILENO }); let stdstream_isatty = match stdstream_handle { Ok(handle) => { // If this function doesn't fail then fd is a TTY get_console_mode(handle).is_ok() } Err(_) => false, }; Console { stdin_isatty, stdin_handle: stdin_handle.unwrap_or(ptr::null_mut()), stdstream_isatty, stdstream_handle: stdstream_handle.unwrap_or(ptr::null_mut()), color_mode, ansi_colors_supported: false, stream_type, bell_style, } } /// Checking for an unsupported TERM in windows is a no-op fn is_unsupported(&self) -> bool { false } fn is_stdin_tty(&self) -> bool { self.stdin_isatty } fn is_output_tty(&self) -> bool { self.stdstream_isatty } // pub fn install_sigwinch_handler(&mut self) { // See ReadConsoleInputW && WINDOW_BUFFER_SIZE_EVENT // } /// Enable RAW mode for the terminal. fn enable_raw_mode(&mut self) -> Result { if !self.stdin_isatty { Err(io::Error::new( io::ErrorKind::Other, "no stdio handle available for this process", ))?; } let original_stdin_mode = get_console_mode(self.stdin_handle)?; // Disable these modes let mut raw = original_stdin_mode & !(wincon::ENABLE_LINE_INPUT | wincon::ENABLE_ECHO_INPUT | wincon::ENABLE_PROCESSED_INPUT); // Enable these modes raw |= wincon::ENABLE_EXTENDED_FLAGS; raw |= wincon::ENABLE_INSERT_MODE; raw |= wincon::ENABLE_QUICK_EDIT_MODE; raw |= wincon::ENABLE_WINDOW_INPUT; check!(consoleapi::SetConsoleMode(self.stdin_handle, raw)); let original_stdstream_mode = if self.stdstream_isatty { let original_stdstream_mode = get_console_mode(self.stdstream_handle)?; // 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 = unsafe { consoleapi::SetConsoleMode(self.stdstream_handle, raw) != 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, }) } fn create_reader(&self, _: &Config) -> Result { ConsoleRawReader::create() } fn create_writer(&self) -> ConsoleRenderer { ConsoleRenderer::new( self.stdstream_handle, self.stream_type, self.colors_enabled(), self.bell_style, ) } } unsafe impl Send for Console {} unsafe impl Sync for Console {} #[cfg(test)] mod test { use super::Console; #[test] fn test_send() { fn assert_send() {} assert_send::(); } #[test] fn test_sync() { fn assert_sync() {} assert_sync::(); } } rustyline-6.0.0/src/undo.rs010064400007650000024000000325421352702755400141140ustar0000000000000000//! Undo API use std::fmt::Debug; use crate::keymap::RepeatCount; use crate::line_buffer::{ChangeListener, DeleteListener, Direction, LineBuffer}; use log::debug; use unicode_segmentation::UnicodeSegmentation; enum Change { Begin, End, Insert { idx: usize, text: String, }, // QuotedInsert, SelfInsert, Yank Delete { idx: usize, text: String, }, /* BackwardDeleteChar, BackwardKillWord, DeleteChar, * KillLine, KillWholeLine, KillWord, * UnixLikeDiscard, ViDeleteTo */ Replace { idx: usize, old: String, new: String, }, /* CapitalizeWord, Complete, DowncaseWord, Replace, TransposeChars, TransposeWords, * UpcaseWord, YankPop */ } impl Change { fn undo(&self, line: &mut LineBuffer) { match *self { Change::Begin | Change::End => { unreachable!(); } Change::Insert { idx, ref text } => { line.delete_range(idx..idx + text.len()); } Change::Delete { idx, ref text } => { line.insert_str(idx, text); line.set_pos(idx + text.len()); } Change::Replace { idx, ref old, ref new, } => { line.replace(idx..idx + new.len(), old); } } } #[cfg(test)] fn redo(&self, line: &mut LineBuffer) { match *self { Change::Begin | Change::End => { unreachable!(); } Change::Insert { idx, ref text } => { line.insert_str(idx, text); } Change::Delete { idx, ref text } => { line.delete_range(idx..idx + text.len()); } Change::Replace { idx, ref old, ref new, } => { line.replace(idx..idx + old.len(), new); } } } fn insert_seq(&self, indx: usize) -> bool { if let Change::Insert { idx, ref text } = *self { idx + text.len() == indx } else { false } } fn delete_seq(&self, indx: usize, len: usize) -> bool { if let Change::Delete { idx, .. } = *self { // delete or backspace idx == indx || idx == indx + len } else { false } } fn replace_seq(&self, indx: usize) -> bool { if let Change::Replace { idx, ref new, .. } = *self { idx + new.len() == indx } else { false } } } pub struct Changeset { undo_group_level: u32, undos: Vec, // undoable changes redos: Vec, // undone changes, redoable } impl Changeset { pub fn new() -> Self { Self { undo_group_level: 0, undos: Vec::new(), redos: Vec::new(), } } pub fn begin(&mut self) -> usize { debug!(target: "rustyline", "Changeset::begin"); self.redos.clear(); let mark = self.undos.len(); self.undos.push(Change::Begin); self.undo_group_level += 1; mark } /// Returns `true` when changes happen between the last call to `begin` and /// this `end`. pub fn end(&mut self) -> bool { debug!(target: "rustyline", "Changeset::end"); self.redos.clear(); let mut touched = false; while self.undo_group_level > 0 { self.undo_group_level -= 1; if let Some(&Change::Begin) = self.undos.last() { // empty Begin..End self.undos.pop(); } else { self.undos.push(Change::End); touched = true; } } touched } fn insert_char(idx: usize, c: char) -> Change { let mut text = String::new(); text.push(c); Change::Insert { idx, text } } pub fn insert(&mut self, idx: usize, c: char) { debug!(target: "rustyline", "Changeset::insert({}, {:?})", idx, c); self.redos.clear(); if !c.is_alphanumeric() || !self.undos.last().map_or(false, |lc| lc.insert_seq(idx)) { self.undos.push(Self::insert_char(idx, c)); return; } // merge consecutive char insertions when char is alphanumeric let mut last_change = self.undos.pop().unwrap(); if let Change::Insert { ref mut text, .. } = last_change { text.push(c); } else { unreachable!(); } self.undos.push(last_change); } pub fn insert_str + Into + Debug>(&mut self, idx: usize, string: S) { debug!(target: "rustyline", "Changeset::insert_str({}, {:?})", idx, string); self.redos.clear(); if string.as_ref().is_empty() { return; } self.undos.push(Change::Insert { idx, text: string.into(), }); } pub fn delete + Into + Debug>(&mut self, indx: usize, string: S) { debug!(target: "rustyline", "Changeset::delete({}, {:?})", indx, string); self.redos.clear(); if string.as_ref().is_empty() { return; } if !Self::single_char(string.as_ref()) || !self .undos .last() .map_or(false, |lc| lc.delete_seq(indx, string.as_ref().len())) { self.undos.push(Change::Delete { idx: indx, text: string.into(), }); return; } // merge consecutive char deletions when char is alphanumeric let mut last_change = self.undos.pop().unwrap(); if let Change::Delete { ref mut idx, ref mut text, } = last_change { if *idx == indx { text.push_str(string.as_ref()); } else { text.insert_str(0, string.as_ref()); *idx = indx; } } else { unreachable!(); } self.undos.push(last_change); } fn single_char(s: &str) -> bool { let mut graphemes = s.graphemes(true); graphemes.next().map_or(false, |grapheme| { grapheme.chars().all(char::is_alphanumeric) }) && graphemes.next().is_none() } pub fn replace + Into + Debug>(&mut self, indx: usize, old_: S, new_: S) { debug!(target: "rustyline", "Changeset::replace({}, {:?}, {:?})", indx, old_, new_); self.redos.clear(); if !self.undos.last().map_or(false, |lc| lc.replace_seq(indx)) { self.undos.push(Change::Replace { idx: indx, old: old_.into(), new: new_.into(), }); return; } // merge consecutive char replacements let mut last_change = self.undos.pop().unwrap(); if let Change::Replace { ref mut old, ref mut new, .. } = last_change { old.push_str(old_.as_ref()); new.push_str(new_.as_ref()); } else { unreachable!(); } self.undos.push(last_change); } pub fn undo(&mut self, line: &mut LineBuffer, n: RepeatCount) -> bool { debug!(target: "rustyline", "Changeset::undo"); let mut count = 0; let mut waiting_for_begin = 0; let mut undone = false; loop { if let Some(change) = self.undos.pop() { match change { Change::Begin => { waiting_for_begin -= 1; } Change::End => { waiting_for_begin += 1; } _ => { change.undo(line); undone = true; } }; self.redos.push(change); } else { break; } if waiting_for_begin <= 0 { count += 1; if count >= n { break; } } } undone } pub fn truncate(&mut self, len: usize) { debug!(target: "rustyline", "Changeset::truncate({})", len); self.undos.truncate(len); } #[cfg(test)] pub fn redo(&mut self, line: &mut LineBuffer) -> bool { let mut waiting_for_end = 0; let mut redone = false; loop { if let Some(change) = self.redos.pop() { match change { Change::Begin => { waiting_for_end += 1; } Change::End => { waiting_for_end -= 1; } _ => { change.redo(line); redone = true; } }; self.undos.push(change); } else { break; } if waiting_for_end <= 0 { break; } } redone } pub fn last_insert(&self) -> Option { for change in self.undos.iter().rev() { match change { Change::Insert { ref text, .. } => return Some(text.to_owned()), Change::Replace { ref new, .. } => return Some(new.to_owned()), Change::End => { continue; } _ => { return None; } } } None } } impl DeleteListener for Changeset { fn start_killing(&mut self) {} fn delete(&mut self, idx: usize, string: &str, _: Direction) { self.delete(idx, string); } fn stop_killing(&mut self) {} } impl ChangeListener for Changeset { fn insert_char(&mut self, idx: usize, c: char) { self.insert(idx, c); } fn insert_str(&mut self, idx: usize, string: &str) { self.insert_str(idx, string); } fn replace(&mut self, idx: usize, old: &str, new: &str) { self.replace(idx, old, new); } } #[cfg(test)] mod tests { use super::Changeset; use crate::line_buffer::LineBuffer; #[test] fn test_insert_chars() { let mut cs = Changeset::new(); cs.insert(0, 'H'); cs.insert(1, 'i'); assert_eq!(1, cs.undos.len()); assert_eq!(0, cs.redos.len()); cs.insert(0, ' '); assert_eq!(2, cs.undos.len()); } #[test] fn test_insert_strings() { let mut cs = Changeset::new(); cs.insert_str(0, "Hello"); cs.insert_str(5, ", "); assert_eq!(2, cs.undos.len()); assert_eq!(0, cs.redos.len()); } #[test] fn test_undo_insert() { let mut buf = LineBuffer::init("", 0, None); buf.insert_str(0, "Hello"); buf.insert_str(5, ", world!"); let mut cs = Changeset::new(); assert_eq!(buf.as_str(), "Hello, world!"); cs.insert_str(5, ", world!"); cs.undo(&mut buf, 1); assert_eq!(0, cs.undos.len()); assert_eq!(1, cs.redos.len()); assert_eq!(buf.as_str(), "Hello"); cs.redo(&mut buf); assert_eq!(1, cs.undos.len()); assert_eq!(0, cs.redos.len()); assert_eq!(buf.as_str(), "Hello, world!"); } #[test] fn test_undo_delete() { let mut buf = LineBuffer::init("", 0, None); buf.insert_str(0, "Hello"); let mut cs = Changeset::new(); assert_eq!(buf.as_str(), "Hello"); cs.delete(5, ", world!"); cs.undo(&mut buf, 1); assert_eq!(buf.as_str(), "Hello, world!"); cs.redo(&mut buf); assert_eq!(buf.as_str(), "Hello"); } #[test] fn test_delete_chars() { let mut buf = LineBuffer::init("", 0, None); buf.insert_str(0, "Hlo"); let mut cs = Changeset::new(); cs.delete(1, "e"); cs.delete(1, "l"); assert_eq!(1, cs.undos.len()); cs.undo(&mut buf, 1); assert_eq!(buf.as_str(), "Hello"); } #[test] fn test_backspace_chars() { let mut buf = LineBuffer::init("", 0, None); buf.insert_str(0, "Hlo"); let mut cs = Changeset::new(); cs.delete(2, "l"); cs.delete(1, "e"); assert_eq!(1, cs.undos.len()); cs.undo(&mut buf, 1); assert_eq!(buf.as_str(), "Hello"); } #[test] fn test_undo_replace() { let mut buf = LineBuffer::init("", 0, None); buf.insert_str(0, "Hello, world!"); let mut cs = Changeset::new(); assert_eq!(buf.as_str(), "Hello, world!"); buf.replace(1..5, "i"); assert_eq!(buf.as_str(), "Hi, world!"); cs.replace(1, "ello", "i"); cs.undo(&mut buf, 1); assert_eq!(buf.as_str(), "Hello, world!"); cs.redo(&mut buf); assert_eq!(buf.as_str(), "Hi, world!"); } #[test] fn test_last_insert() { let mut cs = Changeset::new(); cs.begin(); cs.delete(0, "Hello"); cs.insert_str(0, "Bye"); cs.end(); let insert = cs.last_insert(); assert_eq!(Some("Bye".to_owned()), insert); } #[test] fn test_end() { let mut cs = Changeset::new(); cs.begin(); assert!(!cs.end()); cs.begin(); cs.insert_str(0, "Hi"); assert!(cs.end()); } } rustyline-6.0.0/src/validate.rs010064400007650000024000000047331360441142000147220ustar0000000000000000//! 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), } 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 } } 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`. // TODO we can implement this later. 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() } } rustyline-6.0.0/TODO.md010064400007650000024000000055171353737232000130760ustar0000000000000000API - [ ] expose an API callable from C Async (#126) Bell - [X] bell-style Color - [X] ANSI Colors & Windows 10+ - [ ] ANSI Colors & Windows <10 (https://docs.rs/console/0.6.1/console/fn.strip_ansi_codes.html ? https://github.com/mattn/go-colorable/blob/master/colorable_windows.go, https://github.com/mattn/ansicolor-w32.c) - [ ] Syntax highlighting (https://github.com/trishume/syntect/) - [ ] clicolors spec (https://docs.rs/console/0.6.1/console/fn.colors_enabled.html) Completion - [X] Quoted path - [ ] Windows escape/unescape space in path - [ ] file completion & escape/unescape (#106) - [ ] file completion & tilde (#62) - [X] display versus replacement - [ ] composite/alternate completer (if the current completer returns nothing, try the next one) Config - [ ] Maximum buffer size for the line read Cursor - [ ] insert versus overwrite versus command mode - [ ] In vi command mode, prevent user from going to end of line. (#94) Grapheme - [ ] grapheme & input auto-wrap are buggy Hints Callback - [X] Not implemented on windows - [X] Do an implementation based on previous history History - [ ] Move to the history line n - [ ] historyFile: Where to read/write the history at the start and end of each line input session. - [ ] append_history - [ ] history_truncate_file Input - [ ] Password input (#58) (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) Layout - [ ] Redraw perf (https://crates.io/crates/cassowary) Misc - [ ] fallible iterator (https://docs.rs/fallible-iterator/0.2.1/fallible_iterator/) Mouse - [ ] Mouse support Movement - [ ] Move to the corresponding opening/closing bracket Redo - [X] redo substitute Repeat - [X] dynamic prompt (arg: ?) - [ ] transpose chars Syntax - [ ] syntax specific tokenizer/parser - [ ] highlighting Undo - [ ] Merge consecutive Replace - [X] Undo group - [ ] Undo all changes made to this line. - [X] Kill+Insert (substitute/replace) - [X] Repeated undo `Undo(RepeatCount)` Unix - [ ] Terminfo (https://github.com/Stebalien/term) - [ ] [ncurses](https://crates.io/crates/ncurses) alternative backend ? - [X] [bracketed paste mode](https://cirw.in/blog/bracketed-paste) - [ ] async stdin (https://github.com/Rufflewind/tokio-file-unix) Windows - [ ] is_atty is not working with Cygwin/MSYS (https://github.com/softprops/atty works but then how to make `enable_raw_mode` works ?) (https://github.com/mitsuhiko/console/blob/master/src/windows_term.rs#L285) (https://github.com/mattn/go-isatty/blob/master/isatty_windows.go, https://github.com/mattn/go-tty/blob/master/tty_windows.go#L143) - [X] UTF-16 surrogate pair - [ ] handle ANSI escape code (#61) (https://github.com/DanielKeep/rust-ansi-interpreter) rustyline-6.0.0/.cargo_vcs_info.json0000644000000001121360441330600131650ustar00{ "git": { "sha1": "92f7dfb23a4888af1aa1a2e0aad3bb649eed93a9" } } rustyline-6.0.0/Cargo.lock0000644000000572221360441330600111560ustar00# This file is automatically @generated by Cargo. # It is not intended for manual editing. [[package]] name = "aho-corasick" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "arrayref" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "arrayvec" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "assert_matches" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "atty" version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "backtrace" version = "0.3.40" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "backtrace-sys 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "backtrace-sys" version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cc 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "base64" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "bitflags" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "blake2b_simd" version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "arrayref 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "arrayvec 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "constant_time_eq 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "byteorder" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "cc" version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "cfg-if" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "cloudabi" version = "0.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "constant_time_eq" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "crossbeam-utils" version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "dirs" version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "dirs-sys 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "dirs-sys" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", "redox_users 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "env_logger" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)", "humantime 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "termcolor 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "failure" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "backtrace 0.3.40 (registry+https://github.com/rust-lang/crates.io-index)", "failure_derive 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "failure_derive" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "syn 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", "synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "fuchsia-cprng" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "humantime" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "quick-error 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "libc" version = "0.2.66" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "log" version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "memchr" version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "nix" version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "cc 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)", "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "proc-macro2" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "quick-error" version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "quote" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "rand" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "rand_core" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "rand_core" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "rand_os" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", "fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "rdrand" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "redox_syscall" version = "0.1.56" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "redox_users" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", "rust-argon2 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "regex" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "aho-corasick 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)", "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "regex-syntax 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)", "thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "regex-syntax" version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "remove_dir_all" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "rust-argon2" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "blake2b_simd 0.5.10 (registry+https://github.com/rust-lang/crates.io-index)", "crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "rustc-demangle" version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "rustyline" version = "6.0.0" dependencies = [ "assert_matches 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "dirs 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "nix 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)", "rustyline-derive 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", "utf8parse 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "rustyline-derive" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "syn 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "syn" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "synstructure" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "syn 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "tempdir" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "rand 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "termcolor" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "wincolor 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "thread_local" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "unicode-segmentation" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "unicode-width" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "unicode-xid" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "utf8parse" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "void" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "winapi" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "winapi-util" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "wincolor" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", ] [metadata] "checksum aho-corasick 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)" = "58fb5e95d83b38284460a5fda7d6470aa0b8844d283a0b614b8535e880800d2d" "checksum arrayref 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "0d382e583f07208808f6b1249e60848879ba3543f57c32277bf52d69c2f0f0ee" "checksum arrayvec 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" "checksum assert_matches 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7deb0a829ca7bcfaf5da70b073a8d128619259a7be8216a355e23f00763059e5" "checksum atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "1803c647a3ec87095e7ae7acfca019e98de5ec9a7d01343f611cf3152ed71a90" "checksum backtrace 0.3.40 (registry+https://github.com/rust-lang/crates.io-index)" = "924c76597f0d9ca25d762c25a4d369d51267536465dc5064bdf0eb073ed477ea" "checksum backtrace-sys 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)" = "5d6575f128516de27e3ce99689419835fce9643a9b215a14d2b5b685be018491" "checksum base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" "checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" "checksum blake2b_simd 0.5.10 (registry+https://github.com/rust-lang/crates.io-index)" = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a" "checksum byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5" "checksum cc 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)" = "f52a465a666ca3d838ebbf08b241383421412fe7ebb463527bba275526d89f76" "checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" "checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" "checksum constant_time_eq 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "995a44c877f9212528ccc74b21a232f66ad69001e40ede5bcee2ac9ef2657120" "checksum crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)" = "04973fa96e96579258a5091af6003abde64af786b860f18622b82e026cca60e6" "checksum dirs 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" "checksum dirs-sys 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "afa0b23de8fd801745c471deffa6e12d248f962c9fd4b4c33787b055599bde7b" "checksum env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" "checksum failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "f8273f13c977665c5db7eb2b99ae520952fe5ac831ae4cd09d80c4c7042b5ed9" "checksum failure_derive 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0bc225b78e0391e4b8683440bf2e63c2deeeb2ce5189eab46e2b68c6d3725d08" "checksum fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" "checksum humantime 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" "checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" "checksum libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)" = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558" "checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" "checksum memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "88579771288728879b57485cc7d6b07d648c9f0141eb955f8ab7f9d45394468e" "checksum nix 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6c722bee1037d430d0f8e687bbdbf222f27cc6e4e68d5caf630857bb2b6dbdce" "checksum proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "0319972dcae462681daf4da1adeeaa066e3ebd29c69be96c6abb1259d2ee2bcc" "checksum quick-error 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" "checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" "checksum rand 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" "checksum rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" "checksum rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" "checksum rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" "checksum rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" "checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" "checksum redox_users 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4ecedbca3bf205f8d8f5c2b44d83cd0690e39ee84b951ed649e9f1841132b66d" "checksum regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dc220bd33bdce8f093101afe22a037b8eb0e5af33592e6a9caafff0d4cb81cbd" "checksum regex-syntax 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)" = "11a7e20d1cce64ef2fed88b66d347f88bd9babb82845b2b858f3edbf59a4f716" "checksum remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" "checksum rust-argon2 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4ca4eaef519b494d1f2848fc602d18816fed808a981aedf4f1f00ceb7c9d32cf" "checksum rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" "checksum rustyline-derive 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "69377056936308234fbb8f8789cada21b40b7796808a62783cdbe5c1315a0212" "checksum syn 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)" = "1e4ff033220a41d1a57d8125eab57bf5263783dfdcc18688b1dacc6ce9651ef8" "checksum synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545" "checksum tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" "checksum termcolor 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "96d6098003bde162e4277c70665bd87c326f5a0c3f3fbfb285787fa482d54e6e" "checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" "checksum unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" "checksum unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" "checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" "checksum utf8parse 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8772a4ccbb4e89959023bc5b7cb8623a795caa7092d99f3aa9501b9484d4557d" "checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" "checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" "checksum winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7168bab6e1daee33b4557efd0e95d5ca70a03706d39fa5f3fe7a236f584b03c9" "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" "checksum wincolor 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "96f5016b18804d24db43cebf3c77269e7569b8954a8464501c216cc5e070eaa9"