rustyline-13.0.0/.cargo_vcs_info.json0000644000000001360000000000100131530ustar { "git": { "sha1": "64e1082750a991b9fe18383a954f5eab0a29e039" }, "path_in_vcs": "" }rustyline-13.0.0/.gitignore000064400000000000000000000001441046102023000137320ustar 00000000000000# Generated by Cargo /target/ Cargo.lock # vim swap file *.swp # default history file history.txt rustyline-13.0.0/Ansi.md000064400000000000000000000041501046102023000131570ustar 00000000000000# Output | Seq | Terminfo | Comment | | --------- | -------------------------- | -------------------------------------------------------- | | \E[H | cursor_home, home, ho | | | \E[K | clr_eol, el, ce | | | \E[H\E[J | clear_screen, clear, cl | | | \E[6n | user7, u7, u7 | cursor position report | | ^M | carriage_return, cr, cr | move cursor to bol | | \E[B | cursor_down, cud1, do | ^J | | \E[%p1%dB | parm_down_cursor, cud, DO | | | \E[A | cursor_up, cuu1, up | | | \E[%p1%dA | parm_up_cursor, cuu, UP | | | \E[C | cursor_right, cuf1, nd | | | \E[%p1%dC | parm_right_cursor, cuf, RI | | | \E[D | cursor_left, cub1, le | ^H | | \E[%p1%dD | parm_left_cursor, cub, LE | | | ^G | bell, bel, bl | | | \E[?2004h | | bracketed paste on | | \E[?2004l | | bracketed paste off | | \E[?1000h | | X11 mouse reporting, reports on button press and release | | \E[?1015h | | Enable urxvt Mouse mode | | \E[?1006h | | Enable Xterm SGR mouse mode | rustyline-13.0.0/BUGS.md000064400000000000000000000036731046102023000130360ustar 00000000000000Know 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 / highlighting / validation become easy. So we need to send events to a lexer/parser, update `Document` accordingly. And fix `Completer` / `Hinter` / `Highlighter` API such as they have access to `Document`. 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`). See `replxx` refresh delay (`_lastRefreshTime`) or `python-prompt-toolkit` max_render_postpone_time. https://docs.rs/xi-unicode/0.3.0/xi_unicode/struct.LineBreakIterator.html https://github.com/xi-editor/xi-editor/blob/master/rust/core-lib/src/linewrap.rs [vt100](https://docs.rs/vt100/0.12.0/vt100/struct.Screen.html#method.contents_diff) ## Action / Command 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`). ## Line wrapping (should be fixed with versions >= 6.1.2) On Unix platform, we assume that `auto_right_margin` (`am`) is enabled. And on Windows, we activate `ENABLE_WRAP_AT_EOL_OUTPUT`. But on Windows 10, `ENABLE_WRAP_AT_EOL_OUTPUT` and `ENABLE_VIRTUAL_TERMINAL_PROCESSING` seems to be incompatible. ## Colors We assume that ANSI colors are supported. Which is not the case on Windows (except on Windows 10)! ## Emoji https://github.com/kkawakam/rustyline/issues/184 https://docs.rs/xi-unicode/0.3.0/xi_unicode/trait.EmojiExt.html https://docs.rs/termwiz/0.11.0/termwiz/cell/fn.grapheme_column_width.html rustyline-13.0.0/Cargo.lock0000644000001041670000000000100111370ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "ahash" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" dependencies = [ "cfg-if", "once_cell", "version_check", "zerocopy", ] [[package]] name = "aho-corasick" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" dependencies = [ "memchr", ] [[package]] name = "allocator-api2" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" [[package]] name = "android-tzdata" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "arrayvec" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] name = "assert_matches" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "beef" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" [[package]] name = "bumpalo" version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "cc" version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "libc", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", "windows-targets 0.48.5", ] [[package]] name = "clipboard-win" version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c57002a5d9be777c1ef967e33674dac9ebd310d8893e4e3437b14d5f0f6372cc" dependencies = [ "error-code", ] [[package]] name = "core-foundation-sys" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "crossbeam" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2801af0d36612ae591caa9568261fddce32ce6e08a7275ea334a06a4ad021a2c" dependencies = [ "cfg-if", "crossbeam-channel", "crossbeam-deque", "crossbeam-epoch", "crossbeam-queue", "crossbeam-utils", ] [[package]] name = "crossbeam-channel" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" dependencies = [ "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-deque" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" dependencies = [ "cfg-if", "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" dependencies = [ "autocfg", "cfg-if", "crossbeam-utils", "memoffset 0.9.0", "scopeguard", ] [[package]] name = "crossbeam-queue" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" dependencies = [ "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" dependencies = [ "cfg-if", ] [[package]] name = "darling" version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" dependencies = [ "darling_core", "darling_macro", ] [[package]] name = "darling_core" version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim", "syn 1.0.109", ] [[package]] name = "darling_macro" version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" dependencies = [ "darling_core", "quote", "syn 1.0.109", ] [[package]] name = "defer-drop" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f613ec9fa66a6b28cdb1842b27f9adf24f39f9afc4dcdd9fdecee4aca7945c57" dependencies = [ "crossbeam-channel", "once_cell", ] [[package]] name = "deranged" version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc" dependencies = [ "powerfmt", ] [[package]] name = "derive_builder" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d07adf7be193b71cc36b193d0f5fe60b918a3a9db4dad0449f57bcfd519704a3" dependencies = [ "derive_builder_macro", ] [[package]] name = "derive_builder_core" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f91d4cfa921f1c05904dc3c57b4a32c38aed3340cce209f3a6fd1478babafc4" dependencies = [ "darling", "proc-macro2", "quote", "syn 1.0.109", ] [[package]] name = "derive_builder_macro" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f0314b72bed045f3a68671b3c86328386762c93f82d98c65c3cb5e5f573dd68" dependencies = [ "derive_builder_core", "syn 1.0.109", ] [[package]] name = "dirs-next" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" dependencies = [ "cfg-if", "dirs-sys-next", ] [[package]] name = "dirs-sys-next" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", "redox_users", "winapi", ] [[package]] name = "doc-comment" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "either" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "endian-type" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" [[package]] name = "env_logger" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece" dependencies = [ "log", ] [[package]] name = "errno" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "error-code" version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "281e452d3bad4005426416cdba5ccfd4f5c1280e10099e21db27f7c1c28347fc" [[package]] name = "fallible-iterator" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" [[package]] name = "fallible-streaming-iterator" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fastrand" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "fd-lock" version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b93f7a0db71c99f68398f80653ed05afb0b00e062e1a20c7ff849c4edfabbbcc" dependencies = [ "cfg-if", "rustix", "windows-sys 0.52.0", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "fuzzy-matcher" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" dependencies = [ "thread_local", ] [[package]] name = "getrandom" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "hashbrown" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" dependencies = [ "ahash", "allocator-api2", ] [[package]] name = "hashlink" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ "hashbrown", ] [[package]] name = "home" version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" dependencies = [ "windows-sys 0.48.0", ] [[package]] name = "iana-time-zone" version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", "windows-core", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "js-sys" version = "0.3.66" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" dependencies = [ "wasm-bindgen", ] [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" [[package]] name = "libredox" version = "0.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" dependencies = [ "bitflags 2.4.1", "libc", "redox_syscall", ] [[package]] name = "libsqlite3-sys" version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" dependencies = [ "cc", "pkg-config", "vcpkg", ] [[package]] name = "linux-raw-sys" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" [[package]] name = "log" version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "memchr" version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" [[package]] name = "memoffset" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" dependencies = [ "autocfg", ] [[package]] name = "memoffset" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" dependencies = [ "autocfg", ] [[package]] name = "nibble_vec" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" dependencies = [ "smallvec", ] [[package]] name = "nix" version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" dependencies = [ "bitflags 1.3.2", "cfg-if", "libc", ] [[package]] name = "nix" version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" dependencies = [ "autocfg", "bitflags 1.3.2", "cfg-if", "libc", "memoffset 0.6.5", "pin-utils", ] [[package]] name = "nix" version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ "bitflags 2.4.1", "cfg-if", "libc", ] [[package]] name = "num-traits" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", ] [[package]] name = "once_cell" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "powerfmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] [[package]] name = "radix_trie" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" dependencies = [ "endian-type", "nibble_vec", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] [[package]] name = "rayon" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" dependencies = [ "either", "rayon-core", ] [[package]] name = "rayon-core" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" dependencies = [ "crossbeam-deque", "crossbeam-utils", ] [[package]] name = "redox_syscall" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "redox_users" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" dependencies = [ "getrandom", "libredox", "thiserror", ] [[package]] name = "regex" version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "rusqlite" version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a78046161564f5e7cd9008aff3b2990b3850dc8e0349119b98e8f251e099f24d" dependencies = [ "bitflags 2.4.1", "fallible-iterator", "fallible-streaming-iterator", "hashlink", "libsqlite3-sys", "smallvec", ] [[package]] name = "rustix" version = "0.38.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9470c4bf8246c8daf25f9598dca807fb6510347b1e1cfa55749113850c79d88a" dependencies = [ "bitflags 2.4.1", "errno", "libc", "linux-raw-sys", "windows-sys 0.52.0", ] [[package]] name = "rustversion" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "rustyline" version = "13.0.0" dependencies = [ "assert_matches", "bitflags 2.4.1", "cfg-if", "clipboard-win", "doc-comment", "env_logger", "fd-lock", "home", "libc", "log", "memchr", "nix 0.27.1", "radix_trie", "rand", "regex", "rusqlite", "rustyline-derive", "signal-hook", "skim", "tempfile", "termios", "unicode-segmentation", "unicode-width", "utf8parse", "winapi", ] [[package]] name = "rustyline-derive" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5af959c8bf6af1aff6d2b463a57f71aae53d1332da58419e30ad8dc7011d951" dependencies = [ "proc-macro2", "quote", "syn 2.0.39", ] [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", "syn 2.0.39", ] [[package]] name = "signal-hook" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" dependencies = [ "libc", "signal-hook-registry", ] [[package]] name = "signal-hook-registry" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" dependencies = [ "libc", ] [[package]] name = "skim" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5d28de0a6cb2cdd83a076f1de9d965b973ae08b244df1aa70b432946dda0f32" dependencies = [ "beef", "bitflags 1.3.2", "chrono", "crossbeam", "defer-drop", "derive_builder", "fuzzy-matcher", "lazy_static", "log", "nix 0.25.1", "rayon", "regex", "time", "timer", "tuikit", "unicode-width", "vte", ] [[package]] name = "smallvec" version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "syn" version = "2.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" dependencies = [ "cfg-if", "fastrand", "redox_syscall", "rustix", "windows-sys 0.48.0", ] [[package]] name = "term" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" dependencies = [ "dirs-next", "rustversion", "winapi", ] [[package]] name = "termios" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" dependencies = [ "libc", ] [[package]] name = "thiserror" version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", "syn 2.0.39", ] [[package]] name = "thread_local" version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" dependencies = [ "cfg-if", "once_cell", ] [[package]] name = "time" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" dependencies = [ "deranged", "powerfmt", "serde", "time-core", ] [[package]] name = "time-core" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "timer" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31d42176308937165701f50638db1c31586f183f1aab416268216577aec7306b" dependencies = [ "chrono", ] [[package]] name = "tuikit" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e19c6ab038babee3d50c8c12ff8b910bdb2196f62278776422f50390d8e53d8" dependencies = [ "bitflags 1.3.2", "lazy_static", "log", "nix 0.24.3", "term", "unicode-width", ] [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-segmentation" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" [[package]] name = "unicode-width" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "utf8parse" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "vte" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197" dependencies = [ "arrayvec", "utf8parse", "vte_generate_state_changes", ] [[package]] name = "vte_generate_state_changes" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" dependencies = [ "proc-macro2", "quote", ] [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" dependencies = [ "cfg-if", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", "syn 2.0.39", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", "syn 2.0.39", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" version = "0.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" dependencies = [ "windows-targets 0.48.5", ] [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets 0.48.5", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.0", ] [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm 0.48.5", "windows_aarch64_msvc 0.48.5", "windows_i686_gnu 0.48.5", "windows_i686_msvc 0.48.5", "windows_x86_64_gnu 0.48.5", "windows_x86_64_gnullvm 0.48.5", "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" dependencies = [ "windows_aarch64_gnullvm 0.52.0", "windows_aarch64_msvc 0.52.0", "windows_i686_gnu 0.52.0", "windows_i686_msvc 0.52.0", "windows_x86_64_gnu 0.52.0", "windows_x86_64_gnullvm 0.52.0", "windows_x86_64_msvc 0.52.0", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "zerocopy" version = "0.7.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d6f15f7ade05d2a4935e34a457b936c23dc70a05cc1d97133dc99e7a3fe0f0e" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.7.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbbad221e3f78500350ecbd7dfa4e63ef945c05f4c61cb7f4d3f84cd0bba649b" dependencies = [ "proc-macro2", "quote", "syn 2.0.39", ] rustyline-13.0.0/Cargo.toml0000644000000076120000000000100111570ustar # 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 are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "rustyline" version = "13.0.0" authors = ["Katsu Kawakami "] exclude = [ "/.github/*", "/rustfmt.toml", ] description = "Rustyline, a readline implementation based on Antirez's Linenoise" documentation = "https://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 = [ "custom-bindings", "derive", "with-dirs", "with-file-history", "with-fuzzy", ] no-default-features = true rustdoc-args = [ "--cfg", "docsrs", ] [[example]] name = "custom_key_bindings" required-features = [ "custom-bindings", "derive", ] [[example]] name = "diy_hints" required-features = ["derive"] [[example]] name = "example" required-features = [ "custom-bindings", "derive", ] [[example]] name = "input_multiline" required-features = [ "custom-bindings", "derive", ] [[example]] name = "input_validation" required-features = ["derive"] [[example]] name = "numeric_input" required-features = ["custom-bindings"] [[example]] name = "read_password" required-features = ["derive"] [[example]] name = "sqlite_history" required-features = ["with-sqlite-history"] [dependencies.bitflags] version = "2.0" [dependencies.cfg-if] version = "1.0" [dependencies.fd-lock] version = "4.0.0" optional = true [dependencies.home] version = "0.5.4" optional = true [dependencies.libc] version = "0.2" [dependencies.log] version = "0.4" [dependencies.memchr] version = "2.0" [dependencies.radix_trie] version = "0.2" optional = true [dependencies.regex] version = "1.5.5" optional = true [dependencies.rusqlite] version = "0.30.0" features = [ "bundled", "backup", ] optional = true default-features = false [dependencies.rustyline-derive] version = "0.10.0" optional = true [dependencies.unicode-segmentation] version = "1.0" [dependencies.unicode-width] version = "0.1" [dev-dependencies.assert_matches] version = "1.2" [dev-dependencies.doc-comment] version = "0.3" [dev-dependencies.env_logger] version = "0.10" default-features = false [dev-dependencies.rand] version = "0.8" [dev-dependencies.tempfile] version = "3.1.0" [features] case_insensitive_history_search = ["regex"] custom-bindings = ["radix_trie"] default = [ "custom-bindings", "with-dirs", "with-file-history", ] derive = ["rustyline-derive"] with-dirs = ["home"] with-file-history = ["fd-lock"] with-fuzzy = ["skim"] with-sqlite-history = ["rusqlite"] [target."cfg(unix)".dependencies.nix] version = "0.27" features = [ "fs", "ioctl", "poll", "signal", "term", ] default-features = false [target."cfg(unix)".dependencies.signal-hook] version = "0.3" optional = true default-features = false [target."cfg(unix)".dependencies.skim] version = "0.10" optional = true default-features = false [target."cfg(unix)".dependencies.termios] version = "0.3.3" optional = true [target."cfg(unix)".dependencies.utf8parse] version = "0.2" [target."cfg(windows)".dependencies.clipboard-win] version = "5.0" [target."cfg(windows)".dependencies.winapi] version = "0.3" features = [ "consoleapi", "handleapi", "synchapi", "minwindef", "processenv", "std", "winbase", "wincon", "winerror", "winuser", ] [badges.maintenance] status = "actively-developed" rustyline-13.0.0/Cargo.toml.orig000064400000000000000000000057211046102023000146370ustar 00000000000000[package] name = "rustyline" version = "13.0.0" authors = ["Katsu Kawakami "] edition = "2021" description = "Rustyline, a readline implementation based on Antirez's Linenoise" documentation = "https://docs.rs/rustyline" repository = "https://github.com/kkawakam/rustyline" readme = "README.md" keywords = ["readline"] license = "MIT" categories = ["command-line-interface"] exclude = [ "/.github/*", "/rustfmt.toml", ] [badges] maintenance = { status = "actively-developed" } [workspace] members = ["rustyline-derive"] [dependencies] bitflags = "2.0" cfg-if = "1.0" # For file completion home = { version = "0.5.4", optional = true } # For History fd-lock = { version = "4.0.0", optional = true } rusqlite = { version = "0.30.0", optional = true, default-features = false, features = ["bundled", "backup"] } libc = "0.2" log = "0.4" unicode-width = "0.1" unicode-segmentation = "1.0" memchr = "2.0" # For custom bindings radix_trie = { version = "0.2", optional = true } regex = { version = "1.5.5", optional = true } # For derive rustyline-derive = { version = "0.10.0", optional = true, path = "rustyline-derive" } [target.'cfg(unix)'.dependencies] nix = { version = "0.27", default-features = false, features = ["fs", "ioctl", "poll", "signal", "term"] } utf8parse = "0.2" skim = { version = "0.10", optional = true, default-features = false } signal-hook = { version = "0.3", optional = true, default-features = false } termios = { version = "0.3.3", optional = true } [target.'cfg(windows)'.dependencies] winapi = { version = "0.3", features = ["consoleapi", "handleapi", "synchapi", "minwindef", "processenv", "std", "winbase", "wincon", "winerror", "winuser"] } clipboard-win = "5.0" [dev-dependencies] doc-comment = "0.3" env_logger = { version = "0.10", default-features = false } tempfile = "3.1.0" rand = "0.8" assert_matches = "1.2" [features] default = ["custom-bindings", "with-dirs", "with-file-history"] custom-bindings = ["radix_trie"] derive = ["rustyline-derive"] with-dirs = ["home"] with-file-history = ["fd-lock"] with-sqlite-history = ["rusqlite"] with-fuzzy = ["skim"] case_insensitive_history_search = ["regex"] [[example]] name = "custom_key_bindings" required-features = ["custom-bindings", "derive"] [[example]] name = "diy_hints" required-features = ["derive"] [[example]] name = "example" required-features = ["custom-bindings", "derive"] [[example]] name = "input_multiline" required-features = ["custom-bindings", "derive"] [[example]] name = "input_validation" required-features = ["derive"] [[example]] name = "numeric_input" required-features = ["custom-bindings"] [[example]] name = "read_password" required-features = ["derive"] [[example]] name = "sqlite_history" required-features = ["with-sqlite-history"] [package.metadata.docs.rs] features = ["custom-bindings", "derive", "with-dirs", "with-file-history", "with-fuzzy"] all-features = false no-default-features = true default-target = "x86_64-unknown-linux-gnu" rustdoc-args = ["--cfg", "docsrs"] rustyline-13.0.0/CustomBinding.md000064400000000000000000000052651046102023000150420ustar 00000000000000## Related topics - [Multiple commands for a keybinding](https://github.com/kkawakam/rustyline/issues/306) and [Conditional Bind Sequences](https://github.com/kkawakam/rustyline/issues/269) : original issues - [Conditional Bind Sequences](https://github.com/kkawakam/rustyline/pull/293) : incomplete proposal - [Add `Cmd::Yield` for complex custom bindings](https://github.com/kkawakam/rustyline/pull/342) : another proposal - [Initial invoke trait and auto-indent example](https://github.com/kkawakam/rustyline/pull/466) : a validator is like a custom action triggered indirectly. And other issues that should be solved if our design is good: - [Extend Meta-F,Alt+Right feature for hint partial completion](https://github.com/kkawakam/rustyline/pull/430) - [Use Arrow-Up to search history with prefix](https://github.com/kkawakam/rustyline/issues/423) - [Execute Arbitrary Command Via Keybinding](https://github.com/kkawakam/rustyline/issues/418) - [Use Ctrl-E for hint completion](https://github.com/kkawakam/rustyline/pull/407) - [Add History Search Behaviour](https://github.com/kkawakam/rustyline/pull/424) - ... ## Conditions / Filters See https://python-prompt-toolkit.readthedocs.io/en/master/pages/advanced_topics/key_bindings.html?highlight=filter#attaching-a-filter-condition Some keys/commands may behave differently depending on: - edit mode (emacs vs vi) - vi input mode (insert vs replace vs command modes) - empty line - cursor position - repeat count - original key pressed (when same command is bound to different key) - hint - ... ## More input Some keys/commands may ask for more input. I am not sure if this point should be tackled here. ## Multiple / complex actions For one key/command, we may want to perform multiple actions. We should ask the undo manager to start a "transaction" before first action and commit it after the last action. Should we do something specific with the kill ring ? We should refresh / repaint only when all actions are performed (or if ask explicitly?) depending on cumulated action impacts. ... ## Misc ```rust /// Command / action result #[derive(Debug, Clone, PartialEq, Copy)] #[non_exhaustive] pub enum ActionResult { // Interrupt / reject user input // => Err should be fine //Bail, /// Continue, /// Accept user input (except if `Validator` disagrees) Return, } ``` ```rust bitflags::bitflags! { #[doc = "Action invocation impacts"] pub struct Impacts: u8 { const PUSH_CHAR = 0b0000_0001; const BEEP = 0b0000_0010; const MOVE_CURSOR = 0b0000_0100; // State::move_cursor const REFRESH = 0b0000_1000; // State::refresh_line const CLEAR_SREEN = 0b0001_0000; // State::clear_screen } } ``` rustyline-13.0.0/Features.md000064400000000000000000000042161046102023000140460ustar 00000000000000# Extra features | Alt | Scroll | Continuation prompt | Right prompt | Suspend | | -------------- | ------ | ------------------- | ------------ | ------- | | isocline | X | X | | | | linefeed | | | | Unix | | liner | | | | | | prompt-toolkit | X | X | X | Unix | | reedline | | X | X | | | replxx | | | | Unix | | rustyline | | | | Unix | | termwiz | | | | | Scroll: for very long line (longer than screen cols\*rows), scrolls from start to end.\ Continuation prompt: for multiline input, display a different prompt\ Suspend: Control-Z | Alt | Editable History | Custom history backend | History timestamp | | -------------- | ---------------- | ---------------------- | ----------------- | | isocline | | | | | linefeed | X | | | | liner | | | | | prompt-toolkit | | X | | | reedline | | X | | | replxx | X | | X | | rustyline | | X | | | termwiz | | X | \* | Editable History: any history entry can be edited and saved\ Custom history backend: history persistence can be customized\ History timestamp: history entries are timestamped Mouse support Text selection Completion candidates display Multiple commands for a keybinding Auto indent Minimal repaint Overwrite mode Lexer / Parser Configuration file (inputrc) Dynamic prompt (editing mode) External print rustyline-13.0.0/History.md000064400000000000000000000021271046102023000137300ustar 00000000000000# Config max_history_size # Current session - we should remember (index) of the first line inserted by this session. - if no line has been inserted => do nothing on save - reset this index after saving successfully. - we should remember (path and timestamp) of the file used to initialize/`load` history. - if path used to save history is the same: - if timestamp is still the same => we can append only new lines if history has not been truncated. - update timestamp after saving successfully. - we should remember (path and timestamp) of the file used to persist/`save` history. - reset them if `load` is then called with a different path - update them if `load` is then called with same path. - update them after saving successfully - if path used to save history is the same: - if timestamp is still the same => we can append only new lines if history has not been truncated. ``` HistoryInfo first_add_index: Option, // first line inserted by this session truncated: bool // path_info: Option, ``` ``` PathInfo path: Path, modified: SystemTime, ``` rustyline-13.0.0/Incremental.md000064400000000000000000000034371046102023000145350ustar 00000000000000## Incremental computation We would like to avoid redrawing all row(s) when an event occurs. Currently, we redraw all row(s) except when: - a character is inserted at the end of input (and there is no hint and no `highlight_char`), - only the cursor is moved (input is not touched and no `highlight_char`). Ideally, we would like to redraw only impacted row(s) / cell(s). ### Observable values Currently, we assume that highlighting does not impact layout / rendered text size. So only the following observables impact layout: - prompt (interactive search, [input mode indicator](https://github.com/kkawakam/rustyline/pull/369)), - [input mode](https://github.com/kkawakam/rustyline/pull/369), - line(s) buffer, - cursor position, - hint / input validation message, - screen size (line wrapping), - [prompt continuation](https://github.com/kkawakam/rustyline/pull/372)s, - row/wrap count. Some other values may impact layout but they are/should be constant: - tab stop, ### Line wrapping and highlighting Currently, there is no word wrapping (only grapheme wrapping). But we highlight the whole input at once. So there is no issue: for example, even if a keyword is wrapped, style is preserved. With [prompt continuation](https://github.com/kkawakam/rustyline/pull/372)s, we (will) interleave user input with continuations. So we need to preserve style. TODO How prompt_toolkit handle this problem ? Maybe using ANSI sequence directly was a bad idea. If `Highlighter` returns style ranges, applying style on input slice is easy (and also supporting styles on Windows < 10). ### Impacts Current granularity: - PUSH_CHAR at end of input - BEEP - MOVE_CURSOR - REFRESH whole input / rows - CLEAR_SCREEN (+REFRESH) Wanted additional granularity: - PUSH_STRING at end of input - REFRESH_DIRTY only rows / cells rustyline-13.0.0/LICENSE000064400000000000000000000021161046102023000127500ustar 00000000000000The 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-13.0.0/README.md000064400000000000000000000370611046102023000132310ustar 00000000000000# 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) [![](https://img.shields.io/crates/v/rustyline.svg)](https://crates.io/crates/rustyline) [![Docs](https://docs.rs/rustyline/badge.svg)](https://docs.rs/rustyline) Readline implementation in Rust that is based on [Antirez' Linenoise](https://github.com/antirez/linenoise) **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 - Highlighting / Colors are not supported on Windows < Windows 10 except with ConEmu and `ColorMode::Forced`. ## Example ```rust use rustyline::error::ReadlineError; use rustyline::{DefaultEditor, Result}; fn main() -> Result<()> { // `()` can be used when no completer is required let mut rl = DefaultEditor::new()?; #[cfg(feature = "with-file-history")] 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 } } } #[cfg(feature = "with-file-history")] rl.save_history("history.txt"); Ok(()) } ``` ## crates.io You can use this package in your project by adding the following to your `Cargo.toml`: ```toml [dependencies] rustyline = "13.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 $ # print out a terminfo description: $ infocmp ``` ## 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 | ? | ? | | [isocline][] | C | Ux/Win | ANSI | Yes | Yes | any | Emacs | No | Yes | 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 | | [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 | | [reedline][] | Rust | Ux/Win | ANSI | Yes | Yes | any | Emacs/vi/bind | No | Yes | Ux/Win | Yes | | [replxx][] | C/C++ | Ux/Win | ANSI | Yes | Yes | only line | Emacs | Yes | No | Ux/Win | Yes | | Rustyline | Rust | Ux/Win | ANSI | Yes | Yes | any | Emacs/vi/bind | Yes | Yes | Ux/Win 10+ | Yes | | [termwiz][] | Rust | Ux/Win | Any | ? | Yes | any | Emacs | No | No | Ux/Win | No | [go-prompt]: https://github.com/c-bata/go-prompt [haskeline]: https://github.com/judah/haskeline [isocline]: https://github.com/daanx/isocline [linefeed]: https://github.com/murarth/linefeed [linenoise]: https://github.com/antirez/linenoise [liner]: https://github.com/redox-os/liner [prompt_toolkit]: https://github.com/jonathanslenders/python-prompt-toolkit [reedline]: https://github.com/nushell/reedline [replxx]: https://github.com/AmokHuginnsson/replxx [termwiz]: https://github.com/wez/wezterm/tree/main/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-13.0.0/TODO.md000064400000000000000000000056511046102023000130410ustar 00000000000000API - [ ] 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 - [x] 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. - [x] append_history - [ ] history_truncate_file - [X] custom persistent storage Input - [ ] Password input (#58) (https://github.com/conradkdotcom/rpassword) (https://github.com/antirez/linenoise/issues/125) - [x] quoted insert (#65) - [ ] Overwrite mode (em-toggle-overwrite, vi-replace-mode, rl_insert_mode) - [ ] Encoding - [x] \[Ctrl-]\[Alt-]\[Shift-] (#121) Layout - [ ] Scroll - [ ] 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-13.0.0/examples/custom_key_bindings.rs000064400000000000000000000065411046102023000201740ustar 00000000000000use std::borrow::Cow::{self, Borrowed, Owned}; use rustyline::highlight::Highlighter; use rustyline::hint::HistoryHinter; use rustyline::history::DefaultHistory; use rustyline::{ Cmd, ConditionalEventHandler, Editor, Event, EventContext, EventHandler, KeyEvent, RepeatCount, Result, }; use rustyline::{Completer, Helper, Hinter, Validator}; #[derive(Completer, Helper, Hinter, Validator)] struct MyHelper(#[rustyline(Hinter)] HistoryHinter); impl Highlighter for MyHelper { fn highlight_prompt<'b, 's: 'b, 'p: 'b>( &'s self, prompt: &'p str, default: bool, ) -> Cow<'b, str> { if default { Owned(format!("\x1b[1;32m{prompt}\x1b[m")) } else { Borrowed(prompt) } } fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { Owned(format!("\x1b[1m{hint}\x1b[m")) } } #[derive(Clone)] struct CompleteHintHandler; impl ConditionalEventHandler for CompleteHintHandler { fn handle(&self, evt: &Event, _: RepeatCount, _: bool, ctx: &EventContext) -> Option { if !ctx.has_hint() { return None; // default } if let Some(k) = evt.get(0) { #[allow(clippy::if_same_then_else)] if *k == KeyEvent::ctrl('E') { Some(Cmd::CompleteHint) } else if *k == KeyEvent::alt('f') && ctx.line().len() == ctx.pos() { let text = ctx.hint_text()?; let mut start = 0; if let Some(first) = text.chars().next() { if !first.is_alphanumeric() { start = text.find(|c: char| c.is_alphanumeric()).unwrap_or_default(); } } let text = text .chars() .enumerate() .take_while(|(i, c)| *i <= start || c.is_alphanumeric()) .map(|(_, c)| c) .collect::(); Some(Cmd::Insert(1, text)) } else { None } } else { unreachable!() } } } struct TabEventHandler; impl ConditionalEventHandler for TabEventHandler { fn handle(&self, evt: &Event, n: RepeatCount, _: bool, ctx: &EventContext) -> Option { debug_assert_eq!(*evt, Event::from(KeyEvent::from('\t'))); if ctx.line()[..ctx.pos()] .chars() .next_back() .filter(|c| c.is_whitespace()) .is_some() { Some(Cmd::SelfInsert(n, '\t')) } else { None // default complete } } } fn main() -> Result<()> { let mut rl = Editor::::new()?; rl.set_helper(Some(MyHelper(HistoryHinter::new()))); let ceh = Box::new(CompleteHintHandler); rl.bind_sequence(KeyEvent::ctrl('E'), EventHandler::Conditional(ceh.clone())); rl.bind_sequence(KeyEvent::alt('f'), EventHandler::Conditional(ceh)); rl.bind_sequence( KeyEvent::from('\t'), EventHandler::Conditional(Box::new(TabEventHandler)), ); rl.bind_sequence( Event::KeySeq(vec![KeyEvent::ctrl('X'), KeyEvent::ctrl('E')]), EventHandler::Simple(Cmd::Suspend), // TODO external editor ); loop { let line = rl.readline("> ")?; rl.add_history_entry(line.as_str())?; println!("Line: {line}"); } } rustyline-13.0.0/examples/diy_hints.rs000064400000000000000000000051061046102023000161230ustar 00000000000000use std::collections::HashSet; use rustyline::hint::{Hint, Hinter}; use rustyline::history::DefaultHistory; use rustyline::Context; use rustyline::{Completer, Helper, Highlighter, Validator}; use rustyline::{Editor, Result}; #[derive(Completer, Helper, Validator, Highlighter)] struct DIYHinter { // It's simple example of rustyline, for more efficient, please use ** radix trie ** hints: HashSet, } #[derive(Hash, Debug, PartialEq, Eq)] struct CommandHint { display: String, complete_up_to: usize, } impl Hint for CommandHint { fn display(&self) -> &str { &self.display } fn completion(&self) -> Option<&str> { if self.complete_up_to > 0 { Some(&self.display[..self.complete_up_to]) } else { None } } } impl CommandHint { fn new(text: &str, complete_up_to: &str) -> CommandHint { assert!(text.starts_with(complete_up_to)); CommandHint { display: text.into(), complete_up_to: complete_up_to.len(), } } fn suffix(&self, strip_chars: usize) -> CommandHint { CommandHint { display: self.display[strip_chars..].to_owned(), complete_up_to: self.complete_up_to.saturating_sub(strip_chars), } } } impl Hinter for DIYHinter { type Hint = CommandHint; fn hint(&self, line: &str, pos: usize, _ctx: &Context<'_>) -> Option { if line.is_empty() || pos < line.len() { return None; } self.hints .iter() .filter_map(|hint| { // expect hint after word complete, like redis cli, add condition: // line.ends_with(" ") if hint.display.starts_with(line) { Some(hint.suffix(pos)) } else { None } }) .next() } } fn diy_hints() -> HashSet { let mut set = HashSet::new(); set.insert(CommandHint::new("help", "help")); set.insert(CommandHint::new("get key", "get ")); set.insert(CommandHint::new("set key value", "set ")); set.insert(CommandHint::new("hget key field", "hget ")); set.insert(CommandHint::new("hset key field value", "hset ")); set } fn main() -> Result<()> { println!("This is a DIY hint hack of rustyline"); let h = DIYHinter { hints: diy_hints() }; let mut rl: Editor = Editor::new()?; rl.set_helper(Some(h)); loop { let input = rl.readline("> ")?; println!("input: {input}"); } } rustyline-13.0.0/examples/example.rs000064400000000000000000000060711046102023000155660ustar 00000000000000use std::borrow::Cow::{self, Borrowed, Owned}; use rustyline::completion::FilenameCompleter; use rustyline::error::ReadlineError; use rustyline::highlight::{Highlighter, MatchingBracketHighlighter}; use rustyline::hint::HistoryHinter; use rustyline::validate::MatchingBracketValidator; use rustyline::{Cmd, CompletionType, Config, EditMode, Editor, KeyEvent}; use rustyline::{Completer, Helper, Hinter, Validator}; #[derive(Helper, Completer, Hinter, Validator)] struct MyHelper { #[rustyline(Completer)] completer: FilenameCompleter, highlighter: MatchingBracketHighlighter, #[rustyline(Validator)] validator: MatchingBracketValidator, #[rustyline(Hinter)] hinter: HistoryHinter, colored_prompt: String, } 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, forced: bool) -> bool { self.highlighter.highlight_char(line, pos, forced) } } // 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) .build(); let h = MyHelper { completer: FilenameCompleter::new(), highlighter: MatchingBracketHighlighter::new(), hinter: HistoryHinter::new(), colored_prompt: "".to_owned(), validator: MatchingBracketValidator::new(), }; let mut rl = Editor::with_config(config)?; rl.set_helper(Some(h)); rl.bind_sequence(KeyEvent::alt('n'), Cmd::HistorySearchForward); rl.bind_sequence(KeyEvent::alt('p'), Cmd::HistorySearchBackward); if rl.load_history("history.txt").is_err() { println!("No previous history."); } let mut count = 1; loop { let p = format!("{count}> "); rl.helper_mut().expect("No helper").colored_prompt = format!("\x1b[1;32m{p}\x1b[0m"); let readline = rl.readline(&p); match readline { Ok(line) => { rl.add_history_entry(line.as_str())?; println!("Line: {line}"); } Err(ReadlineError::Interrupted) => { println!("Interrupted"); break; } Err(ReadlineError::Eof) => { println!("Encountered Eof"); break; } Err(err) => { println!("Error: {err:?}"); break; } } count += 1; } rl.append_history("history.txt") } rustyline-13.0.0/examples/external_print.rs000064400000000000000000000014071046102023000171670ustar 00000000000000use std::thread; use std::time::Duration; use rand::{thread_rng, Rng}; use rustyline::{DefaultEditor, ExternalPrinter, Result}; fn main() -> Result<()> { let mut rl = DefaultEditor::new()?; let mut printer = rl.create_external_printer()?; thread::spawn(move || { let mut rng = thread_rng(); let mut i = 0usize; loop { printer .print(format!("External message #{i}")) .expect("External print failure"); let wait_ms = rng.gen_range(1000..10000); thread::sleep(Duration::from_millis(wait_ms)); i += 1; } }); loop { let line = rl.readline("> ")?; rl.add_history_entry(line.as_str())?; println!("Line: {line}"); } } rustyline-13.0.0/examples/input_multiline.rs000064400000000000000000000016401046102023000173510ustar 00000000000000use rustyline::highlight::MatchingBracketHighlighter; use rustyline::validate::MatchingBracketValidator; use rustyline::{Cmd, Editor, EventHandler, KeyCode, KeyEvent, Modifiers, Result}; use rustyline::{Completer, Helper, Highlighter, Hinter, Validator}; #[derive(Completer, Helper, Highlighter, Hinter, Validator)] struct InputValidator { #[rustyline(Validator)] brackets: MatchingBracketValidator, #[rustyline(Highlighter)] highlighter: MatchingBracketHighlighter, } fn main() -> Result<()> { let h = InputValidator { brackets: MatchingBracketValidator::new(), highlighter: MatchingBracketHighlighter::new(), }; let mut rl = Editor::new()?; rl.set_helper(Some(h)); rl.bind_sequence( KeyEvent(KeyCode::Char('s'), Modifiers::CTRL), EventHandler::Simple(Cmd::Newline), ); let input = rl.readline("> ")?; println!("Input: {input}"); Ok(()) } rustyline-13.0.0/examples/input_validation.rs000064400000000000000000000016261046102023000175050ustar 00000000000000use rustyline::validate::{ValidationContext, ValidationResult, Validator}; use rustyline::{Completer, Helper, Highlighter, Hinter}; use rustyline::{Editor, Result}; #[derive(Completer, Helper, Highlighter, Hinter)] struct InputValidator {} impl Validator for InputValidator { fn validate(&self, ctx: &mut ValidationContext) -> Result { use ValidationResult::{Incomplete, Invalid, Valid}; let input = ctx.input(); let result = if !input.starts_with("SELECT") { Invalid(Some(" --< Expect: SELECT stmt".to_owned())) } else if !input.ends_with(';') { Incomplete } else { Valid(None) }; Ok(result) } } fn main() -> Result<()> { let h = InputValidator {}; let mut rl = Editor::new()?; rl.set_helper(Some(h)); let input = rl.readline("> ")?; println!("Input: {input}"); Ok(()) } rustyline-13.0.0/examples/minimal.rs000064400000000000000000000004201046102023000155510ustar 00000000000000use rustyline::{DefaultEditor, Result}; /// Minimal REPL fn main() -> Result<()> { env_logger::init(); let mut rl = DefaultEditor::new()?; loop { let line = rl.readline("> ")?; // read println!("Line: {line}"); // eval / print } // loop } rustyline-13.0.0/examples/numeric_input.rs000064400000000000000000000016651046102023000170200ustar 00000000000000use rustyline::{ Cmd, ConditionalEventHandler, DefaultEditor, Event, EventContext, EventHandler, KeyCode, KeyEvent, Modifiers, RepeatCount, Result, }; struct FilteringEventHandler; impl ConditionalEventHandler for FilteringEventHandler { fn handle(&self, evt: &Event, _: RepeatCount, _: bool, _: &EventContext) -> Option { if let Some(KeyEvent(KeyCode::Char(c), m)) = evt.get(0) { if m.contains(Modifiers::CTRL) || m.contains(Modifiers::ALT) || c.is_ascii_digit() { None } else { Some(Cmd::Noop) // filter out invalid input } } else { None } } } fn main() -> Result<()> { let mut rl = DefaultEditor::new()?; rl.bind_sequence( Event::Any, EventHandler::Conditional(Box::new(FilteringEventHandler)), ); loop { let line = rl.readline("> ")?; println!("Num: {line}"); } } rustyline-13.0.0/examples/read_password.rs000064400000000000000000000025631046102023000167720ustar 00000000000000use std::borrow::Cow::{self, Borrowed, Owned}; use rustyline::config::Configurer; use rustyline::highlight::Highlighter; use rustyline::{ColorMode, Editor, Result}; use rustyline::{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, _forced: bool) -> bool { self.masking } } fn main() -> Result<()> { println!("This is just a hack. Reading passwords securely requires more than that."); let h = MaskingHighlighter { masking: false }; let mut rl = Editor::new()?; 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 mut guard = rl.set_cursor_visibility(false)?; let passwd = rl.readline("Password:")?; guard.take(); println!("Secret: {passwd}"); Ok(()) } rustyline-13.0.0/examples/sqlite_history.rs000064400000000000000000000010221046102023000172040ustar 00000000000000use rustyline::{Config, Editor, Result}; fn main() -> Result<()> { let config = Config::builder().auto_add_history(true).build(); let history = if false { // memory rustyline::sqlite_history::SQLiteHistory::with_config(config)? } else { // file rustyline::sqlite_history::SQLiteHistory::open(config, "history.sqlite3")? }; let mut rl: Editor<(), _> = Editor::with_history(config, history)?; loop { let line = rl.readline("> ")?; println!("{line}"); } } rustyline-13.0.0/linenoise.md000064400000000000000000000044151046102023000142560ustar 00000000000000Mapping between linenoise API and rustyline API | linenoise | rustyline | Remarks | |--------------------------------|------------------------------|---------------------------| | linenoiseState | State | | | *Blocking API* | | linenoise | Editor::readline | | linenoiseFree | _ | RAII | | *Non blocking API* | | | linenoiseEditStart | _ | | linenoiseEditFeed | _ | | linenoiseEditStop | _ | | linenoiseHide | Renderer::clear_rows | | linenoiseShow | State::refresh_line | | *Completion API* | | linenoiseCompletions | Vec | | linenoiseCompletionCallback | Completer | | linenoiseAddCompletion | _ | std Vec::add | | linenoiseSetCompletionCallback | Editor::set_helper | | linenoiseHintsCallback | Hinter | | linenoiseSetHintsCallback | Editor::set_helper | | linenoiseFreeHintsCallback | _ | RAII | | linenoiseSetFreeHintsCallback | _ | RAII | | *History API* | | linenoiseHistoryAdd | Editor::add_history_entry | | linenoiseHistorySetMaxLen | Editor::set_max_history_size | | linenoiseHistorySave | Editor::save_history | | linenoiseHistoryLoad | Editor::load_history | | *Other utilities* | | linenoiseClearScreen | Editor::clear_screen | | linenoiseSetMultiLine | _ | Always activated | | linenoisePrintKeyCodes | _ | debug logs | | linenoiseMaskModeEnable | _ | see read_password example | | linenoiseMaskModeDisable | _ | rustyline-13.0.0/src/binding.rs000064400000000000000000000162171046102023000145210ustar 00000000000000/// Custom event handlers use crate::{ Cmd, EditMode, InputMode, InputState, KeyCode, KeyEvent, Modifiers, Refresher, RepeatCount, }; use radix_trie::TrieKey; /// Input event #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr(docsrs, doc(cfg(feature = "custom-bindings")))] pub enum Event { /// Wildcard. /// Useful if you want to filter out some keys. Any, /// Key sequence KeySeq(Vec), /// TODO Mouse event Mouse(), } impl Event { /// See [`KeyEvent::normalize`] pub(crate) fn normalize(mut self) -> Self { if let Event::KeySeq(ref mut keys) = self { for key in keys.iter_mut() { *key = KeyEvent::normalize(*key); } } self } /// Return `i`th key event #[must_use] pub fn get(&self, i: usize) -> Option<&KeyEvent> { if let Event::KeySeq(ref ks) = self { ks.get(i) } else { None } } } impl From for Event { fn from(k: KeyEvent) -> Event { Event::KeySeq(vec![k]) } } const BASE: u32 = 0x0010_ffff + 1; const BASE_CONTROL: u32 = 0x0200_0000; const BASE_META: u32 = 0x0400_0000; const BASE_SHIFT: u32 = 0x0100_0000; const ESCAPE: u32 = 27; const PAGE_UP: u32 = BASE + 1; const PAGE_DOWN: u32 = PAGE_UP + 1; const DOWN: u32 = PAGE_DOWN + 1; const UP: u32 = DOWN + 1; const LEFT: u32 = UP + 1; const RIGHT: u32 = LEFT + 1; const HOME: u32 = RIGHT + 1; const END: u32 = HOME + 1; const DELETE: u32 = END + 1; const INSERT: u32 = DELETE + 1; //const F1: u32 = INSERT + 1; const MOUSE: u32 = /* F24 + 1 */ INSERT + 25; const PASTE_START: u32 = MOUSE + 1; const PASTE_FINISH: u32 = PASTE_START + 1; const ANY: u32 = PASTE_FINISH + 1; impl KeyEvent { fn encode(&self) -> u32 { let mut u = match self.0 { KeyCode::UnknownEscSeq | KeyCode::Null => 0, KeyCode::Backspace => u32::from('\x7f'), KeyCode::BackTab => u32::from('\t') | BASE_SHIFT, KeyCode::BracketedPasteStart => PASTE_START, KeyCode::BracketedPasteEnd => PASTE_FINISH, KeyCode::Char(c) => u32::from(c), KeyCode::Delete => DELETE, KeyCode::Down => DOWN, KeyCode::End => END, KeyCode::Enter => u32::from('\r'), KeyCode::F(i) => INSERT + u32::from(i), KeyCode::Esc => ESCAPE, KeyCode::Home => HOME, KeyCode::Insert => INSERT, KeyCode::Left => LEFT, KeyCode::PageDown => PAGE_DOWN, KeyCode::PageUp => PAGE_UP, KeyCode::Right => RIGHT, KeyCode::Tab => u32::from('\t'), KeyCode::Up => UP, }; if self.1.contains(Modifiers::CTRL) { u |= BASE_CONTROL; } if self.1.contains(Modifiers::ALT) { u |= BASE_META; } if self.1.contains(Modifiers::SHIFT) { u |= BASE_SHIFT; } u } } impl TrieKey for Event { fn encode_bytes(&self) -> Vec { match self { Event::Any => ANY.to_be_bytes().to_vec(), Event::KeySeq(keys) => { let mut dst = Vec::with_capacity(keys.len() * 4); for key in keys { dst.extend_from_slice(&key.encode().to_be_bytes()); } dst } Event::Mouse() => MOUSE.to_be_bytes().to_vec(), } } } /// Event handler #[cfg_attr(docsrs, doc(cfg(feature = "custom-bindings")))] pub enum EventHandler { /// unconditional command Simple(Cmd), /// handler behaviour depends on input state Conditional(Box), /* invoke multiple actions * TODO Macro(), */ } impl From for EventHandler { fn from(c: Cmd) -> EventHandler { EventHandler::Simple(c) } } /// Give access to user input. #[cfg_attr(docsrs, doc(cfg(feature = "custom-bindings")))] pub struct EventContext<'r> { mode: EditMode, input_mode: InputMode, wrt: &'r dyn Refresher, } impl<'r> EventContext<'r> { pub(crate) fn new(is: &InputState, wrt: &'r dyn Refresher) -> Self { EventContext { mode: is.mode, input_mode: is.input_mode, wrt, } } /// emacs or vi mode #[must_use] pub fn mode(&self) -> EditMode { self.mode } /// vi input mode #[must_use] pub fn input_mode(&self) -> InputMode { self.input_mode } /// Returns `true` if there is a hint displayed. #[must_use] pub fn has_hint(&self) -> bool { self.wrt.has_hint() } /// Returns the hint text that is shown after the current cursor position. #[must_use] pub fn hint_text(&self) -> Option<&str> { self.wrt.hint_text() } /// currently edited line #[must_use] pub fn line(&self) -> &str { self.wrt.line() } /// Current cursor position (byte position) #[must_use] pub fn pos(&self) -> usize { self.wrt.pos() } } /// May behave differently depending on: /// * edit mode (emacs vs vi) /// * vi input mode (insert vs replace vs command modes) /// * empty line /// * cursor position /// * repeat count /// * original key pressed (when same command is bound to different key) /// * hint /// * ... #[cfg_attr(docsrs, doc(cfg(feature = "custom-bindings")))] pub trait ConditionalEventHandler: Send + Sync { /// Takes the current input state and /// returns the command to be performed or `None` to perform the default /// one. fn handle( &self, evt: &Event, n: RepeatCount, positive: bool, ctx: &EventContext, ) -> Option; } #[cfg(test)] mod test { use super::{Event, EventHandler}; use crate::{Cmd, KeyCode, KeyEvent, Modifiers}; use radix_trie::Trie; #[test] fn encode() { let mut trie = Trie::new(); let evt = Event::KeySeq(vec![KeyEvent::ctrl('X'), KeyEvent::ctrl('E')]); trie.insert(evt.clone(), EventHandler::from(Cmd::Noop)); let prefix = Event::from(KeyEvent::ctrl('X')); let subtrie = trie.get_raw_descendant(&prefix); assert!(subtrie.is_some()); let subtrie = subtrie.unwrap(); let sub_result = subtrie.get(&evt); assert!(sub_result.unwrap().is_some()); let prefix = Event::from(KeyEvent::ctrl('O')); let subtrie = trie.get_raw_descendant(&prefix); assert!(subtrie.is_none()) } #[test] fn no_collision() { use {Event as E, EventHandler as H, KeyCode as C, KeyEvent as K, Modifiers as M}; let mut trie = Trie::new(); trie.insert(E::from(K(C::Backspace, M::NONE)), H::from(Cmd::Noop)); trie.insert(E::from(K(C::Enter, M::NONE)), H::from(Cmd::Noop)); trie.insert(E::from(K(C::Tab, M::NONE)), H::from(Cmd::Noop)); trie.insert(E::from(K(C::Backspace, M::CTRL)), H::from(Cmd::Noop)); trie.insert(E::from(K(C::Enter, M::CTRL)), H::from(Cmd::Noop)); trie.insert(E::from(K(C::Tab, M::CTRL)), H::from(Cmd::Noop)); } #[test] #[cfg(target_arch = "x86_64")] fn size_of_event() { use core::mem::size_of; assert_eq!(size_of::(), 32); } } rustyline-13.0.0/src/command.rs000064400000000000000000000174331046102023000145260ustar 00000000000000use crate::complete_hint_line; use crate::config::Config; use crate::edit::State; use crate::error; use crate::history::SearchDirection; use crate::keymap::{Anchor, At, Cmd, Movement, Word}; use crate::keymap::{InputState, Refresher}; use crate::kill_ring::{KillRing, Mode}; use crate::line_buffer::WordAction; use crate::{Helper, Result}; pub enum Status { Proceed, Submit, } pub fn execute( cmd: Cmd, s: &mut State<'_, '_, H>, input_state: &InputState, kill_ring: &mut KillRing, config: &Config, ) -> Result { use Status::{Proceed, Submit}; match cmd { Cmd::EndOfFile | Cmd::AcceptLine | Cmd::AcceptOrInsertLine { .. } | Cmd::Newline => { if s.has_hint() || !s.is_default_prompt() || s.highlight_char { // Force a refresh without hints to leave the previous // line as the user typed it after a newline. s.forced_refresh = true; s.refresh_line_with_msg(None)?; s.forced_refresh = false; } } _ => {} }; match cmd { Cmd::CompleteHint => { complete_hint_line(s)?; } Cmd::SelfInsert(n, c) => { s.edit_insert(c, n)?; } Cmd::Insert(n, text) => { s.edit_yank(input_state, &text, Anchor::Before, n)?; } Cmd::Move(Movement::BeginningOfLine) => { // Move to the beginning of line. s.edit_move_home()?; } Cmd::Move(Movement::ViFirstPrint) => { s.edit_move_home()?; if s.line.starts_with(char::is_whitespace) { 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, kill_ring)?; if let Some(text) = text { s.edit_insert_text(&text)?; } } Cmd::Overwrite(c) => { s.edit_overwrite_char(c)?; } Cmd::EndOfFile => { if s.line.is_empty() { return Err(error::ReadlineError::Eof); } else if !input_state.is_emacs_mode() { return Ok(Submit); } } Cmd::Move(Movement::EndOfLine) => { // Move to the end of line. s.edit_move_end()?; } Cmd::Move(Movement::ForwardChar(n)) => { // Move forward a character. s.edit_move_forward(n)?; } Cmd::ClearScreen => { // Clear the screen leaving the current line at the top of the screen. s.clear_screen()?; s.refresh_line()?; } Cmd::NextHistory => { // Fetch the next command from the history list. s.edit_history_next(false)?; } Cmd::PreviousHistory => { // Fetch the previous command from the history list. s.edit_history_next(true)?; } Cmd::LineUpOrPreviousHistory(n) => { if !s.edit_move_line_up(n)? { s.edit_history_next(true)?; } } Cmd::LineDownOrNextHistory(n) => { if !s.edit_move_line_down(n)? { s.edit_history_next(false)?; } } Cmd::HistorySearchBackward => s.edit_history_search(SearchDirection::Reverse)?, Cmd::HistorySearchForward => s.edit_history_search(SearchDirection::Forward)?, Cmd::TransposeChars => { // Exchange the char before cursor with the character at cursor. s.edit_transpose_chars()?; } Cmd::Yank(n, anchor) => { // retrieve (yank) last item killed 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) { kill_ring.kill(&text, Mode::Append); } } Cmd::Newline => { s.edit_insert('\n', 1)?; } Cmd::Repaint => { s.refresh_line()?; } Cmd::AcceptLine | Cmd::AcceptOrInsertLine { .. } => { let validation_result = s.validate()?; let valid = validation_result.is_valid(); let end = s.line.is_end_of_input(); match (cmd, valid, end) { (Cmd::AcceptLine, ..) | (Cmd::AcceptOrInsertLine { .. }, true, true) | ( Cmd::AcceptOrInsertLine { accept_in_the_middle: true, }, true, _, ) => { return Ok(Submit); } (Cmd::AcceptOrInsertLine { .. }, false, _) | (Cmd::AcceptOrInsertLine { .. }, true, false) => { if valid || !validation_result.has_message() { s.edit_insert('\n', 1)?; } } _ => unreachable!(), } } Cmd::BeginningOfHistory => { // move to first entry in history s.edit_history(true)?; } Cmd::EndOfHistory => { // move to last entry in history s.edit_history(false)?; } Cmd::Move(Movement::BackwardWord(n, word_def)) => { // move backwards one word s.edit_move_to_prev_word(word_def, n)?; } Cmd::CapitalizeWord => { // capitalize word after point s.edit_word(WordAction::Capitalize)?; } Cmd::Kill(ref mvt) => { s.edit_kill(mvt, kill_ring)?; } Cmd::Move(Movement::ForwardWord(n, at, word_def)) => { // move forwards one word s.edit_move_to_next_word(at, word_def, n)?; } Cmd::Move(Movement::LineUp(n)) => { s.edit_move_line_up(n)?; } Cmd::Move(Movement::LineDown(n)) => { s.edit_move_line_down(n)?; } Cmd::Move(Movement::BeginningOfBuffer) => { // Move to the start of the buffer. s.edit_move_buffer_start()?; } Cmd::Move(Movement::EndOfBuffer) => { // Move to the end of the buffer. s.edit_move_buffer_end()?; } Cmd::DowncaseWord => { // lowercase word after point s.edit_word(WordAction::Lowercase)?; } Cmd::TransposeWords(n) => { // transpose words s.edit_transpose_words(n)?; } Cmd::UpcaseWord => { // uppercase word after point s.edit_word(WordAction::Uppercase)?; } Cmd::YankPop => { // yank-pop 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.undo(&mut s.line, n) { s.refresh_line()?; } } Cmd::Dedent(mvt) => { s.edit_indent(&mvt, config.indent_size(), true)?; } Cmd::Indent(mvt) => { s.edit_indent(&mvt, config.indent_size(), false)?; } Cmd::Interrupt => { // Move to end, in case cursor was in the middle of the // line, so that next thing application prints goes after // the input s.move_cursor_to_end()?; return Err(error::ReadlineError::Interrupted); } _ => { // Ignore the character typed. } } Ok(Proceed) } rustyline-13.0.0/src/completion.rs000064400000000000000000000463421046102023000152620ustar 00000000000000//! 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}; /// 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() } } /// #[deprecated = "Unusable"] impl Candidate for str { fn display(&self) -> &str { self } fn replacement(&self) -> &str { self } } impl Candidate for &'_ str { fn display(&self) -> &str { self } fn replacement(&self) -> &str { self } } impl Candidate for Rc { fn display(&self) -> &str { self } fn replacement(&self) -> &str { self } } /// Completion candidate pair #[derive(Clone)] pub struct Pair { /// Text to display when listing alternatives. pub display: String, /// Text to insert in line. pub replacement: String, } impl Candidate for Pair { fn display(&self) -> &str { self.display.as_str() } fn replacement(&self) -> &str { self.replacement.as_str() } } // TODO: let the implementers customize how the candidate(s) are displayed // https://github.com/kkawakam/rustyline/issues/302 /// To be called for tab-completion. pub trait Completer { /// Specific completion candidate. type Candidate: Candidate; // TODO: let the implementers choose/find word boundaries ??? => Lexer /// Takes the currently edited `line` with the cursor `pos`ition and /// returns the start position and the completion candidates for the /// partial word to be completed. /// /// ("ls /usr/loc", 11) => Ok((3, vec!["/usr/local/"])) fn complete( &self, // FIXME should be `&mut 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, cl: &mut Changeset) { let end = line.pos(); line.replace(start..end, elected, cl); } } impl Completer for () { type Candidate = String; fn update(&self, _line: &mut LineBuffer, _start: usize, _elected: &str, _cl: &mut Changeset) { 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, cl: &mut Changeset) { (**self).update(line, start, elected, cl); } } 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, cl: &mut Changeset) { (**self).update(line, start, elected, cl) } } )* } } use crate::undo::Changeset; 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: fn(char) -> bool, double_quotes_special_chars: fn(char) -> bool, } 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 fn default_break_chars(c : char) -> bool { matches!(c, ' ' | '\t' | '\n' | '"' | '\\' | '\'' | '`' | '@' | '$' | '>' | '<' | '=' | ';' | '|' | '&' | '{' | '(' | '\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 fn double_quotes_special_chars(c: char) -> bool { matches!(c, '"' | '$' | '\\' | '`') } } else if #[cfg(windows)] { // Remove \ to make file completion works on windows const fn default_break_chars(c: char) -> bool { matches!(c, ' ' | '\t' | '\n' | '"' | '\'' | '`' | '@' | '$' | '>' | '<' | '=' | ';' | '|' | '&' | '{' | '(' | '\0') } const ESCAPE_CHAR: Option = None; const fn double_quotes_special_chars(c: char) -> bool { c == '"' } // TODO Validate: only '"' ? } else if #[cfg(target_arch = "wasm32")] { const fn default_break_chars(c: char) -> bool { false } const ESCAPE_CHAR: Option = None; const fn double_quotes_special_chars(c: char) -> bool { false } } } /// Kind of quote. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum Quote { /// Double quote: `"` Double, /// Single quote: `'` Single, /// No quote None, } impl FilenameCompleter { /// Constructor #[must_use] pub fn new() -> Self { Self { break_chars: default_break_chars, double_quotes_special_chars, } } /// Takes the currently edited `line` with the cursor `pos`ition and /// returns the start position and the completion candidates for the /// partial path to be completed. pub fn complete_path(&self, line: &str, pos: usize) -> Result<(usize, Vec)> { let (start, mut matches) = self.complete_path_unsorted(line, pos)?; #[allow(clippy::unnecessary_sort_by)] matches.sort_by(|a, b| a.display().cmp(b.display())); Ok((start, matches)) } /// Similar to [`Self::complete_path`], but the returned paths are unsorted. pub fn complete_path_unsorted(&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 #[must_use] 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. #[must_use] pub fn escape( mut input: String, esc_char: Option, is_break_char: fn(char) -> bool, quote: Quote, ) -> String { if quote == Quote::Single { return input; // no escape in single quotes } let n = input.chars().filter(|c| is_break_char(*c)).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 is_break_char(c) { result.push(esc_char); } result.push(c); } result } fn filename_complete( path: &str, esc_char: Option, is_break_char: fn(char) -> bool, quote: Quote, ) -> Vec { #[cfg(feature = "with-dirs")] use home::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 entries; } // if any of the below IO operations have errors, just ignore them if let Ok(read_dir) = dir.read_dir() { let file_name = normalize(file_name); for entry in read_dir.flatten() { if let Some(s) = entry.file_name().to_str() { let ns = normalize(s); if ns.starts_with(file_name.as_ref()) { if let Ok(metadata) = fs::metadata(entry.path()) { let mut path = String::from(dir_name) + s; if metadata.is_dir() { path.push(sep); } entries.push(Pair { display: String::from(s), replacement: escape(path, esc_char, is_break_char, quote), }); } // else ignore PermissionDenied } } } } entries } #[cfg(any(windows, target_os = "macos"))] fn normalize(s: &str) -> Cow { // case insensitive Owned(s.to_lowercase()) } #[cfg(not(any(windows, target_os = "macos")))] fn normalize(s: &str) -> Cow { Cow::Borrowed(s) } /// Given a `line` and a cursor `pos`ition, /// 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. #[must_use] pub fn extract_word( line: &str, pos: usize, esc_char: Option, is_break_char: fn(char) -> bool, ) -> (usize, &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; } break; } if is_break_char(c) { 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), } } /// Returns the longest common prefix among all `Candidate::replacement()`s. pub fn longest_common_prefix(candidates: &[C]) -> Option<&str> { if candidates.is_empty() { return None; } else if candidates.len() == 1 { return Some(candidates[0].replacement()); } 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(Eq, 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 = 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 = 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); { let lcp = super::longest_common_prefix(&candidates); assert_eq!(Some(s), lcp); } let c2 = String::from("Users"); candidates.push(c2); { let lcp = super::longest_common_prefix(&candidates); assert_eq!(Some(s), lcp); } let c3 = String::from(""); candidates.push(c3); { 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\\") ) } #[cfg(windows)] #[test] pub fn normalize() { assert_eq!(super::normalize("Windows"), "windows") } } rustyline-13.0.0/src/config.rs000064400000000000000000000375321046102023000143570ustar 00000000000000//! Customize line editor use crate::Result; 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 stdio or not behavior: Behavior, /// Horizontal space taken by a tab. tab_stop: usize, /// Indentation size for indent/dedent commands indent_size: usize, /// Check if cursor position is at leftmost before displaying prompt check_cursor_position: bool, /// Bracketed paste on unix platform enable_bracketed_paste: bool, } impl Config { /// Returns a `Config` builder. #[must_use] pub fn builder() -> Builder { Builder::new() } /// Tell the maximum length (i.e. number of entries) for the history. #[must_use] 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. #[must_use] 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. #[must_use] 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; } /// Completion behaviour. /// /// By default, `CompletionType::Circular`. #[must_use] pub fn completion_type(&self) -> CompletionType { self.completion_type } /// When listing completion alternatives, only display /// one screen of possibilities at a time (used for `CompletionType::List` /// mode). #[must_use] pub fn completion_prompt_limit(&self) -> usize { self.completion_prompt_limit } /// Duration (milliseconds) Rustyline will wait for a character when /// reading an ambiguous key sequence (used for `EditMode::Vi` mode on unix /// platform). /// /// By default, no timeout (-1) or 500ms if `EditMode::Vi` is activated. #[must_use] pub fn keyseq_timeout(&self) -> i32 { self.keyseq_timeout } /// Emacs or Vi mode #[must_use] pub fn edit_mode(&self) -> EditMode { self.edit_mode } /// Tell if lines are automatically added to the history. /// /// By default, they are not. #[must_use] pub fn auto_add_history(&self) -> bool { self.auto_add_history } /// Bell style: beep, flash or nothing. #[must_use] 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. #[must_use] 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; } /// Whether to use stdio or not /// /// By default, stdio is used. #[must_use] pub fn behavior(&self) -> Behavior { self.behavior } pub(crate) fn set_behavior(&mut self, behavior: Behavior) { self.behavior = behavior; } /// Horizontal space taken by a tab. /// /// By default, 8. #[must_use] 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; } /// Check if cursor position is at leftmost before displaying prompt. /// /// By default, we don't check. #[must_use] pub fn check_cursor_position(&self) -> bool { self.check_cursor_position } /// Indentation size used by indentation commands /// /// By default, 2. #[must_use] pub fn indent_size(&self) -> usize { self.indent_size } pub(crate) fn set_indent_size(&mut self, indent_size: usize) { self.indent_size = indent_size; } /// Bracketed paste on unix platform /// /// By default, it's enabled. #[must_use] pub fn enable_bracketed_paste(&self) -> bool { self.enable_bracketed_paste } } 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, behavior: Behavior::default(), tab_stop: 8, indent_size: 2, check_cursor_position: false, enable_bracketed_paste: true, } } } /// 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 (overridden 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 } } /// History filter #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum HistoryDuplicates { /// No filter AlwaysAdd, /// a line will not be added to the history if it matches the previous entry IgnoreConsecutive, } /// Tab completion style #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum CompletionType { /// Complete the next full match (like in Vim by default) Circular, /// Complete till longest match. /// When more than one match, list all matches /// (like in Bash/Readline). List, /// Complete the match using fuzzy search and selection /// (like fzf and plugins) /// Currently only available for unix platforms as dependency on /// skim->tuikit Compile with `--features=fuzzy` to enable #[cfg(all(unix, feature = "with-fuzzy"))] Fuzzy, } /// Style of editing / Standard keymaps #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum EditMode { /// Emacs keymap Emacs, /// Vi keymap Vi, } /// Colorization mode #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum ColorMode { /// Activate highlighting if platform/terminal is supported. Enabled, /// Activate highlighting even if platform is not supported (windows < 10). Forced, /// Deactivate highlighting even if platform/terminal is supported. Disabled, } /// Should the editor use stdio #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] #[non_exhaustive] pub enum Behavior { /// Use stdin / stdout #[default] Stdio, /// Use terminal-style interaction whenever possible, even if 'stdin' and/or /// 'stdout' are not terminals. PreferTerm, // TODO // Use file-style interaction, reading input from the given file. // useFile } /// Configuration builder #[derive(Clone, Debug, Default)] pub struct Builder { p: Config, } impl Builder { /// Returns a `Config` builder. #[must_use] 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) -> Result { self.set_max_history_size(max_size)?; Ok(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) -> Result { self.set_history_ignore_dups(yes)?; Ok(self) } /// Tell if lines which begin with a space character are saved or not in /// the history list. /// /// By default, they are saved. #[must_use] pub fn history_ignore_space(mut self, yes: bool) -> Self { self.set_history_ignore_space(yes); self } /// Set `completion_type`. #[must_use] 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. #[must_use] 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. #[must_use] 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. #[must_use] 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. #[must_use] pub fn auto_add_history(mut self, yes: bool) -> Self { self.set_auto_add_history(yes); self } /// Set bell style: beep, flash or nothing. #[must_use] 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. #[must_use] pub fn color_mode(mut self, color_mode: ColorMode) -> Self { self.set_color_mode(color_mode); self } /// Whether to use stdio or not /// /// By default, stdio is used. #[must_use] pub fn behavior(mut self, behavior: Behavior) -> Self { self.set_behavior(behavior); self } /// Horizontal space taken by a tab. /// /// By default, `8` #[must_use] pub fn tab_stop(mut self, tab_stop: usize) -> Self { self.set_tab_stop(tab_stop); self } /// Check if cursor position is at leftmost before displaying prompt. /// /// By default, we don't check. #[must_use] pub fn check_cursor_position(mut self, yes: bool) -> Self { self.set_check_cursor_position(yes); self } /// Indentation size /// /// By default, `2` #[must_use] pub fn indent_size(mut self, indent_size: usize) -> Self { self.set_indent_size(indent_size); self } /// Enable or disable bracketed paste on unix platform /// /// By default, it's enabled. #[must_use] pub fn bracketed_paste(mut self, enabled: bool) -> Self { self.enable_bracketed_paste(enabled); self } /// Builds a `Config` with the settings specified so far. #[must_use] pub fn build(self) -> Config { self.p } } impl Configurer for Builder { fn config_mut(&mut self) -> &mut Config { &mut self.p } } /// Trait for component that holds a `Config`. pub trait Configurer { /// `Config` accessor. fn config_mut(&mut self) -> &mut Config; /// Set the maximum length for the history. fn set_max_history_size(&mut self, max_size: usize) -> Result<()> { self.config_mut().set_max_history_size(max_size); Ok(()) } /// 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) -> Result<()> { self.config_mut().set_history_ignore_dups(yes); Ok(()) } /// 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 stdio or not /// /// By default, stdio is used. fn set_behavior(&mut self, behavior: Behavior) { self.config_mut().set_behavior(behavior); } /// 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); } /// Check if cursor position is at leftmost before displaying prompt. /// /// By default, we don't check. fn set_check_cursor_position(&mut self, yes: bool) { self.config_mut().check_cursor_position = yes; } /// Indentation size for indent/dedent commands /// /// By default, `2` fn set_indent_size(&mut self, size: usize) { self.config_mut().set_indent_size(size); } /// Enable or disable bracketed paste on unix platform /// /// By default, it's enabled. fn enable_bracketed_paste(&mut self, enabled: bool) { self.config_mut().enable_bracketed_paste = enabled; } } rustyline-13.0.0/src/edit.rs000064400000000000000000000627731046102023000140440ustar 00000000000000//! Command processor use log::debug; use std::fmt; use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthChar; use super::{Context, Helper, Result}; use crate::error::ReadlineError; use crate::highlight::Highlighter; use crate::hint::Hint; use crate::history::SearchDirection; 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::{ ChangeListener, DeleteListener, Direction, LineBuffer, NoListener, WordAction, MAX_LINE, }; use crate::tty::{Renderer, Term, Terminal}; use crate::undo::Changeset; use crate::validate::{ValidationContext, ValidationResult}; use crate::KillRing; /// 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: Changeset, // 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 pub highlight_char: bool, // `true` if a char has been highlighted pub forced_refresh: bool, // `true` if line is redraw without hint or highlight_char } 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: Changeset::new(), helper, ctx, hint: None, highlight_char: false, forced_refresh: 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, ignore_external_print: bool, ) -> Result { loop { let rc = input_state.next_cmd(rdr, self, single_esc_abort, ignore_external_print); if let Err(ReadlineError::WindowResized) = rc { debug!(target: "rustyline", "SIGWINCH"); let old_cols = self.out.get_columns(); self.out.update_size(); let new_cols = self.out.get_columns(); if new_cols != old_cols && (self.layout.end.row > 0 || self.layout.end.col >= new_cols) { self.prompt_size = self .out .calculate_position(self.prompt, Position::default()); self.refresh_line()?; } continue; } if let Ok(Cmd::Replace(..)) = rc { self.changes.begin(); } return rc; } } pub fn backup(&mut self) { self.saved_line_for_history .update(self.line.as_str(), self.line.pos(), &mut NoListener); } pub fn restore(&mut self) { self.line.update( self.saved_line_for_history.as_str(), self.saved_line_for_history.pos(), &mut self.changes, ); } 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_to_end(&mut self) -> Result<()> { if self.layout.cursor == self.layout.end { return Ok(()); } self.out.move_cursor(self.layout.cursor, self.layout.end)?; self.layout.cursor = self.layout.end; Ok(()) } pub fn move_cursor_at_leftmost(&mut self, rdr: &mut ::Reader) -> Result<()> { self.out.move_cursor_at_leftmost(rdr) } fn refresh( &mut self, prompt: &str, prompt_size: Position, default_prompt: bool, info: Info<'_>, ) -> Result<()> { let info = match info { Info::NoHint => None, Info::Hint => self.hint.as_ref().map(|h| h.display()), Info::Msg(msg) => msg, }; let highlighter = if self.out.colors_enabled() { self.helper.map(|h| h as &dyn Highlighter) } else { None }; let new_layout = self .out .compute_layout(prompt_size, default_prompt, &self.line, info); debug!(target: "rustyline", "old layout: {:?}", self.layout); debug!(target: "rustyline", "new layout: {:?}", new_layout); self.out.refresh_line( prompt, &self.line, 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 = match hint { Some(val) if !val.display().is_empty() => Some(Box::new(val) as Box), _ => None, }; } 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(), self.forced_refresh); 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.begin(); let result = validator.validate(&mut ValidationContext::new(self))?; let corrected = self.changes.end(); match result { ValidationResult::Incomplete => {} ValidationResult::Valid(ref msg) => { // Accept the line regardless of where the cursor is. if corrected || self.has_hint() || msg.is_some() { // Force a refresh without hints to leave the previous // line as the user typed it after a newline. self.refresh_line_with_msg(msg.as_deref())?; } } ValidationResult::Invalid(ref msg) => { if corrected || self.has_hint() || msg.is_some() { self.refresh_line_with_msg(msg.as_deref())?; } } } Ok(result) } else { Ok(ValidationResult::Valid(None)) } } } impl<'out, 'prompt, H: Helper> Invoke for State<'out, 'prompt, H> { fn input(&self) -> &str { self.line.as_str() } } impl<'out, 'prompt, H: Helper> Refresher for State<'out, 'prompt, H> { fn refresh_line(&mut self) -> Result<()> { let prompt_size = self.prompt_size; self.hint(); self.highlight_char(); self.refresh(self.prompt, prompt_size, true, Info::Hint) } fn refresh_line_with_msg(&mut self, msg: Option<&str>) -> Result<()> { let prompt_size = self.prompt_size; self.hint = None; self.highlight_char(); self.refresh(self.prompt, prompt_size, true, Info::Msg(msg)) } 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.begin(); } fn done_inserting(&mut self) { self.changes.end(); } fn last_insert(&self) -> Option { self.changes.last_insert() } fn is_cursor_at_end(&self) -> bool { self.line.pos() == self.line.len() } fn has_hint(&self) -> bool { self.hint.is_some() } fn hint_text(&self) -> Option<&str> { self.hint.as_ref().and_then(|hint| hint.completion()) } fn line(&self) -> &str { self.line.as_str() } fn pos(&self) -> usize { self.line.pos() } fn external_print(&mut self, msg: String) -> Result<()> { self.out.clear_rows(&self.layout)?; self.layout.end.row = 0; self.layout.cursor.row = 0; self.out.write_and_flush(msg.as_str())?; if !msg.ends_with('\n') { self.out.write_and_flush("\n")?; } self.refresh_line() } } 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, &mut self.changes) { 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); 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.begin(); let succeed = if let Some(chars) = self.line.delete(n, &mut self.changes) { let count = chars.graphemes(true).count(); self.line.insert(ch, count, &mut self.changes); self.line.move_backward(1); true } else { false }; self.changes.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, &mut self.changes); } 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, &mut self.changes).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.begin(); let result = if self .line .yank_pop(yank_size, text, &mut self.changes) .is_some() { self.refresh_line() } else { Ok(()) }; self.changes.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(()) } } /// Move cursor to the start of the buffer. pub fn edit_move_buffer_start(&mut self) -> Result<()> { if self.line.move_buffer_start() { self.move_cursor() } else { Ok(()) } } /// Move cursor to the end of the buffer. pub fn edit_move_buffer_end(&mut self) -> Result<()> { if self.line.move_buffer_end() { self.move_cursor() } else { Ok(()) } } pub fn edit_kill(&mut self, mvt: &Movement, kill_ring: &mut KillRing) -> Result<()> { struct Proxy<'p>(&'p mut Changeset, &'p mut KillRing); let mut proxy = Proxy(&mut self.changes, kill_ring); impl DeleteListener for Proxy<'_> { fn start_killing(&mut self) { self.1.start_killing(); } fn delete(&mut self, idx: usize, string: &str, dir: Direction) { self.0.delete(idx, string); self.1.delete(idx, string, dir); } fn stop_killing(&mut self) { self.1.stop_killing() } } impl ChangeListener for Proxy<'_> { fn insert_char(&mut self, idx: usize, c: char) { self.0.insert_char(idx, c) } fn insert_str(&mut self, idx: usize, string: &str) { self.0.insert_str(idx, string) } fn replace(&mut self, idx: usize, old: &str, new: &str) { self.0.replace(idx, old, new) } } if self.line.kill(mvt, &mut proxy) { 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, &mut self.changes); self.refresh_line() } /// Exchange the char before cursor with the character at cursor. pub fn edit_transpose_chars(&mut self) -> Result<()> { self.changes.begin(); let succeed = self.line.transpose_chars(&mut self.changes); self.changes.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(()) } } /// Moves the cursor to the same column in the line above pub fn edit_move_line_up(&mut self, n: RepeatCount) -> Result { if self.line.move_to_line_up(n) { self.move_cursor()?; Ok(true) } else { Ok(false) } } /// Moves the cursor to the same column in the line above pub fn edit_move_line_down(&mut self, n: RepeatCount) -> Result { if self.line.move_to_line_down(n) { self.move_cursor()?; Ok(true) } else { Ok(false) } } pub fn edit_move_to(&mut self, cs: CharSearch, n: RepeatCount) -> Result<()> { if self.line.move_to(cs, n) { self.move_cursor() } else { Ok(()) } } pub fn edit_word(&mut self, a: WordAction) -> Result<()> { self.changes.begin(); let succeed = self.line.edit_word(a, &mut self.changes); self.changes.end(); if succeed { self.refresh_line() } else { Ok(()) } } pub fn edit_transpose_words(&mut self, n: RepeatCount) -> Result<()> { self.changes.begin(); let succeed = self.line.transpose_words(n, &mut self.changes); self.changes.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(()); } let (idx, dir) = if prev { (self.ctx.history_index - 1, SearchDirection::Reverse) } else { self.ctx.history_index += 1; (self.ctx.history_index, SearchDirection::Forward) }; if idx < history.len() { if let Some(r) = history.get(idx, dir)? { let buf = r.entry; self.ctx.history_index = r.idx; self.changes.begin(); self.line.update(&buf, buf.len(), &mut self.changes); self.changes.end(); } else { return Ok(()); } } else { // Restore current edited line self.restore(); } self.refresh_line() } // Non-incremental, anchored search pub fn edit_history_search(&mut self, dir: SearchDirection) -> Result<()> { let history = self.ctx.history; if history.is_empty() { return self.out.beep(); } if self.ctx.history_index == history.len() && dir == SearchDirection::Forward || self.ctx.history_index == 0 && dir == SearchDirection::Reverse { return self.out.beep(); } if dir == SearchDirection::Reverse { self.ctx.history_index -= 1; } else { self.ctx.history_index += 1; } if let Some(sr) = history.starts_with( &self.line.as_str()[..self.line.pos()], self.ctx.history_index, dir, )? { self.ctx.history_index = sr.idx; self.changes.begin(); self.line.update(&sr.entry, sr.pos, &mut self.changes); self.changes.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 { if let Some(r) = history.get(0, SearchDirection::Forward)? { let buf = r.entry; self.ctx.history_index = r.idx; self.changes.begin(); self.line.update(&buf, buf.len(), &mut self.changes); self.changes.end(); } else { return Ok(()); } } else { self.ctx.history_index = history.len(); // Restore current edited line self.restore(); } self.refresh_line() } /// Change the indentation of the lines covered by movement pub fn edit_indent(&mut self, mvt: &Movement, amount: usize, dedent: bool) -> Result<()> { if self.line.indent(mvt, amount, dedent, &mut self.changes) { self.refresh_line() } else { Ok(()) } } } #[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::DefaultHistory, ) -> State<'out, 'static, H> { State { out, prompt: "", prompt_size: Position::default(), line: LineBuffer::init(line, pos), layout: Layout::default(), saved_line_for_history: LineBuffer::with_capacity(100), byte_buffer: [0; 4], changes: Changeset::new(), helper, ctx: Context::new(history), hint: Some(Box::new("hint".to_owned())), highlight_char: false, forced_refresh: false, } } #[cfg(test)] mod test { use super::init_state; use crate::history::{DefaultHistory, History}; use crate::tty::Sink; #[test] fn edit_history_next() { let mut out = Sink::default(); let mut history = DefaultHistory::new(); history.add("line0").unwrap(); history.add("line1").unwrap(); 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-13.0.0/src/error.rs000064400000000000000000000104271046102023000142350ustar 00000000000000//! Contains error type for handling I/O and Errno errors #[cfg(windows)] use std::char; use std::error::Error; use std::fmt; use std::io; /// The error type for Rustyline errors that can arise from /// I/O related errors or Errno when using the nix-rust library // #[non_exhaustive] #[allow(clippy::module_name_repetitions)] #[derive(Debug)] #[non_exhaustive] pub enum ReadlineError { /// I/O Error Io(io::Error), /// EOF (VEOF / Ctrl-D) Eof, /// Interrupt signal (VINTR / VQUIT / Ctrl-C) Interrupted, /// Unix Error from syscall #[cfg(unix)] Errno(nix::Error), /// Error generated on WINDOW_BUFFER_SIZE_EVENT / SIGWINCH signal WindowResized, /// Like Utf8Error on unix #[cfg(windows)] Decode(char::DecodeUtf16Error), /// Something went wrong calling a Windows API #[cfg(windows)] SystemError(clipboard_win::ErrorCode), /// Error related to SQLite history backend #[cfg(feature = "with-sqlite-history")] SQLiteError(rusqlite::Error), } 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::Errno(ref err) => err.fmt(f), ReadlineError::WindowResized => write!(f, "WindowResized"), #[cfg(windows)] ReadlineError::Decode(ref err) => err.fmt(f), #[cfg(windows)] ReadlineError::SystemError(ref err) => err.fmt(f), #[cfg(feature = "with-sqlite-history")] ReadlineError::SQLiteError(ref err) => err.fmt(f), } } } impl Error for ReadlineError { fn source(&self) -> Option<&(dyn Error + 'static)> { match *self { ReadlineError::Io(ref err) => Some(err), ReadlineError::Eof => None, ReadlineError::Interrupted => None, #[cfg(unix)] ReadlineError::Errno(ref err) => Some(err), ReadlineError::WindowResized => None, #[cfg(windows)] ReadlineError::Decode(ref err) => Some(err), #[cfg(windows)] ReadlineError::SystemError(_) => None, #[cfg(feature = "with-sqlite-history")] ReadlineError::SQLiteError(ref err) => Some(err), } } } #[cfg(unix)] #[derive(Debug)] pub(crate) struct WindowResizedError; #[cfg(unix)] impl fmt::Display for WindowResizedError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "WindowResized") } } #[cfg(unix)] impl Error for WindowResizedError {} impl From for ReadlineError { fn from(err: io::Error) -> Self { #[cfg(unix)] if err.kind() == io::ErrorKind::Interrupted { if let Some(e) = err.get_ref() { if e.downcast_ref::().is_some() { return ReadlineError::WindowResized; } } } 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::Io(io::Error::new(io::ErrorKind::InvalidData, err)) } } #[cfg(windows)] impl From for ReadlineError { fn from(err: std::string::FromUtf8Error) -> Self { ReadlineError::Io(io::Error::new(io::ErrorKind::InvalidData, err)) } } #[cfg(unix)] impl From for ReadlineError { fn from(err: fmt::Error) -> Self { ReadlineError::Io(io::Error::new(io::ErrorKind::Other, err)) } } #[cfg(windows)] impl From for ReadlineError { fn from(err: clipboard_win::ErrorCode) -> Self { ReadlineError::SystemError(err) } } #[cfg(feature = "with-sqlite-history")] impl From for ReadlineError { fn from(err: rusqlite::Error) -> Self { ReadlineError::SQLiteError(err) } } rustyline-13.0.0/src/highlight.rs000064400000000000000000000216451046102023000150570ustar 00000000000000//! Syntax highlighting use crate::config::CompletionType; 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, // FIXME should be Completer::Candidate 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. /// `forced` flag is `true` mainly when user presses Enter (i.e. transient /// vs final highlight). /// /// Used to optimize refresh when a character is inserted or the cursor is /// moved. fn highlight_char(&self, line: &str, pos: usize, forced: bool) -> bool { let _ = (line, pos, forced); 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, forced: bool) -> bool { (**self).highlight_char(line, pos, forced) } } // TODO versus https://python-prompt-toolkit.readthedocs.io/en/master/pages/reference.html?highlight=HighlightMatchingBracketProcessor#prompt_toolkit.layout.processors.HighlightMatchingBracketProcessor /// Highlight matching bracket when typed or cursor moved on. #[derive(Default)] pub struct MatchingBracketHighlighter { bracket: Cell>, // memorize the character to search... } impl MatchingBracketHighlighter { /// Constructor #[must_use] 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, forced: bool) -> bool { if forced { self.bracket.set(None); return false; } // 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) { return if pos == 0 { None } else { Some((b, pos)) }; } else if is_open_bracket(b) { return if pos + 1 == line.len() { None } else { Some((b, pos)) }; } else if under_cursor && pos > 0 { under_cursor = false; pos -= 1; // or before cursor } else { return None; } } } } const fn matching_bracket(bracket: u8) -> u8 { match bracket { b'{' => b'}', b'}' => b'{', b'[' => b']', b']' => b'[', b'(' => b')', b')' => b'(', b => b, } } const fn is_open_bracket(bracket: u8) -> bool { matches!(bracket, b'{' | b'[' | b'(') } const fn is_close_bracket(bracket: u8) -> bool { matches!(bracket, b'}' | b']' | b')') } #[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-13.0.0/src/hint.rs000064400000000000000000000051461046102023000140500ustar 00000000000000//! Hints (suggestions at the right of the prompt as you type). use crate::history::SearchDirection; use crate::Context; /// A hint returned by Hinter pub trait Hint { /// Text to display when hint is active fn display(&self) -> &str; /// Text to insert in line when right arrow is pressed fn completion(&self) -> Option<&str>; } impl Hint for String { fn display(&self) -> &str { self.as_str() } fn completion(&self) -> Option<&str> { Some(self.as_str()) } } /// Hints provider pub trait Hinter { /// Specific hint type type Hint: Hint + 'static; /// Takes the currently edited `line` with the cursor `pos`ition and /// returns the string that should be displayed or `None` /// if no hint is available for the text the user currently typed. // TODO Validate: called while editing line but not while moving cursor. fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option { let _ = (line, pos, ctx); None } } impl Hinter for () { type Hint = String; } impl<'r, H: ?Sized + Hinter> Hinter for &'r H { type Hint = H::Hint; fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option { (**self).hint(line, pos, ctx) } } /// Add suggestion based on previous history entries matching current user /// input. #[derive(Default)] pub struct HistoryHinter {} impl HistoryHinter { /// Create a new `HistoryHinter` pub fn new() -> HistoryHinter { HistoryHinter::default() } } impl Hinter for HistoryHinter { type Hint = String; fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option { if line.is_empty() || 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(sr) = ctx .history .starts_with(line, start, SearchDirection::Reverse) .unwrap_or(None) { if sr.entry == line { return None; } return Some(sr.entry[pos..].to_owned()); } None } } #[cfg(test)] mod test { use super::{Hinter, HistoryHinter}; use crate::history::DefaultHistory; use crate::Context; #[test] pub fn empty_history() { let history = DefaultHistory::new(); let ctx = Context::new(&history); let hinter = HistoryHinter {}; let hint = hinter.hint("test", 4, &ctx); assert_eq!(None, hint); } } rustyline-13.0.0/src/history.rs000064400000000000000000001004551046102023000146060ustar 00000000000000//! History API #[cfg(feature = "with-file-history")] use fd_lock::RwLock; #[cfg(feature = "with-file-history")] use log::{debug, warn}; use std::borrow::Cow; use std::collections::vec_deque; use std::collections::VecDeque; #[cfg(feature = "with-file-history")] use std::fs::{File, OpenOptions}; #[cfg(feature = "with-file-history")] use std::io::SeekFrom; #[cfg(feature = "with-file-history")] use std::iter::DoubleEndedIterator; use std::ops::Index; use std::path::Path; #[cfg(feature = "with-file-history")] use std::time::SystemTime; use super::Result; use crate::config::{Config, HistoryDuplicates}; /// Search direction #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum SearchDirection { /// Search history forward Forward, /// Search history backward Reverse, } /// History search result #[derive(Debug, Clone, Eq, PartialEq)] pub struct SearchResult<'a> { /// history entry pub entry: Cow<'a, str>, /// history index pub idx: usize, /// match position in `entry` pub pos: usize, } /// Interface for navigating/loading/storing history // TODO Split navigation part from backend part pub trait History { // TODO jline3: interface Entry { // int index(); // Instant time(); // String line(); // } // replxx: HistoryEntry { // std::string _timestamp; // std::string _text; // termwiz: fn get(&self, idx: HistoryIndex) -> Option>; /// Return the history entry at position `index`, starting from 0. /// /// `SearchDirection` is useful only for implementations without direct /// indexing. fn get(&self, index: usize, dir: SearchDirection) -> Result>; // termwiz: fn last(&self) -> Option; // jline3: default void add(String line) { // add(Instant.now(), line); // } // jline3: void add(Instant time, String line); // termwiz: fn add(&mut self, line: &str); // reedline: fn append(&mut self, entry: &str); /// Add a new entry in the history. fn add(&mut self, line: &str) -> Result; /// Add a new entry in the history. fn add_owned(&mut self, line: String) -> Result; // TODO check AsRef + Into vs object safe /// Return the number of entries in the history. #[must_use] fn len(&self) -> usize; /// Return true if the history has no entry. #[must_use] fn is_empty(&self) -> bool; // TODO jline3: int index(); // TODO jline3: String current(); // reedline: fn string_at_cursor(&self) -> Option; // TODO jline3: boolean previous(); // reedline: fn back(&mut self); // TODO jline3: boolean next(); // reedline: fn forward(&mut self); // TODO jline3: boolean moveToFirst(); // TODO jline3: boolean moveToFirst(); // TODO jline3: boolean moveToLast(); // TODO jline3: boolean moveTo(int index); // TODO jline3: void moveToEnd(); // TODO jline3: void resetIndex(); // TODO jline3: int first(); // TODO jline3: default boolean isPersistable(Entry entry) { // return true; // } /// 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://tiswww.case.edu/php/chet/readline/history.html#IDX11). fn set_max_len(&mut self, len: usize) -> Result<()>; /// Ignore consecutive duplicates fn ignore_dups(&mut self, yes: bool) -> Result<()>; /// Ignore lines which begin with a space or not fn ignore_space(&mut self, yes: bool); /// Save the history in the specified file. // TODO history_truncate_file // https://tiswww.case.edu/php/chet/readline/history.html#IDX31 fn save(&mut self, path: &Path) -> Result<()>; // FIXME Path vs AsRef /// Append new entries in the specified file. // Like [append_history](http://tiswww.case.edu/php/chet/readline/history.html#IDX30). fn append(&mut self, path: &Path) -> Result<()>; // FIXME Path vs AsRef /// Load the history from the specified file. /// /// # Errors /// Will return `Err` if path does not already exist or could not be read. fn load(&mut self, path: &Path) -> Result<()>; // FIXME Path vs AsRef /// Clear in-memory history fn clear(&mut self) -> Result<()>; // termwiz: fn search( // &self, // idx: HistoryIndex, // style: SearchStyle, // direction: SearchDirection, // pattern: &str, // ) -> Option; // reedline: fn set_navigation(&mut self, navigation: HistoryNavigationQuery); // reedline: fn get_navigation(&self) -> HistoryNavigationQuery; /// 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. fn search( &self, term: &str, start: usize, dir: SearchDirection, ) -> Result>; /// Anchored search fn starts_with( &self, term: &str, start: usize, dir: SearchDirection, ) -> Result>; /* TODO How ? DoubleEndedIterator may be difficult to implement (for an SQLite backend) /// Return a iterator. #[must_use] fn iter(&self) -> impl DoubleEndedIterator + '_; */ } /// Transient in-memory history implementation. pub struct MemHistory { entries: VecDeque, max_len: usize, ignore_space: bool, ignore_dups: bool, } impl MemHistory { /// Default constructor #[must_use] pub fn new() -> Self { Self::with_config(Config::default()) } /// Customized constructor with: /// - `Config::max_history_size()`, /// - `Config::history_ignore_space()`, /// - `Config::history_duplicates()`. #[must_use] 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, } } fn search_match( &self, term: &str, start: usize, dir: SearchDirection, test: F, ) -> Option where F: Fn(&str) -> Option, { if term.is_empty() || start >= self.len() { return None; } match dir { SearchDirection::Reverse => { for (idx, entry) in self .entries .iter() .rev() .skip(self.len() - 1 - start) .enumerate() { if let Some(cursor) = test(entry) { return Some(SearchResult { idx: start - idx, entry: Cow::Borrowed(entry), pos: cursor, }); } } None } SearchDirection::Forward => { for (idx, entry) in self.entries.iter().skip(start).enumerate() { if let Some(cursor) = test(entry) { return Some(SearchResult { idx: idx + start, entry: Cow::Borrowed(entry), pos: cursor, }); } } None } } } fn ignore(&self, line: &str) -> bool { if self.max_len == 0 { return true; } if line.is_empty() || (self.ignore_space && line.chars().next().map_or(true, char::is_whitespace)) { return true; } if self.ignore_dups { if let Some(s) = self.entries.back() { if s == line { return true; } } } false } fn insert(&mut self, line: String) { if self.entries.len() == self.max_len { self.entries.pop_front(); } self.entries.push_back(line); } } impl Default for MemHistory { fn default() -> Self { Self::new() } } impl History for MemHistory { fn get(&self, index: usize, _: SearchDirection) -> Result> { Ok(self .entries .get(index) .map(String::as_ref) .map(Cow::Borrowed) .map(|entry| SearchResult { entry, idx: index, pos: 0, })) } fn add(&mut self, line: &str) -> Result { if self.ignore(line) { return Ok(false); } self.insert(line.to_owned()); Ok(true) } fn add_owned(&mut self, line: String) -> Result { if self.ignore(&line) { return Ok(false); } self.insert(line); Ok(true) } fn len(&self) -> usize { self.entries.len() } fn is_empty(&self) -> bool { self.entries.is_empty() } fn set_max_len(&mut self, len: usize) -> Result<()> { self.max_len = len; if self.len() > len { self.entries.drain(..self.len() - len); } Ok(()) } fn ignore_dups(&mut self, yes: bool) -> Result<()> { self.ignore_dups = yes; Ok(()) } fn ignore_space(&mut self, yes: bool) { self.ignore_space = yes; } fn save(&mut self, _: &Path) -> Result<()> { unimplemented!(); } fn append(&mut self, _: &Path) -> Result<()> { unimplemented!(); } fn load(&mut self, _: &Path) -> Result<()> { unimplemented!(); } fn clear(&mut self) -> Result<()> { self.entries.clear(); Ok(()) } fn search( &self, term: &str, start: usize, dir: SearchDirection, ) -> Result> { #[cfg(not(feature = "case_insensitive_history_search"))] { let test = |entry: &str| entry.find(term); Ok(self.search_match(term, start, dir, test)) } #[cfg(feature = "case_insensitive_history_search")] { use regex::{escape, RegexBuilder}; Ok( if let Ok(re) = RegexBuilder::new(&escape(term)) .case_insensitive(true) .build() { let test = |entry: &str| re.find(entry).map(|m| m.start()); self.search_match(term, start, dir, test) } else { None }, ) } } fn starts_with( &self, term: &str, start: usize, dir: SearchDirection, ) -> Result> { #[cfg(not(feature = "case_insensitive_history_search"))] { let test = |entry: &str| { if entry.starts_with(term) { Some(term.len()) } else { None } }; Ok(self.search_match(term, start, dir, test)) } #[cfg(feature = "case_insensitive_history_search")] { use regex::{escape, RegexBuilder}; Ok( if let Ok(re) = RegexBuilder::new(&escape(term)) .case_insensitive(true) .build() { let test = |entry: &str| { re.find(entry) .and_then(|m| if m.start() == 0 { Some(m) } else { None }) .map(|m| m.end()) }; self.search_match(term, start, dir, test) } else { None }, ) } } } impl Index for MemHistory { type Output = String; fn index(&self, index: usize) -> &String { &self.entries[index] } } impl<'a> IntoIterator for &'a MemHistory { type IntoIter = vec_deque::Iter<'a, String>; type Item = &'a String; fn into_iter(self) -> Self::IntoIter { self.entries.iter() } } /// Current state of the history stored in a file. #[derive(Default)] #[cfg(feature = "with-file-history")] pub struct FileHistory { mem: MemHistory, /// Number of entries inputted by user and not saved yet new_entries: usize, /// last path used by either `load` or `save` path_info: Option, } // TODO impl Deref for FileHistory ? /// Last histo path, modified timestamp and size #[cfg(feature = "with-file-history")] struct PathInfo(std::path::PathBuf, SystemTime, usize); #[cfg(feature = "with-file-history")] impl FileHistory { // New multiline-aware history files start with `#V2\n` and have newlines // and backslashes escaped in them. const FILE_VERSION_V2: &'static str = "#V2"; /// Default constructor #[must_use] pub fn new() -> Self { Self::with_config(Config::default()) } /// Customized constructor with: /// - `Config::max_history_size()`, /// - `Config::history_ignore_space()`, /// - `Config::history_duplicates()`. #[must_use] pub fn with_config(config: Config) -> Self { Self { mem: MemHistory::with_config(config), new_entries: 0, path_info: None, } } fn save_to(&mut self, file: &File, append: bool) -> Result<()> { use std::io::{BufWriter, Write}; fix_perm(file); let mut wtr = BufWriter::new(file); let first_new_entry = if append { self.mem.len().saturating_sub(self.new_entries) } else { wtr.write_all(Self::FILE_VERSION_V2.as_bytes())?; wtr.write_all(b"\n")?; 0 }; for entry in self.mem.entries.iter().skip(first_new_entry) { let mut bytes = entry.as_bytes(); while let Some(i) = memchr::memchr2(b'\\', b'\n', bytes) { let (head, tail) = bytes.split_at(i); wtr.write_all(head)?; let (&escapable_byte, tail) = tail .split_first() .expect("memchr guarantees i is a valid index"); if escapable_byte == b'\n' { wtr.write_all(br"\n")?; // escaped line feed } else { debug_assert_eq!(escapable_byte, b'\\'); wtr.write_all(br"\\")?; // escaped backslash } bytes = tail; } wtr.write_all(bytes)?; // remaining bytes with no \n or \ wtr.write_all(b"\n")?; } // https://github.com/rust-lang/rust/issues/32677#issuecomment-204833485 wtr.flush()?; Ok(()) } fn load_from(&mut self, file: &File) -> Result { use std::io::{BufRead, BufReader}; let rdr = BufReader::new(file); let mut lines = rdr.lines(); let mut v2 = false; if let Some(first) = lines.next() { let line = first?; if line == Self::FILE_VERSION_V2 { v2 = true; } else { self.add_owned(line)?; } } let mut appendable = v2; for line in lines { let mut line = line?; if line.is_empty() { continue; } if v2 { let mut copy = None; // lazily copy line if unescaping is needed let mut str = line.as_str(); while let Some(i) = str.find('\\') { if copy.is_none() { copy = Some(String::with_capacity(line.len())); } let s = copy.as_mut().unwrap(); s.push_str(&str[..i]); let j = i + 1; // escaped char idx let b = if j < str.len() { str.as_bytes()[j] } else { 0 // unexpected if History::save works properly }; match b { b'n' => { s.push('\n'); // unescaped line feed } b'\\' => { s.push('\\'); // unescaped back slash } _ => { // only line feed and back slash should have been escaped warn!(target: "rustyline", "bad escaped line: {}", line); copy = None; break; } } str = &str[j + 1..]; } if let Some(mut s) = copy { s.push_str(str); // remaining bytes with no escaped char line = s; } } appendable &= self.add_owned(line)?; // TODO truncate to MAX_LINE } self.new_entries = 0; // TODO we may lost new entries if loaded lines < max_len Ok(appendable) } fn update_path(&mut self, path: &Path, file: &File, size: usize) -> Result<()> { let modified = file.metadata()?.modified()?; if let Some(PathInfo( ref mut previous_path, ref mut previous_modified, ref mut previous_size, )) = self.path_info { if previous_path.as_path() != path { *previous_path = path.to_owned(); } *previous_modified = modified; *previous_size = size; } else { self.path_info = Some(PathInfo(path.to_owned(), modified, size)); } debug!(target: "rustyline", "PathInfo({:?}, {:?}, {})", path, modified, size); Ok(()) } fn can_just_append(&self, path: &Path, file: &File) -> Result { if let Some(PathInfo(ref previous_path, ref previous_modified, ref previous_size)) = self.path_info { if previous_path.as_path() != path { debug!(target: "rustyline", "cannot append: {:?} <> {:?}", previous_path, path); return Ok(false); } let modified = file.metadata()?.modified()?; if *previous_modified != modified || self.mem.max_len <= *previous_size || self.mem.max_len < (*previous_size).saturating_add(self.new_entries) { debug!(target: "rustyline", "cannot append: {:?} < {:?} or {} < {} + {}", previous_modified, modified, self.mem.max_len, previous_size, self.new_entries); Ok(false) } else { Ok(true) } } else { Ok(false) } } /// Return a forward iterator. #[must_use] pub fn iter(&self) -> impl DoubleEndedIterator + '_ { self.mem.entries.iter() } } /// Default transient in-memory history implementation #[cfg(not(feature = "with-file-history"))] pub type DefaultHistory = MemHistory; /// Default file-based history implementation #[cfg(feature = "with-file-history")] pub type DefaultHistory = FileHistory; #[cfg(feature = "with-file-history")] impl History for FileHistory { fn get(&self, index: usize, dir: SearchDirection) -> Result> { self.mem.get(index, dir) } fn add(&mut self, line: &str) -> Result { if self.mem.add(line)? { self.new_entries = self.new_entries.saturating_add(1).min(self.len()); Ok(true) } else { Ok(false) } } fn add_owned(&mut self, line: String) -> Result { if self.mem.add_owned(line)? { self.new_entries = self.new_entries.saturating_add(1).min(self.len()); Ok(true) } else { Ok(false) } } fn len(&self) -> usize { self.mem.len() } fn is_empty(&self) -> bool { self.mem.is_empty() } fn set_max_len(&mut self, len: usize) -> Result<()> { self.mem.set_max_len(len)?; self.new_entries = self.new_entries.min(len); Ok(()) } fn ignore_dups(&mut self, yes: bool) -> Result<()> { self.mem.ignore_dups(yes) } fn ignore_space(&mut self, yes: bool) { self.mem.ignore_space(yes); } fn save(&mut self, path: &Path) -> Result<()> { if self.is_empty() || self.new_entries == 0 { return Ok(()); } let old_umask = umask(); let f = File::create(path); restore_umask(old_umask); let file = f?; let mut lock = RwLock::new(file); let lock_guard = lock.write()?; self.save_to(&lock_guard, false)?; self.new_entries = 0; self.update_path(path, &lock_guard, self.len()) } fn append(&mut self, path: &Path) -> Result<()> { use std::io::Seek; if self.is_empty() || self.new_entries == 0 { return Ok(()); } if !path.exists() || self.new_entries == self.mem.max_len { return self.save(path); } let file = OpenOptions::new().write(true).read(true).open(path)?; let mut lock = RwLock::new(file); let mut lock_guard = lock.write()?; if self.can_just_append(path, &lock_guard)? { lock_guard.seek(SeekFrom::End(0))?; self.save_to(&lock_guard, true)?; let size = self .path_info .as_ref() .unwrap() .2 .saturating_add(self.new_entries); self.new_entries = 0; return self.update_path(path, &lock_guard, size); } // we may need to truncate file before appending new entries let mut other = Self { mem: MemHistory { entries: VecDeque::new(), max_len: self.mem.max_len, ignore_space: self.mem.ignore_space, ignore_dups: self.mem.ignore_dups, }, new_entries: 0, path_info: None, }; other.load_from(&lock_guard)?; let first_new_entry = self.mem.len().saturating_sub(self.new_entries); for entry in self.mem.entries.iter().skip(first_new_entry) { other.add(entry)?; } lock_guard.seek(SeekFrom::Start(0))?; lock_guard.set_len(0)?; // if new size < old size other.save_to(&lock_guard, false)?; self.update_path(path, &lock_guard, other.len())?; self.new_entries = 0; Ok(()) } fn load(&mut self, path: &Path) -> Result<()> { let file = File::open(path)?; let lock = RwLock::new(file); let lock_guard = lock.read()?; let len = self.len(); if self.load_from(&lock_guard)? { self.update_path(path, &lock_guard, self.len() - len) } else { // discard old version on next save self.path_info = None; Ok(()) } } fn clear(&mut self) -> Result<()> { self.mem.clear()?; self.new_entries = 0; Ok(()) } fn search( &self, term: &str, start: usize, dir: SearchDirection, ) -> Result> { self.mem.search(term, start, dir) } fn starts_with( &self, term: &str, start: usize, dir: SearchDirection, ) -> Result> { self.mem.starts_with(term, start, dir) } } #[cfg(feature = "with-file-history")] impl Index for FileHistory { type Output = String; fn index(&self, index: usize) -> &String { &self.mem.entries[index] } } #[cfg(feature = "with-file-history")] impl<'a> IntoIterator for &'a FileHistory { type IntoIter = vec_deque::Iter<'a, String>; type Item = &'a String; fn into_iter(self) -> Self::IntoIter { self.mem.entries.iter() } } #[cfg(feature = "with-file-history")] 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)] { use nix::sys::stat::{self, Mode, fchmod}; fn umask() -> Mode { stat::umask(Mode::S_IXUSR | Mode::S_IRWXG | Mode::S_IRWXO) } fn restore_umask(old_umask: Mode) { stat::umask(old_umask); } fn fix_perm(file: &File) { use std::os::unix::io::AsRawFd; let _ = fchmod(file.as_raw_fd(), Mode::S_IRUSR | Mode::S_IWUSR); } } } #[cfg(test)] mod tests { use super::{DefaultHistory, History, SearchDirection, SearchResult}; use crate::config::Config; use crate::Result; fn init() -> DefaultHistory { let mut history = DefaultHistory::new(); assert!(history.add("line1").unwrap()); assert!(history.add("line2").unwrap()); assert!(history.add("line3").unwrap()); history } #[test] fn new() { let history = DefaultHistory::new(); assert_eq!(0, history.len()); } #[test] fn add() { let config = Config::builder().history_ignore_space(true).build(); let mut history = DefaultHistory::with_config(config); #[cfg(feature = "with-file-history")] assert_eq!(config.max_history_size(), history.mem.max_len); assert!(history.add("line1").unwrap()); assert!(history.add("line2").unwrap()); assert!(!history.add("line2").unwrap()); assert!(!history.add("").unwrap()); assert!(!history.add(" line3").unwrap()); } #[test] fn set_max_len() { let mut history = init(); history.set_max_len(1).unwrap(); assert_eq!(1, history.len()); assert_eq!(Some(&"line3".to_owned()), history.into_iter().last()); } #[test] #[cfg(feature = "with-file-history")] #[cfg_attr(miri, ignore)] // unsupported operation: `getcwd` not available when isolation is enabled fn save() -> Result<()> { check_save("line\nfour \\ abc") } #[test] #[cfg(feature = "with-file-history")] #[cfg_attr(miri, ignore)] // unsupported operation: `open` not available when isolation is enabled fn save_windows_path() -> Result<()> { let path = "cd source\\repos\\forks\\nushell\\"; check_save(path) } #[cfg(feature = "with-file-history")] fn check_save(line: &str) -> Result<()> { let mut history = init(); assert!(history.add(line)?); let tf = tempfile::NamedTempFile::new()?; history.save(tf.path())?; let mut history2 = DefaultHistory::new(); history2.load(tf.path())?; for (a, b) in history.iter().zip(history2.iter()) { assert_eq!(a, b); } tf.close()?; Ok(()) } #[test] #[cfg(feature = "with-file-history")] #[cfg_attr(miri, ignore)] // unsupported operation: `getcwd` not available when isolation is enabled fn load_legacy() -> Result<()> { use std::io::Write; let tf = tempfile::NamedTempFile::new()?; { let mut legacy = std::fs::File::create(tf.path())?; // Some data we'd accidentally corrupt if we got the version wrong let data = b"\ test\\n \\abc \\123\n\ 123\\n\\\\n\n\ abcde "; legacy.write_all(data)?; legacy.flush()?; } let mut history = DefaultHistory::new(); history.load(tf.path())?; assert_eq!(history[0], "test\\n \\abc \\123"); assert_eq!(history[1], "123\\n\\\\n"); assert_eq!(history[2], "abcde"); tf.close()?; Ok(()) } #[test] #[cfg(feature = "with-file-history")] #[cfg_attr(miri, ignore)] // unsupported operation: `getcwd` not available when isolation is enabled fn append() -> Result<()> { let mut history = init(); let tf = tempfile::NamedTempFile::new()?; history.append(tf.path())?; let mut history2 = DefaultHistory::new(); history2.load(tf.path())?; history2.add("line4")?; history2.append(tf.path())?; history.add("line5")?; history.append(tf.path())?; let mut history3 = DefaultHistory::new(); history3.load(tf.path())?; assert_eq!(history3.len(), 5); tf.close()?; Ok(()) } #[test] #[cfg(feature = "with-file-history")] #[cfg_attr(miri, ignore)] // unsupported operation: `getcwd` not available when isolation is enabled fn truncate() -> Result<()> { let tf = tempfile::NamedTempFile::new()?; let config = Config::builder().history_ignore_dups(false)?.build(); let mut history = DefaultHistory::with_config(config); history.add("line1")?; history.add("line1")?; history.append(tf.path())?; let mut history = DefaultHistory::new(); history.load(tf.path())?; history.add("l")?; history.append(tf.path())?; let mut history = DefaultHistory::new(); history.load(tf.path())?; assert_eq!(history.len(), 2); assert_eq!(history[1], "l"); tf.close()?; Ok(()) } #[test] fn search() -> Result<()> { let history = init(); assert_eq!(None, history.search("", 0, SearchDirection::Forward)?); assert_eq!(None, history.search("none", 0, SearchDirection::Forward)?); assert_eq!(None, history.search("line", 3, SearchDirection::Forward)?); assert_eq!( Some(SearchResult { idx: 0, entry: history.get(0, SearchDirection::Forward)?.unwrap().entry, pos: 0 }), history.search("line", 0, SearchDirection::Forward)? ); assert_eq!( Some(SearchResult { idx: 1, entry: history.get(1, SearchDirection::Forward)?.unwrap().entry, pos: 0 }), history.search("line", 1, SearchDirection::Forward)? ); assert_eq!( Some(SearchResult { idx: 2, entry: history.get(2, SearchDirection::Forward)?.unwrap().entry, pos: 0 }), history.search("line3", 1, SearchDirection::Forward)? ); Ok(()) } #[test] fn reverse_search() -> Result<()> { let history = init(); assert_eq!(None, history.search("", 2, SearchDirection::Reverse)?); assert_eq!(None, history.search("none", 2, SearchDirection::Reverse)?); assert_eq!(None, history.search("line", 3, SearchDirection::Reverse)?); assert_eq!( Some(SearchResult { idx: 2, entry: history.get(2, SearchDirection::Reverse)?.unwrap().entry, pos: 0 }), history.search("line", 2, SearchDirection::Reverse)? ); assert_eq!( Some(SearchResult { idx: 1, entry: history.get(1, SearchDirection::Reverse)?.unwrap().entry, pos: 0 }), history.search("line", 1, SearchDirection::Reverse)? ); assert_eq!( Some(SearchResult { idx: 0, entry: history.get(0, SearchDirection::Reverse)?.unwrap().entry, pos: 0 }), history.search("line1", 1, SearchDirection::Reverse)? ); Ok(()) } #[test] #[cfg(feature = "case_insensitive_history_search")] fn anchored_search() -> Result<()> { let history = init(); assert_eq!( Some(SearchResult { idx: 2, entry: history.get(2, SearchDirection::Reverse)?.unwrap().entry, pos: 4 }), history.starts_with("LiNe", 2, SearchDirection::Reverse)? ); assert_eq!( None, history.starts_with("iNe", 2, SearchDirection::Reverse)? ); Ok(()) } } rustyline-13.0.0/src/keymap.rs000064400000000000000000001275521046102023000144020ustar 00000000000000//! Bindings from keys to command for Emacs and Vi modes use log::debug; use super::Result; use crate::keys::{KeyCode as K, KeyEvent, KeyEvent as E, Modifiers as M}; use crate::tty::{self, RawReader, Term, Terminal}; use crate::{Config, EditMode}; #[cfg(feature = "custom-bindings")] use crate::{Event, EventContext, EventHandler}; /// The number of times one command should be repeated. pub type RepeatCount = usize; /// Commands #[derive(Debug, Clone, Eq, PartialEq)] #[non_exhaustive] pub enum Cmd { /// abort Abort, // Miscellaneous Command /// accept-line /// /// See also AcceptOrInsertLine AcceptLine, /// beginning-of-history BeginningOfHistory, /// capitalize-word CapitalizeWord, /// clear-screen ClearScreen, /// Paste from the clipboard #[cfg(windows)] PasteFromClipboard, /// complete Complete, /// complete-backward CompleteBackward, /// complete-hint CompleteHint, /// Dedent current line Dedent(Movement), /// downcase-word DowncaseWord, /// vi-eof-maybe EndOfFile, /// end-of-history EndOfHistory, /// forward-search-history (incremental search) ForwardSearchHistory, /// history-search-backward (common prefix search) HistorySearchBackward, /// history-search-forward (common prefix search) HistorySearchForward, /// Indent current line Indent(Movement), /// Insert text Insert(RepeatCount, String), /// Interrupt signal (Ctrl-C) Interrupt, /// backward-delete-char, backward-kill-line, backward-kill-word /// delete-char, kill-line, kill-word, unix-line-discard, unix-word-rubout, /// 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, /// No action Noop, /// repaint Repaint, /// 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 (incremental search) ReverseSearchHistory, /// self-insert SelfInsert(RepeatCount, char), /// Suspend signal (Ctrl-Z on unix platform) Suspend, /// transpose-chars TransposeChars, /// transpose-words TransposeWords(RepeatCount), /// undo Undo(RepeatCount), /// Unsupported / unexpected Unknown, /// upcase-word UpcaseWord, /// vi-yank-to ViYankTo(Movement), /// yank, vi-put Yank(RepeatCount, Anchor), /// yank-pop YankPop, /// moves cursor to the line above or switches to prev history entry if /// the cursor is already on the first line LineUpOrPreviousHistory(RepeatCount), /// moves cursor to the line below or switches to next history entry if /// the cursor is already on the last line LineDownOrNextHistory(RepeatCount), /// Inserts a newline Newline, /// Either accepts or inserts a newline /// /// Always inserts newline if input is non-valid. Can also insert newline /// if cursor is in the middle of the text /// /// If you support multi-line input: /// * Use `accept_in_the_middle: true` for mostly single-line cases, for /// example command-line. /// * Use `accept_in_the_middle: false` for mostly multi-line cases, for /// example SQL or JSON input. AcceptOrInsertLine { /// Whether this commands accepts input if the cursor not at the end /// of the current input accept_in_the_middle: bool, }, } impl Cmd { /// Tells if current command should reset kill ring. #[must_use] pub const fn should_reset_kill_ring(&self) -> bool { #[allow(clippy::match_same_arms)] match *self { Cmd::Kill(Movement::BackwardChar(_) | Movement::ForwardChar(_)) => true, Cmd::ClearScreen | Cmd::Kill(_) | Cmd::Replace(..) | Cmd::Noop | Cmd::Suspend | Cmd::Yank(..) | Cmd::YankPop => false, _ => true, } } const fn is_repeatable_change(&self) -> bool { matches!( *self, Cmd::Dedent(..) | Cmd::Indent(..) | Cmd::Insert(..) | Cmd::Kill(_) | Cmd::ReplaceChar(..) | Cmd::Replace(..) | Cmd::SelfInsert(..) | Cmd::ViYankTo(_) | Cmd::Yank(..) // Cmd::TransposeChars | TODO Validate ) } const 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::Dedent(ref mvt) => Cmd::Dedent(mvt.redo(new)), Cmd::Indent(ref mvt) => Cmd::Indent(mvt.redo(new)), Cmd::Insert(previous, ref text) => { Cmd::Insert(repeat_count(previous, new), text.clone()) } 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!(), } } } const fn repeat_count(previous: RepeatCount, new: Option) -> RepeatCount { match new { Some(n) => n, None => previous, } } /// Different word definitions #[derive(Debug, Clone, Eq, 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, Eq, PartialEq, Copy)] pub enum At { /// Start of word. Start, /// Before end of word. BeforeEnd, /// After end of word. AfterEnd, } /// Where to paste (relative to cursor position) #[derive(Debug, Clone, Eq, PartialEq, Copy)] pub enum Anchor { /// After cursor After, /// Before cursor Before, } /// character search #[derive(Debug, Clone, Eq, PartialEq, Copy)] pub enum CharSearch { /// Forward search Forward(char), /// Forward search until ForwardBefore(char), /// Backward search Backward(char), /// Backward search until BackwardAfter(char), } impl CharSearch { const 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, Eq, PartialEq)] #[non_exhaustive] pub enum Movement { /// Whole current line (not really a movement but a range) WholeLine, /// 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 /// character-search, character-search-backward, vi-char-search ViCharSearch(RepeatCount, CharSearch), /// vi-first-print ViFirstPrint, /// backward-char BackwardChar(RepeatCount), /// forward-char ForwardChar(RepeatCount), /// move to the same column on the previous line LineUp(RepeatCount), /// move to the same column on the next line LineDown(RepeatCount), /// Whole user input (not really a movement but a range) WholeBuffer, /// beginning-of-buffer BeginningOfBuffer, /// end-of-buffer EndOfBuffer, } impl Movement { // Replay this movement with a possible different `RepeatCount`. const 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)), Movement::LineUp(previous) => Movement::LineUp(repeat_count(previous, new)), Movement::LineDown(previous) => Movement::LineDown(repeat_count(previous, new)), Movement::WholeBuffer => Movement::WholeBuffer, Movement::BeginningOfBuffer => Movement::BeginningOfBuffer, Movement::EndOfBuffer => Movement::EndOfBuffer, } } } /// Vi input modes #[derive(Clone, Copy, Eq, PartialEq)] pub 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<'b> { pub(crate) mode: EditMode, #[cfg_attr(not(feature = "custom-bindings"), allow(dead_code))] custom_bindings: &'b Bindings, pub(crate) input_mode: InputMode, // vi only ? // numeric arguments: http://web.mit.edu/gnu/doc/html/rlman_1.html#SEC7 num_args: i16, last_cmd: Cmd, // vi only last_char_search: Option, // vi only } /// Provide indirect mutation to user input. pub trait Invoke { /// currently edited line fn input(&self) -> &str; // TODO //fn invoke(&mut self, cmd: Cmd) -> Result; } impl Invoke for &str { fn input(&self) -> &str { self } } pub trait Refresher { /// Rewrite the currently edited line accordingly to the buffer content, /// cursor position, and number of columns of the terminal. fn refresh_line(&mut self) -> Result<()>; /// Same as [`refresh_line`] with a specific message instead of hint fn refresh_line_with_msg(&mut self, msg: Option<&str>) -> Result<()>; /// Same as `refresh_line` but with a dynamic prompt. fn refresh_prompt_and_line(&mut self, prompt: &str) -> Result<()>; /// Vi only, switch to insert mode. 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; /// Returns the hint text that is shown after the current cursor position. fn hint_text(&self) -> Option<&str>; /// currently edited line fn line(&self) -> &str; /// Current cursor position (byte position) fn pos(&self) -> usize; /// Display `msg` above currently edited line. fn external_print(&mut self, msg: String) -> Result<()>; } impl<'b> InputState<'b> { pub fn new(config: &Config, custom_bindings: &'b Bindings) -> 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, ignore_external_print: bool, ) -> Result { let single_esc_abort = self.single_esc_abort(single_esc_abort); let key; if ignore_external_print { key = rdr.next_key(single_esc_abort)?; } else { loop { let event = rdr.wait_for_input(single_esc_abort)?; match event { tty::Event::KeyPress(k) => { key = k; break; } tty::Event::ExternalPrint(msg) => { wrt.external_print(msg)?; } } } } match self.mode { EditMode::Emacs => self.emacs(rdr, wrt, key), EditMode::Vi if self.input_mode != InputMode::Command => self.vi_insert(rdr, wrt, key), EditMode::Vi => self.vi_command(rdr, wrt, key), } } fn single_esc_abort(&self, single_esc_abort: bool) -> bool { match self.mode { EditMode::Emacs => single_esc_abort, EditMode::Vi => false, } } /// Terminal peculiar binding fn term_binding(rdr: &R, wrt: &dyn Refresher, key: &KeyEvent) -> Option { let cmd = rdr.find_binding(key); if cmd == Some(Cmd::EndOfFile) && !wrt.line().is_empty() { None // ReadlineError::Eof only if line is empty } else { cmd } } fn 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 { E(K::Char(digit @ '0'..='9'), m) if m == M::NONE || m == M::ALT => { if self.num_args == -1 { self.num_args *= digit.to_digit(10).unwrap() as i16; } else if self.num_args.abs() < 1000 { // 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); } } E(K::Char('-'), m) if m == M::NONE || m == M::ALT => {} _ => { wrt.refresh_line()?; return Ok(key); } }; } } fn emacs( &mut self, rdr: &mut R, wrt: &mut dyn Refresher, mut key: KeyEvent, ) -> Result { if let E(K::Char(digit @ '-'), M::ALT) = key { key = self.emacs_digit_argument(rdr, wrt, digit)?; } else if let E(K::Char(digit @ '0'..='9'), M::ALT) = key { key = self.emacs_digit_argument(rdr, wrt, digit)?; } let (n, positive) = self.emacs_num_args(); // consume them in all cases let mut evt = key.into(); if let Some(cmd) = self.custom_binding(wrt, &evt, n, positive) { return Ok(if cmd.is_repeatable() { cmd.redo(Some(n), wrt) } else { cmd }); } else if let Some(cmd) = InputState::term_binding(rdr, wrt, &key) { return Ok(cmd); } let cmd = match key { E(K::Char(c), M::NONE) => { if positive { Cmd::SelfInsert(n, c) } else { Cmd::Unknown } } E(K::Char('A'), M::CTRL) => Cmd::Move(Movement::BeginningOfLine), E(K::Char('B'), M::CTRL) => Cmd::Move(if positive { Movement::BackwardChar(n) } else { Movement::ForwardChar(n) }), E(K::Char('E'), M::CTRL) => Cmd::Move(Movement::EndOfLine), E(K::Char('F'), M::CTRL) => Cmd::Move(if positive { Movement::ForwardChar(n) } else { Movement::BackwardChar(n) }), E(K::Char('G'), M::CTRL | M::CTRL_ALT) | E::ESC => Cmd::Abort, E(K::Char('H'), M::CTRL) | E::BACKSPACE => Cmd::Kill(if positive { Movement::BackwardChar(n) } else { Movement::ForwardChar(n) }), E(K::BackTab, M::NONE) => Cmd::CompleteBackward, E(K::Char('I'), M::CTRL) | E(K::Tab, M::NONE) => { if positive { Cmd::Complete } else { Cmd::CompleteBackward } } // Don't complete hints when the cursor is not at the end of a line E(K::Right, M::NONE) if wrt.has_hint() && wrt.is_cursor_at_end() => Cmd::CompleteHint, E(K::Char('K'), M::CTRL) => Cmd::Kill(if positive { Movement::EndOfLine } else { Movement::BeginningOfLine }), E(K::Char('L'), M::CTRL) => Cmd::ClearScreen, E(K::Char('N'), M::CTRL) => Cmd::NextHistory, E(K::Char('P'), M::CTRL) => Cmd::PreviousHistory, E(K::Char('X'), M::CTRL) => { if let Some(cmd) = self.custom_seq_binding(rdr, wrt, &mut evt, n, positive)? { cmd } else { let snd_key = match evt { // we may have already read the second key in custom_seq_binding Event::KeySeq(ref key_seq) if key_seq.len() > 1 => key_seq[1], _ => rdr.next_key(true)?, }; match snd_key { E(K::Char('G'), M::CTRL) | E::ESC => Cmd::Abort, E(K::Char('U'), M::CTRL) => Cmd::Undo(n), E(K::Backspace, M::NONE) => Cmd::Kill(if positive { Movement::BeginningOfLine } else { Movement::EndOfLine }), _ => Cmd::Unknown, } } } // character-search, character-search-backward E(K::Char(']'), m @ (M::CTRL | M::CTRL_ALT)) => { let ch = rdr.next_key(false)?; match ch { E(K::Char(ch), M::NONE) => Cmd::Move(Movement::ViCharSearch( n, if positive { if m.contains(M::ALT) { CharSearch::Backward(ch) } else { CharSearch::ForwardBefore(ch) } } else if m.contains(M::ALT) { CharSearch::ForwardBefore(ch) } else { CharSearch::Backward(ch) }, )), _ => Cmd::Unknown, } } E(K::Backspace, M::ALT) => Cmd::Kill(if positive { Movement::BackwardWord(n, Word::Emacs) } else { Movement::ForwardWord(n, At::AfterEnd, Word::Emacs) }), E(K::Char('<'), M::ALT) => Cmd::BeginningOfHistory, E(K::Char('>'), M::ALT) => Cmd::EndOfHistory, E(K::Char('B' | 'b') | K::Left, M::ALT) | E(K::Left, M::CTRL) => { Cmd::Move(if positive { Movement::BackwardWord(n, Word::Emacs) } else { Movement::ForwardWord(n, At::AfterEnd, Word::Emacs) }) } E(K::Char('C' | 'c'), M::ALT) => Cmd::CapitalizeWord, E(K::Char('D' | 'd'), M::ALT) => Cmd::Kill(if positive { Movement::ForwardWord(n, At::AfterEnd, Word::Emacs) } else { Movement::BackwardWord(n, Word::Emacs) }), E(K::Char('F' | 'f') | K::Right, M::ALT) | E(K::Right, M::CTRL) => { Cmd::Move(if positive { Movement::ForwardWord(n, At::AfterEnd, Word::Emacs) } else { Movement::BackwardWord(n, Word::Emacs) }) } E(K::Char('L' | 'l'), M::ALT) => Cmd::DowncaseWord, E(K::Char('T' | 't'), M::ALT) => Cmd::TransposeWords(n), // TODO ESC-R (r): Undo all changes made to this line. E(K::Char('U' | 'u'), M::ALT) => Cmd::UpcaseWord, E(K::Char('Y' | 'y'), M::ALT) => Cmd::YankPop, _ => self.common(rdr, wrt, evt, 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 E(K::Char(digit @ '0'..='9'), M::NONE) = key { if self.num_args.abs() < 1000 { // shouldn't ever need more than 4 digits self.num_args = self .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, mut key: KeyEvent, ) -> Result { if let E(K::Char(digit @ '1'..='9'), M::NONE) = key { key = self.vi_arg_digit(rdr, wrt, digit)?; } let no_num_args = self.num_args == 0; let n = self.vi_num_args(); // consume them in all cases let evt = key.into(); if let Some(cmd) = self.custom_binding(wrt, &evt, n, true) { return Ok(if cmd.is_repeatable() { if no_num_args { cmd.redo(None, wrt) } else { cmd.redo(Some(n), wrt) } } else { cmd }); } else if let Some(cmd) = InputState::term_binding(rdr, wrt, &key) { return Ok(cmd); } let cmd = match key { E(K::Char('$') | K::End, M::NONE) => Cmd::Move(Movement::EndOfLine), E(K::Char('.'), M::NONE) => { // vi-redo (repeat last command) if !self.last_cmd.is_repeatable() { Cmd::Noop } else if no_num_args { self.last_cmd.redo(None, wrt) } else { self.last_cmd.redo(Some(n), wrt) } } // TODO E(K::Char('%'), M::NONE) => Cmd::???, Move to the corresponding opening/closing // bracket E(K::Char('0'), M::NONE) => Cmd::Move(Movement::BeginningOfLine), E(K::Char('^'), M::NONE) => Cmd::Move(Movement::ViFirstPrint), E(K::Char('a'), M::NONE) => { // vi-append-mode self.input_mode = InputMode::Insert; wrt.doing_insert(); Cmd::Move(Movement::ForwardChar(n)) } E(K::Char('A'), M::NONE) => { // vi-append-eol self.input_mode = InputMode::Insert; wrt.doing_insert(); Cmd::Move(Movement::EndOfLine) } E(K::Char('b'), M::NONE) => Cmd::Move(Movement::BackwardWord(n, Word::Vi)), /* vi-prev-word */ E(K::Char('B'), M::NONE) => Cmd::Move(Movement::BackwardWord(n, Word::Big)), E(K::Char('c'), M::NONE) => { self.input_mode = InputMode::Insert; match self.vi_cmd_motion(rdr, wrt, key, n)? { Some(mvt) => Cmd::Replace(mvt, None), None => Cmd::Unknown, } } E(K::Char('C'), M::NONE) => { self.input_mode = InputMode::Insert; Cmd::Replace(Movement::EndOfLine, None) } E(K::Char('d'), M::NONE) => match self.vi_cmd_motion(rdr, wrt, key, n)? { Some(mvt) => Cmd::Kill(mvt), None => Cmd::Unknown, }, E(K::Char('D'), M::NONE) | E(K::Char('K'), M::CTRL) => Cmd::Kill(Movement::EndOfLine), E(K::Char('e'), M::NONE) => { Cmd::Move(Movement::ForwardWord(n, At::BeforeEnd, Word::Vi)) } E(K::Char('E'), M::NONE) => { Cmd::Move(Movement::ForwardWord(n, At::BeforeEnd, Word::Big)) } E(K::Char('i'), M::NONE) => { // vi-insertion-mode self.input_mode = InputMode::Insert; wrt.doing_insert(); Cmd::Noop } E(K::Char('I'), M::NONE) => { // vi-insert-beg self.input_mode = InputMode::Insert; wrt.doing_insert(); Cmd::Move(Movement::BeginningOfLine) } E(K::Char(c), M::NONE) if c == 'f' || c == 'F' || c == 't' || c == 'T' => { // vi-char-search let cs = self.vi_char_search(rdr, c)?; match cs { Some(cs) => Cmd::Move(Movement::ViCharSearch(n, cs)), None => Cmd::Unknown, } } E(K::Char(';'), M::NONE) => match self.last_char_search { Some(cs) => Cmd::Move(Movement::ViCharSearch(n, cs)), None => Cmd::Noop, }, E(K::Char(','), M::NONE) => match self.last_char_search { Some(ref cs) => Cmd::Move(Movement::ViCharSearch(n, cs.opposite())), None => Cmd::Noop, }, // TODO E(K::Char('G'), M::NONE) => Cmd::???, Move to the history line n E(K::Char('p'), M::NONE) => Cmd::Yank(n, Anchor::After), // vi-put E(K::Char('P'), M::NONE) => Cmd::Yank(n, Anchor::Before), // vi-put E(K::Char('r'), M::NONE) => { // vi-replace-char: let ch = rdr.next_key(false)?; match ch { E(K::Char(c), M::NONE) => Cmd::ReplaceChar(n, c), E::ESC => Cmd::Noop, _ => Cmd::Unknown, } } E(K::Char('R'), M::NONE) => { // vi-replace-mode (overwrite-mode) self.input_mode = InputMode::Replace; Cmd::Replace(Movement::ForwardChar(0), None) } E(K::Char('s'), M::NONE) => { // vi-substitute-char: self.input_mode = InputMode::Insert; Cmd::Replace(Movement::ForwardChar(n), None) } E(K::Char('S'), M::NONE) => { // vi-substitute-line: self.input_mode = InputMode::Insert; Cmd::Replace(Movement::WholeLine, None) } E(K::Char('u'), M::NONE) => Cmd::Undo(n), // E(K::Char('U'), M::NONE) => Cmd::???, // revert-line E(K::Char('w'), M::NONE) => Cmd::Move(Movement::ForwardWord(n, At::Start, Word::Vi)), /* vi-next-word */ E(K::Char('W'), M::NONE) => Cmd::Move(Movement::ForwardWord(n, At::Start, Word::Big)), /* vi-next-word */ // TODO move backward if eol E(K::Char('x'), M::NONE) => Cmd::Kill(Movement::ForwardChar(n)), // vi-delete E(K::Char('X'), M::NONE) => Cmd::Kill(Movement::BackwardChar(n)), // vi-rubout E(K::Char('y'), M::NONE) => match self.vi_cmd_motion(rdr, wrt, key, n)? { Some(mvt) => Cmd::ViYankTo(mvt), None => Cmd::Unknown, }, // E(K::Char('Y'), M::NONE) => Cmd::???, // vi-yank-to E(K::Char('h'), M::NONE) | E(K::Char('H'), M::CTRL) | E::BACKSPACE => { Cmd::Move(Movement::BackwardChar(n)) } E(K::Char('G'), M::CTRL) => Cmd::Abort, E(K::Char('l' | ' '), M::NONE) => Cmd::Move(Movement::ForwardChar(n)), E(K::Char('L'), M::CTRL) => Cmd::ClearScreen, E(K::Char('+' | 'j'), M::NONE) => Cmd::LineDownOrNextHistory(n), // TODO: move to the start of the line. E(K::Char('N'), M::CTRL) => Cmd::NextHistory, E(K::Char('-' | 'k'), M::NONE) => Cmd::LineUpOrPreviousHistory(n), // TODO: move to the start of the line. E(K::Char('P'), M::CTRL) => Cmd::PreviousHistory, E(K::Char('R'), M::CTRL) => { self.input_mode = InputMode::Insert; // TODO Validate Cmd::ReverseSearchHistory } E(K::Char('S'), M::CTRL) => { self.input_mode = InputMode::Insert; // TODO Validate Cmd::ForwardSearchHistory } E(K::Char('<'), M::NONE) => match self.vi_cmd_motion(rdr, wrt, key, n)? { Some(mvt) => Cmd::Dedent(mvt), None => Cmd::Unknown, }, E(K::Char('>'), M::NONE) => match self.vi_cmd_motion(rdr, wrt, key, n)? { Some(mvt) => Cmd::Indent(mvt), None => Cmd::Unknown, }, E::ESC => Cmd::Noop, _ => self.common(rdr, wrt, evt, key, n, true)?, }; debug!(target: "rustyline", "Vi command: {:?}", cmd); if cmd.is_repeatable_change() { self.last_cmd = cmd.clone(); } Ok(cmd) } fn vi_insert( &mut self, rdr: &mut R, wrt: &mut dyn Refresher, key: KeyEvent, ) -> Result { let evt = key.into(); if let Some(cmd) = self.custom_binding(wrt, &evt, 0, true) { return Ok(if cmd.is_repeatable() { cmd.redo(None, wrt) } else { cmd }); } else if let Some(cmd) = InputState::term_binding(rdr, wrt, &key) { return Ok(cmd); } let cmd = match key { E(K::Char(c), M::NONE) => { if self.input_mode == InputMode::Replace { Cmd::Overwrite(c) } else { Cmd::SelfInsert(1, c) } } E(K::Char('H'), M::CTRL) | E::BACKSPACE => Cmd::Kill(Movement::BackwardChar(1)), E(K::BackTab, M::NONE) => Cmd::CompleteBackward, E(K::Char('I'), M::CTRL) | E(K::Tab, M::NONE) => Cmd::Complete, // Don't complete hints when the cursor is not at the end of a line E(K::Right, M::NONE) if wrt.has_hint() && wrt.is_cursor_at_end() => Cmd::CompleteHint, E(K::Char(k), M::ALT) => { debug!(target: "rustyline", "Vi fast command mode: {}", k); self.input_mode = InputMode::Command; wrt.done_inserting(); self.vi_command(rdr, wrt, E(K::Char(k), M::NONE))? } E::ESC => { // vi-movement-mode/vi-command-mode self.input_mode = InputMode::Command; wrt.done_inserting(); Cmd::Move(Movement::BackwardChar(1)) } _ => self.common(rdr, wrt, evt, key, 1, true)?, }; debug!(target: "rustyline", "Vi insert: {:?}", cmd); if cmd.is_repeatable_change() { #[allow(clippy::if_same_then_else)] if let (Cmd::Replace(..), Cmd::SelfInsert(..)) = (&self.last_cmd, &cmd) { // replacing... } else if let (Cmd::SelfInsert(..), Cmd::SelfInsert(..)) = (&self.last_cmd, &cmd) { // inserting... } else { self.last_cmd = cmd.clone(); } } Ok(cmd) } fn vi_cmd_motion( &mut self, rdr: &mut R, wrt: &mut dyn Refresher, key: KeyEvent, n: RepeatCount, ) -> Result> { let mut mvt = rdr.next_key(false)?; if mvt == key { return Ok(Some(Movement::WholeLine)); } let mut n = n; if let E(K::Char(digit @ '1'..='9'), M::NONE) = mvt { // vi-arg-digit mvt = self.vi_arg_digit(rdr, wrt, digit)?; n = self.vi_num_args().saturating_mul(n); } Ok(match mvt { E(K::Char('$'), M::NONE) => Some(Movement::EndOfLine), E(K::Char('0'), M::NONE) => Some(Movement::BeginningOfLine), E(K::Char('^'), M::NONE) => Some(Movement::ViFirstPrint), E(K::Char('b'), M::NONE) => Some(Movement::BackwardWord(n, Word::Vi)), E(K::Char('B'), M::NONE) => Some(Movement::BackwardWord(n, Word::Big)), E(K::Char('e'), M::NONE) => Some(Movement::ForwardWord(n, At::AfterEnd, Word::Vi)), E(K::Char('E'), M::NONE) => Some(Movement::ForwardWord(n, At::AfterEnd, Word::Big)), E(K::Char(c), M::NONE) if c == 'f' || c == 'F' || c == 't' || c == 'T' => { let cs = self.vi_char_search(rdr, c)?; cs.map(|cs| Movement::ViCharSearch(n, cs)) } E(K::Char(';'), M::NONE) => self .last_char_search .map(|cs| Movement::ViCharSearch(n, cs)), E(K::Char(','), M::NONE) => self .last_char_search .map(|cs| Movement::ViCharSearch(n, cs.opposite())), E(K::Char('h'), M::NONE) | E(K::Char('H'), M::CTRL) | E::BACKSPACE => { Some(Movement::BackwardChar(n)) } E(K::Char('l' | ' '), M::NONE) => Some(Movement::ForwardChar(n)), E(K::Char('j' | '+'), M::NONE) => Some(Movement::LineDown(n)), E(K::Char('k' | '-'), M::NONE) => Some(Movement::LineUp(n)), E(K::Char('w'), M::NONE) => { // 'cw' is 'ce' if key == E(K::Char('c'), M::NONE) { Some(Movement::ForwardWord(n, At::AfterEnd, Word::Vi)) } else { Some(Movement::ForwardWord(n, At::Start, Word::Vi)) } } E(K::Char('W'), M::NONE) => { // 'cW' is 'cE' if key == E(K::Char('c'), M::NONE) { Some(Movement::ForwardWord(n, At::AfterEnd, Word::Big)) } else { Some(Movement::ForwardWord(n, At::Start, Word::Big)) } } _ => None, }) } fn vi_char_search( &mut self, rdr: &mut R, cmd: char, ) -> Result> { let ch = rdr.next_key(false)?; Ok(match ch { E(K::Char(ch), M::NONE) => { 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, wrt: &dyn Refresher, mut evt: Event, key: KeyEvent, n: RepeatCount, positive: bool, ) -> Result { Ok(match key { E(K::Home, M::NONE) => Cmd::Move(Movement::BeginningOfLine), E(K::Left, M::NONE) => Cmd::Move(if positive { Movement::BackwardChar(n) } else { Movement::ForwardChar(n) }), #[cfg(any(windows, test))] E(K::Char('C'), M::CTRL) => Cmd::Interrupt, E(K::Char('D'), M::CTRL) => { if self.is_emacs_mode() && !wrt.line().is_empty() { Cmd::Kill(if positive { Movement::ForwardChar(n) } else { Movement::BackwardChar(n) }) } else if cfg!(windows) || cfg!(test) || !wrt.line().is_empty() { Cmd::EndOfFile } else { Cmd::Unknown } } E(K::Delete, M::NONE) => Cmd::Kill(if positive { Movement::ForwardChar(n) } else { Movement::BackwardChar(n) }), E(K::End, M::NONE) => Cmd::Move(Movement::EndOfLine), E(K::Right, M::NONE) => Cmd::Move(if positive { Movement::ForwardChar(n) } else { Movement::BackwardChar(n) }), E(K::Char('J' | 'M'), M::CTRL) | E::ENTER => Cmd::AcceptOrInsertLine { accept_in_the_middle: true, }, E(K::Down, M::NONE) => Cmd::LineDownOrNextHistory(1), E(K::Up, M::NONE) => Cmd::LineUpOrPreviousHistory(1), E(K::Char('R'), M::CTRL) => Cmd::ReverseSearchHistory, // most terminals override Ctrl+S to suspend execution E(K::Char('S'), M::CTRL) => Cmd::ForwardSearchHistory, E(K::Char('T'), M::CTRL) => Cmd::TransposeChars, E(K::Char('U'), M::CTRL) => Cmd::Kill(if positive { Movement::BeginningOfLine } else { Movement::EndOfLine }), // most terminals override Ctrl+Q to resume execution E(K::Char('Q'), M::CTRL) => Cmd::QuotedInsert, #[cfg(not(windows))] E(K::Char('V'), M::CTRL) => Cmd::QuotedInsert, #[cfg(windows)] E(K::Char('V'), M::CTRL) => Cmd::PasteFromClipboard, E(K::Char('W'), M::CTRL) => Cmd::Kill(if positive { Movement::BackwardWord(n, Word::Big) } else { Movement::ForwardWord(n, At::AfterEnd, Word::Big) }), E(K::Char('Y'), M::CTRL) => { if positive { Cmd::Yank(n, Anchor::Before) } else { Cmd::Unknown // TODO Validate } } E(K::Char('_'), M::CTRL) => Cmd::Undo(n), E(K::UnknownEscSeq, M::NONE) => Cmd::Noop, E(K::BracketedPasteStart, M::NONE) => { let paste = rdr.read_pasted_text()?; Cmd::Insert(1, paste) } _ => self .custom_seq_binding(rdr, wrt, &mut evt, n, positive)? .unwrap_or(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, 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.unsigned_abs() as RepeatCount } } } #[cfg(feature = "custom-bindings")] impl<'b> InputState<'b> { /// Application customized binding fn custom_binding( &self, wrt: &dyn Refresher, evt: &Event, n: RepeatCount, positive: bool, ) -> Option { let bindings = self.custom_bindings; let handler = bindings.get(evt).or_else(|| bindings.get(&Event::Any)); if let Some(handler) = handler { match handler { EventHandler::Simple(cmd) => Some(cmd.clone()), EventHandler::Conditional(handler) => { let ctx = EventContext::new(self, wrt); handler.handle(evt, n, positive, &ctx) } } } else { None } } fn custom_seq_binding( &self, rdr: &mut R, wrt: &dyn Refresher, evt: &mut Event, n: RepeatCount, positive: bool, ) -> Result> { while let Some(subtrie) = self.custom_bindings.get_raw_descendant(evt) { let snd_key = rdr.next_key(true)?; if let Event::KeySeq(ref mut key_seq) = evt { key_seq.push(snd_key); } else { break; } let handler = subtrie.get(evt).unwrap(); if let Some(handler) = handler { let cmd = match handler { EventHandler::Simple(cmd) => Some(cmd.clone()), EventHandler::Conditional(handler) => { let ctx = EventContext::new(self, wrt); handler.handle(evt, n, positive, &ctx) } }; if cmd.is_some() { return Ok(cmd); } } } Ok(None) } } #[cfg(not(feature = "custom-bindings"))] impl<'b> InputState<'b> { fn custom_binding(&self, _: &dyn Refresher, _: &Event, _: RepeatCount, _: bool) -> Option { None } fn custom_seq_binding( &self, _: &mut R, _: &dyn Refresher, _: &mut Event, _: RepeatCount, _: bool, ) -> Result> { Ok(None) } } cfg_if::cfg_if! { if #[cfg(feature = "custom-bindings")] { pub type Bindings = radix_trie::Trie; } else { enum Event { KeySeq([KeyEvent; 1]), } impl From for Event { fn from(k: KeyEvent) -> Event { Event::KeySeq([k]) } } pub struct Bindings {} impl Bindings { pub fn new() -> Bindings { Bindings {} } } } } rustyline-13.0.0/src/keys.rs000064400000000000000000000157341046102023000140650ustar 00000000000000//! Key constants /// Input key pressed and modifiers #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct KeyEvent(pub KeyCode, pub Modifiers); impl KeyEvent { /// Constant value representing an unmodified press of `KeyCode::Backspace`. pub(crate) const BACKSPACE: Self = Self(KeyCode::Backspace, Modifiers::NONE); /// Constant value representing an unmodified press of `KeyCode::Enter`. pub(crate) const ENTER: Self = Self(KeyCode::Enter, Modifiers::NONE); /// Constant value representing an unmodified press of `KeyCode::Esc`. pub(crate) const ESC: Self = Self(KeyCode::Esc, Modifiers::NONE); /// Constructor from `char` and modifiers #[must_use] pub fn new(c: char, mut mods: Modifiers) -> Self { use {KeyCode as K, KeyEvent as E, Modifiers as M}; if !c.is_control() { if !mods.is_empty() { mods.remove(M::SHIFT); // TODO Validate: no SHIFT even if // `c` is uppercase } return E(K::Char(c), mods); } #[allow(clippy::match_same_arms)] match c { '\x00' => E(K::Char('@'), mods | M::CTRL), // '\0' '\x01' => E(K::Char('A'), mods | M::CTRL), '\x02' => E(K::Char('B'), mods | M::CTRL), '\x03' => E(K::Char('C'), mods | M::CTRL), '\x04' => E(K::Char('D'), mods | M::CTRL), '\x05' => E(K::Char('E'), mods | M::CTRL), '\x06' => E(K::Char('F'), mods | M::CTRL), '\x07' => E(K::Char('G'), mods | M::CTRL), // '\a' #[cfg(unix)] '\x08' => E(K::Backspace, mods), // '\b' #[cfg(windows)] '\x08' => E(K::Char('H'), mods | M::CTRL), #[cfg(unix)] '\x09' => { // '\t' if mods.contains(M::SHIFT) { mods.remove(M::SHIFT); E(K::BackTab, mods) } else { E(K::Tab, mods) } } #[cfg(windows)] '\x09' => E(K::Char('I'), mods | M::CTRL), '\x0a' => E(K::Char('J'), mods | M::CTRL), // '\n' (10) '\x0b' => E(K::Char('K'), mods | M::CTRL), '\x0c' => E(K::Char('L'), mods | M::CTRL), #[cfg(unix)] '\x0d' => E(K::Enter, mods), // '\r' (13) #[cfg(windows)] '\x0d' => E(K::Char('M'), mods | M::CTRL), '\x0e' => E(K::Char('N'), mods | M::CTRL), '\x0f' => E(K::Char('O'), mods | M::CTRL), '\x10' => E(K::Char('P'), mods | M::CTRL), '\x11' => E(K::Char('Q'), mods | M::CTRL), '\x12' => E(K::Char('R'), mods | M::CTRL), '\x13' => E(K::Char('S'), mods | M::CTRL), '\x14' => E(K::Char('T'), mods | M::CTRL), '\x15' => E(K::Char('U'), mods | M::CTRL), '\x16' => E(K::Char('V'), mods | M::CTRL), '\x17' => E(K::Char('W'), mods | M::CTRL), '\x18' => E(K::Char('X'), mods | M::CTRL), '\x19' => E(K::Char('Y'), mods | M::CTRL), '\x1a' => E(K::Char('Z'), mods | M::CTRL), '\x1b' => E(K::Esc, mods), // Ctrl-[, '\e' '\x1c' => E(K::Char('\\'), mods | M::CTRL), '\x1d' => E(K::Char(']'), mods | M::CTRL), '\x1e' => E(K::Char('^'), mods | M::CTRL), '\x1f' => E(K::Char('_'), mods | M::CTRL), '\x7f' => E(K::Backspace, mods), // Rubout, Ctrl-? '\u{9b}' => E(K::Esc, mods | M::SHIFT), _ => E(K::Null, mods), } } /// Constructor from `char` with Ctrl modifier #[must_use] pub fn ctrl(c: char) -> Self { Self::new(c, Modifiers::CTRL) } /// Constructor from `char` with Alt modifier #[must_use] pub fn alt(c: char) -> Self { Self::new(c, Modifiers::ALT) } /// ctrl-a => ctrl-A (uppercase) /// shift-A => A (no SHIFT modifier) /// shift-Tab => `BackTab` #[must_use] pub fn normalize(e: Self) -> Self { use {KeyCode as K, KeyEvent as E, Modifiers as M}; match e { E(K::Char(c), m) if c.is_ascii_control() => Self::new(c, m), E(K::Char(c), m) if c.is_ascii_lowercase() && m.contains(M::CTRL) => { E(K::Char(c.to_ascii_uppercase()), m) } E(K::Char(c), m) if c.is_ascii_uppercase() && m.contains(M::SHIFT) => { E(K::Char(c), m ^ M::SHIFT) } E(K::Tab, m) if m.contains(M::SHIFT) => E(K::BackTab, m ^ M::SHIFT), _ => e, } } } impl From for KeyEvent { fn from(c: char) -> Self { Self::new(c, Modifiers::NONE) } } /// Input key pressed #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum KeyCode { /// Unsupported escape sequence (on unix platform) UnknownEscSeq, /// ⌫ or Ctrl-H Backspace, /// ⇤ (usually Shift-Tab) BackTab, /// Paste (on unix platform) BracketedPasteStart, /// Paste (on unix platform) BracketedPasteEnd, /// Single char Char(char), /// ⌦ Delete, /// ↓ arrow key Down, /// ⇲ End, /// ↵ or Ctrl-M Enter, /// Escape or Ctrl-[ Esc, /// Function key F(u8), /// ⇱ Home, /// Insert key Insert, /// ← arrow key Left, /// \0 Null, /// ⇟ PageDown, /// ⇞ PageUp, /// → arrow key Right, /// ⇥ or Ctrl-I Tab, /// ↑ arrow key Up, } bitflags::bitflags! { /// The set of modifier keys that were triggered along with a key press. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct Modifiers: u8 { /// Control modifier const CTRL = 1<<3; /// Escape or Alt modifier const ALT = 1<<2; /// Shift modifier const SHIFT = 1<<1; /// No modifier const NONE = 0; /// Ctrl + Shift const CTRL_SHIFT = Self::CTRL.bits() | Self::SHIFT.bits(); /// Alt + Shift const ALT_SHIFT = Self::ALT.bits() | Self::SHIFT.bits(); /// Ctrl + Alt const CTRL_ALT = Self::CTRL.bits() | Self::ALT.bits(); /// Ctrl + Alt + Shift const CTRL_ALT_SHIFT = Self::CTRL.bits() | Self::ALT.bits() | Self::SHIFT.bits(); } } #[cfg(test)] mod tests { use super::{KeyCode as K, KeyEvent as E, Modifiers as M}; #[test] fn new() { assert_eq!(E::ESC, E::new('\x1b', M::NONE)); } #[test] #[cfg(unix)] fn from() { assert_eq!(E(K::Tab, M::NONE), E::from('\t')); } #[test] #[cfg(windows)] fn from() { assert_eq!(E(K::Char('I'), M::CTRL), E::from('\t')); } #[test] fn normalize() { assert_eq!(E::ctrl('A'), E::normalize(E(K::Char('\x01'), M::NONE))); assert_eq!(E::ctrl('A'), E::normalize(E::ctrl('a'))); assert_eq!(E::from('A'), E::normalize(E(K::Char('A'), M::SHIFT))); assert_eq!(E(K::BackTab, M::NONE), E::normalize(E(K::Tab, M::SHIFT))); } } rustyline-13.0.0/src/kill_ring.rs000064400000000000000000000154631046102023000150630ustar 00000000000000//! 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-13.0.0/src/layout.rs000064400000000000000000000016201046102023000144140ustar 00000000000000use std::cmp::{Ord, Ordering, PartialOrd}; #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] pub struct Position { pub col: usize, // The leftmost column is number 0. pub row: usize, // The highest row is number 0. } 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-13.0.0/src/lib.rs000064400000000000000000001000601046102023000136430ustar 00000000000000//! Readline for Rust //! //! This implementation is based on [Antirez's //! Linenoise](https://github.com/antirez/linenoise) //! //! # Example //! //! Usage //! //! ``` //! let mut rl = rustyline::DefaultEditor::new()?; //! let readline = rl.readline(">> "); //! match readline { //! Ok(line) => println!("Line: {:?}", line), //! Err(_) => println!("No input"), //! } //! # Ok::<(), rustyline::error::ReadlineError>(()) //! ``` #![warn(missing_docs)] #![cfg_attr(docsrs, feature(doc_cfg))] #[cfg(feature = "custom-bindings")] mod binding; mod command; 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; #[cfg(feature = "with-sqlite-history")] pub mod sqlite_history; mod tty; mod undo; pub mod validate; use std::fmt; use std::io::{self, BufRead, Write}; use std::path::Path; use std::result; use log::debug; #[cfg(feature = "derive")] #[cfg_attr(docsrs, doc(cfg(feature = "derive")))] pub use rustyline_derive::{Completer, Helper, Highlighter, Hinter, Validator}; use unicode_width::UnicodeWidthStr; use crate::tty::{RawMode, RawReader, Renderer, Term, Terminal}; #[cfg(feature = "custom-bindings")] pub use crate::binding::{ConditionalEventHandler, Event, EventContext, EventHandler}; use crate::completion::{longest_common_prefix, Candidate, Completer}; pub use crate::config::{Behavior, ColorMode, CompletionType, Config, EditMode, HistoryDuplicates}; use crate::edit::State; use crate::error::ReadlineError; use crate::highlight::Highlighter; use crate::hint::Hinter; use crate::history::{DefaultHistory, History, SearchDirection}; pub use crate::keymap::{Anchor, At, CharSearch, Cmd, InputMode, Movement, RepeatCount, Word}; use crate::keymap::{Bindings, InputState, Refresher}; pub use crate::keys::{KeyCode, KeyEvent, Modifiers}; use crate::kill_ring::KillRing; pub use crate::tty::ExternalPrinter; pub use crate::undo::Changeset; 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> { #[cfg(all(unix, feature = "with-fuzzy"))] use skim::prelude::{ unbounded, Skim, SkimItem, SkimItemReceiver, SkimItemSender, SkimOptionsBuilder, }; let completer = s.helper.unwrap(); // get a list of completions let (start, candidates) = completer.complete(&s.line, s.line.pos(), &s.ctx)?; // 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.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, &mut s.changes); } else { // Restore current edited line s.line.update(&backup, backup_pos, &mut s.changes); } s.refresh_line()?; cmd = s.next_cmd(input_state, rdr, true, 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, &mut s.changes); s.refresh_line()?; } s.changes.truncate(mark); return Ok(None); } _ => { s.changes.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 || candidates.len() == 1 { completer.update(&mut s.line, start, lcp, &mut s.changes); 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, 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_str())?; 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, true)?; } matches!(cmd, Cmd::SelfInsert(1, 'y' | 'Y')) } else { true }; if show_completions { page_completions(rdr, s, input_state, &candidates) } else { s.refresh_line()?; Ok(None) } } else { // if fuzzy feature is enabled and on unix based systems check for the // corresponding completion_type #[cfg(all(unix, feature = "with-fuzzy"))] { use std::borrow::Cow; if CompletionType::Fuzzy == config.completion_type() { struct Candidate { index: usize, text: String, } impl SkimItem for Candidate { fn text(&self) -> Cow { Cow::Borrowed(&self.text) } } let (tx_item, rx_item): (SkimItemSender, SkimItemReceiver) = unbounded(); candidates .iter() .enumerate() .map(|(i, c)| Candidate { index: i, text: c.display().to_owned(), }) .for_each(|c| { let _ = tx_item.send(std::sync::Arc::new(c)); }); drop(tx_item); // so that skim could know when to stop waiting for more items. // setup skim and run with input options // will display UI for fuzzy search and return selected results // by default skim multi select is off so only expect one selection let options = SkimOptionsBuilder::default() .prompt(Some("? ")) .reverse(true) .build() .unwrap(); let selected_items = Skim::run_with(&options, Some(rx_item)) .map(|out| out.selected_items) .unwrap_or_default(); // match the first (and only) returned option with the candidate and update the // line otherwise only refresh line to clear the skim UI changes if let Some(item) = selected_items.first() { let item: &Candidate = (*item).as_any() // cast to Any .downcast_ref::() // downcast to concrete type .expect("something wrong with downcast"); if let Some(candidate) = candidates.get(item.index) { completer.update( &mut s.line, start, candidate.replacement(), &mut s.changes, ); } } s.refresh_line()?; } }; 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 let Some(text) = hint.completion() { if s.line.yank(text, 1, &mut s.changes).is_none() { s.out.beep()?; } } else { s.out.beep()?; } s.refresh_line() } 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("\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 != Cmd::Newline && !matches!(cmd, Cmd::AcceptOrInsertLine { .. }) { cmd = s.next_cmd(input_state, rdr, false, true)?; } match cmd { Cmd::SelfInsert(1, 'y' | 'Y' | ' ') => { pause_row += s.out.get_rows() - 1; } Cmd::AcceptLine | Cmd::Newline | Cmd::AcceptOrInsertLine { .. } => { pause_row += 1; } _ => break, } } s.out.write_and_flush("\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_str())?; } s.out.write_and_flush("\n")?; s.layout.end.row = 0; // dirty way to make clear_old_rows do nothing s.layout.cursor.row = 0; s.refresh_line()?; Ok(None) } /// Incremental search fn reverse_incremental_search( rdr: &mut ::Reader, s: &mut State<'_, '_, H>, input_state: &mut InputState, history: &I, ) -> Result> { if history.is_empty() { return Ok(None); } let mark = s.changes.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 = SearchDirection::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, 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 = SearchDirection::Reverse; if history_idx > 0 { history_idx -= 1; } else { success = false; continue; } } Cmd::ForwardSearchHistory => { direction = SearchDirection::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, &mut s.changes); s.refresh_line()?; s.changes.truncate(mark); return Ok(None); } Cmd::Move(_) => { s.refresh_line()?; // restore prompt break; } _ => break, } } success = match history.search(&search_buf, history_idx, direction)? { Some(sr) => { history_idx = sr.idx; s.line.update(&sr.entry, sr.pos, &mut s.changes); true } _ => false, }; } s.changes.end(); Ok(Some(cmd)) } 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(); } } // Helper to handle backspace characters in a direct input fn apply_backspace_direct(input: &str) -> String { // Setup the output buffer // No '\b' in the input in the common case, so set the capacity to the input // length let mut out = String::with_capacity(input.len()); // Keep track of the size of each grapheme from the input // As many graphemes as input bytes in the common case let mut grapheme_sizes: Vec = Vec::with_capacity(input.len()); for g in unicode_segmentation::UnicodeSegmentation::graphemes(input, true) { if g == "\u{0008}" { // backspace char if let Some(n) = grapheme_sizes.pop() { // Remove the last grapheme out.truncate(out.len() - n as usize); } } else { out.push_str(g); grapheme_sizes.push(g.len() as u8); } } out } fn readline_direct( mut reader: impl BufRead, mut writer: impl Write, validator: &Option, ) -> Result { let mut input = String::new(); loop { if reader.read_line(&mut input)? == 0 { return Err(ReadlineError::Eof); } // Remove trailing newline let trailing_n = input.ends_with('\n'); let trailing_r; if trailing_n { input.pop(); trailing_r = input.ends_with('\r'); if trailing_r { input.pop(); } } else { trailing_r = false; } input = apply_backspace_direct(&input); match validator.as_ref() { None => return Ok(input), Some(v) => { let mut ctx = input.as_str(); let mut ctx = validate::ValidationContext::new(&mut ctx); match v.validate(&mut ctx)? { validate::ValidationResult::Valid(msg) => { if let Some(msg) = msg { writer.write_all(msg.as_bytes())?; } return Ok(input); } validate::ValidationResult::Invalid(Some(msg)) => { writer.write_all(msg.as_bytes())?; } validate::ValidationResult::Incomplete => { // Add newline and keep on taking input if trailing_r { input.push('\r'); } if trailing_n { input.push('\n'); } } _ => {} } } } } } /// 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 dyn History, history_index: usize, } impl<'h> Context<'h> { /// Constructor. Visible for testing. #[must_use] pub fn new(history: &'h dyn History) -> Self { Context { history, history_index: history.len(), } } /// Return an immutable reference to the history object. #[must_use] pub fn history(&self) -> &dyn History { self.history } /// The history index we are currently editing #[must_use] pub fn history_index(&self) -> usize { self.history_index } } /// Line editor #[must_use] pub struct Editor { term: Terminal, history: I, helper: Option, kill_ring: KillRing, config: Config, custom_bindings: Bindings, } /// Default editor with no helper and `DefaultHistory` pub type DefaultEditor = Editor<(), DefaultHistory>; #[allow(clippy::new_without_default)] impl Editor { /// Create an editor with the default configuration pub fn new() -> Result { Self::with_config(Config::default()) } /// Create an editor with a specific configuration. pub fn with_config(config: Config) -> Result { Self::with_history(config, DefaultHistory::with_config(config)) } } impl Editor { /// Create an editor with a custom history impl. pub fn with_history(config: Config, history: I) -> Result { let term = Terminal::new( config.color_mode(), config.behavior(), config.tab_stop(), config.bell_style(), config.enable_bracketed_paste(), )?; Ok(Self { term, history, helper: None, kill_ring: KillRing::new(60), config, custom_bindings: Bindings::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(io::stdin().lock(), io::stderr(), &self.helper) } else if self.term.is_input_tty() { let (original_mode, term_key_map) = self.term.enable_raw_mode()?; let guard = Guard(&original_mode); let user_input = self.readline_edit(prompt, initial, &original_mode, term_key_map); if self.config.auto_add_history() { if let Ok(ref line) = user_input { self.add_history_entry(line.as_str())?; } } drop(guard); // disable_raw_mode(original_mode)?; self.term.writeln()?; user_input } else { debug!(target: "rustyline", "stdin is not a tty"); // Not a tty: read from file / pipe. readline_direct(io::stdin().lock(), io::stderr(), &self.helper) } } /// 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( &mut self, prompt: &str, initial: Option<(&str, &str)>, original_mode: &tty::Mode, term_key_map: tty::KeyMap, ) -> Result { let mut stdout = self.term.create_writer(); self.kill_ring.reset(); // TODO recreate a new kill ring vs reset let ctx = Context::new(&self.history); let mut s = State::new(&mut stdout, prompt, self.helper.as_ref(), ctx); let mut input_state = InputState::new(&self.config, &self.custom_bindings); if let Some((left, right)) = initial { s.line.update( (left.to_owned() + right).as_ref(), left.len(), &mut s.changes, ); } let mut rdr = self.term.create_reader(&self.config, term_key_map); if self.term.is_output_tty() && self.config.check_cursor_position() { if let Err(e) = s.move_cursor_at_leftmost(&mut rdr) { if let ReadlineError::WindowResized = e { s.out.update_size(); } else { return Err(e); } } } s.refresh_line()?; loop { let mut cmd = s.next_cmd(&mut input_state, &mut rdr, false, false)?; if cmd.should_reset_kill_ring() { self.kill_ring.reset(); } // First trigger commands that need extra input if cmd == Cmd::Complete && s.helper.is_some() { let next = complete_line(&mut rdr, &mut s, &mut input_state, &self.config)?; if let Some(next) = next { cmd = next; } else { continue; } } if cmd == Cmd::ReverseSearchHistory { // Search history backward let next = reverse_incremental_search(&mut rdr, &mut s, &mut input_state, &self.history)?; if let Some(next) = next { cmd = next; } else { continue; } } #[cfg(unix)] if cmd == Cmd::Suspend { original_mode.disable_raw_mode()?; tty::suspend()?; let _ = self.term.enable_raw_mode()?; // TODO original_mode may have changed s.out.update_size(); // window may have been resized s.refresh_line()?; continue; } #[cfg(unix)] if cmd == Cmd::QuotedInsert { // Quoted insert let c = rdr.next_char()?; s.edit_insert(c, 1)?; continue; } #[cfg(windows)] if cmd == Cmd::PasteFromClipboard { let clipboard = rdr.read_pasted_text()?; s.edit_yank(&input_state, &clipboard[..], Anchor::Before, 1)?; } // Tiny test quirk #[cfg(test)] if matches!( cmd, Cmd::AcceptLine | Cmd::Newline | Cmd::AcceptOrInsertLine { .. } ) { self.term.cursor = s.layout.cursor.col; } // Execute things can be done solely on a state object match command::execute(cmd, &mut s, &input_state, &mut self.kill_ring, &self.config)? { command::Status::Proceed => continue, command::Status::Submit => break, } } // Move to end, in case cursor was in the middle of the line, so that // next thing application prints goes after the input s.forced_refresh = true; s.edit_move_buffer_end()?; s.forced_refresh = false; if cfg!(windows) { let _ = original_mode; // silent warning } Ok(s.line.into_string()) } /// Load the history from the specified file. pub fn load_history + ?Sized>(&mut self, path: &P) -> Result<()> { self.history.load(path.as_ref()) } /// Save the history in the specified file. pub fn save_history + ?Sized>(&mut self, path: &P) -> Result<()> { self.history.save(path.as_ref()) } /// Append new entries in the specified file. pub fn append_history + ?Sized>(&mut self, path: &P) -> Result<()> { self.history.append(path.as_ref()) } /// Add a new entry in the history. pub fn add_history_entry + Into>(&mut self, line: S) -> Result { self.history.add(line.as_ref()) } /// Clear history. pub fn clear_history(&mut self) -> Result<()> { self.history.clear() } /// Return a mutable reference to the history object. pub fn history_mut(&mut self) -> &mut I { &mut self.history } /// Return an immutable reference to the history object. pub fn history(&self) -> &I { &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. #[cfg(feature = "custom-bindings")] #[cfg_attr(docsrs, doc(cfg(feature = "custom-bindings")))] pub fn bind_sequence, R: Into>( &mut self, key_seq: E, handler: R, ) -> Option { self.custom_bindings .insert(Event::normalize(key_seq.into()), handler.into()) } /// Remove a binding for the given sequence. #[cfg(feature = "custom-bindings")] #[cfg_attr(docsrs, doc(cfg(feature = "custom-bindings")))] pub fn unbind_sequence>(&mut self, key_seq: E) -> Option { self.custom_bindings .remove(&Event::normalize(key_seq.into())) } /// Returns an iterator over edited lines. /// Iterator ends at [EOF](ReadlineError::Eof). /// ``` /// let mut rl = rustyline::DefaultEditor::new()?; /// for readline in rl.iter("> ") { /// match readline { /// Ok(line) => { /// println!("Line: {}", line); /// } /// Err(err) => { /// println!("Error: {:?}", err); /// break; /// } /// } /// } /// # Ok::<(), rustyline::error::ReadlineError>(()) /// ``` pub fn iter<'a>(&'a mut self, prompt: &'a str) -> impl Iterator> + 'a { Iter { editor: self, prompt, } } /// 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 } } /// Clear the screen. pub fn clear_screen(&mut self) -> Result<()> { if self.term.is_output_tty() { let mut out = self.term.create_writer(); out.clear_screen() } else { Ok(()) } } /// Create an external printer pub fn create_external_printer(&mut self) -> Result<::ExternalPrinter> { self.term.create_external_printer() } /// Change cursor visibility pub fn set_cursor_visibility( &mut self, visible: bool, ) -> Result::CursorGuard>> { self.term.set_cursor_visibility(visible) } } 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) -> Result<()> { 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) -> Result<()> { 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() } } struct Iter<'a, H: Helper, I: History> { editor: &'a mut Editor, prompt: &'a str, } impl<'a, H: Helper, I: History> Iterator for Iter<'a, H, I> { type Item = Result; fn next(&mut self) -> Option> { let readline = self.editor.readline(self.prompt); match readline { Ok(l) => Some(Ok(l)), Err(ReadlineError::Eof) => None, e @ Err(_) => Some(e), } } } #[cfg(test)] #[macro_use] extern crate assert_matches; #[cfg(test)] mod test; #[cfg(doctest)] doc_comment::doctest!("../README.md"); rustyline-13.0.0/src/line_buffer.rs000064400000000000000000001640231046102023000153660ustar 00000000000000//! Line buffer with current cursor position use crate::keymap::{At, CharSearch, Movement, RepeatCount, Word}; use std::cmp::min; use std::fmt; use std::iter; use std::ops::{Deref, Index, Range}; use std::string::Drain; use unicode_segmentation::UnicodeSegmentation; /// Default maximum buffer size for the line read pub(crate) const MAX_LINE: usize = 4096; pub(crate) const INDENT: &str = " "; /// Word's case change #[derive(Clone, Copy)] pub enum WordAction { /// Capitalize word Capitalize, /// lowercase word Lowercase, /// uppercase word Uppercase, } /// Delete (kill) direction #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum Direction { /// After cursor #[default] Forward, /// Before cursor Backward, } /// Listener to be notified when some text is deleted. pub trait DeleteListener { /// used to make the distinction between simple character(s) deletion and /// word(s)/line(s) deletion fn start_killing(&mut self) {} /// `string` deleted at `idx` index fn delete(&mut self, idx: usize, string: &str, dir: Direction); /// used to make the distinction between simple character(s) deletion and /// word(s)/line(s) deletion fn stop_killing(&mut self) {} } /// Listener to be notified when the line is modified. pub trait ChangeListener: DeleteListener { /// `c`har inserted at `idx` index fn insert_char(&mut self, idx: usize, c: char); /// `string` inserted at `idx` index fn insert_str(&mut self, idx: usize, string: &str); /// `old` text replaced by `new` text at `idx` index fn replace(&mut self, idx: usize, old: &str, new: &str); } pub(crate) struct NoListener; impl DeleteListener for NoListener { fn delete(&mut self, _idx: usize, _string: &str, _dir: Direction) {} } impl ChangeListener for NoListener { 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) {} } // TODO split / cache lines ? /// 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 } 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`. #[must_use] pub fn with_capacity(capacity: usize) -> Self { Self { buf: String::with_capacity(capacity), pos: 0, can_growth: false, } } /// 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) -> Self { let mut lb = Self::with_capacity(MAX_LINE); assert!(lb.insert_str(0, line, &mut NoListener)); lb.set_pos(pos); lb } /// Extracts a string slice containing the entire buffer. #[must_use] pub fn as_str(&self) -> &str { &self.buf } /// Converts a buffer into a `String` without copying or allocating. #[must_use] pub fn into_string(self) -> String { self.buf } /// Current cursor position (byte position) #[must_use] 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. #[must_use] pub fn len(&self) -> usize { self.buf.len() } /// Returns `true` if this buffer has a length of zero. #[must_use] 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, cl: &mut C) { assert!(pos <= buf.len()); let end = self.len(); self.drain(0..end, Direction::default(), cl); let max = self.buf.capacity(); if self.must_truncate(buf.len()) { self.insert_str(0, &buf[..max], cl); if pos > max { self.pos = max; } else { self.pos = pos; } } else { self.insert_str(0, buf, cl); self.pos = pos; } } fn end_of_line(&self) -> usize { if let Some(n) = self.buf[self.pos..].find('\n') { n + self.pos } else { self.buf.len() } } fn start_of_line(&self) -> usize { if let Some(i) = self.buf[..self.pos].rfind('\n') { // `i` is before the new line, e.g. at the end of the previous one. i + 1 } else { 0 } } /// Returns the character at current cursor position. pub(crate) fn grapheme_at_cursor(&self) -> Option<&str> { if self.pos == self.buf.len() { None } else { self.buf[self.pos..].graphemes(true).next() } } /// Returns the position of the character just after the current cursor /// position. #[must_use] 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, cl: &mut C, ) -> 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); cl.insert_char(self.pos, ch); } else { let text = iter::repeat(ch).take(n).collect::(); let pos = self.pos; self.insert_str(pos, &text, cl); } 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, cl: &mut C, ) -> 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, cl); } else { let text = text.repeat(n); self.insert_str(pos, &text, cl); } 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, cl: &mut C, ) -> Option { let end = self.pos; let start = end - yank_size; self.drain(start..end, Direction::default(), cl); self.pos -= yank_size; self.yank(text, 1, cl) } /// 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 buffer. pub fn move_buffer_start(&mut self) -> bool { if self.pos > 0 { self.pos = 0; true } else { false } } /// Move cursor to the end of the buffer. pub fn move_buffer_end(&mut self) -> bool { if self.pos == self.buf.len() { false } else { self.pos = self.buf.len(); true } } /// Move cursor to the start of the line. pub fn move_home(&mut self) -> bool { let start = self.start_of_line(); if self.pos > start { self.pos = start; true } else { false } } /// Move cursor to the end of the line. pub fn move_end(&mut self) -> bool { let end = self.end_of_line(); if self.pos == end { false } else { self.pos = end; true } } /// Is cursor at the end of input (whitespaces after cursor is discarded) #[must_use] pub fn is_end_of_input(&self) -> bool { self.pos >= self.buf.trim_end().len() } /// Delete the character at the right of the cursor without altering the /// cursor position. Basically this is what happens with the "Delete" /// keyboard key. /// Return the number of characters deleted. pub fn delete(&mut self, n: RepeatCount, dl: &mut D) -> Option { match self.next_pos(n) { Some(pos) => { let start = self.pos; let chars = self .drain(start..pos, Direction::Forward, dl) .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, dl: &mut D) -> bool { match self.prev_pos(n) { Some(pos) => { let end = self.pos; self.drain(pos..end, Direction::Backward, dl); self.pos = pos; true } None => false, } } /// Kill the text from point to the end of the line. pub fn kill_line(&mut self, dl: &mut D) -> bool { if !self.buf.is_empty() && self.pos < self.buf.len() { let start = self.pos; let end = self.end_of_line(); if start == end { self.delete(1, dl); } else { self.drain(start..end, Direction::Forward, dl); } true } else { false } } /// Kill the text from point to the end of the buffer. pub fn kill_buffer(&mut self, dl: &mut D) -> 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, dl); true } else { false } } /// Kill backward from point to the beginning of the line. pub fn discard_line(&mut self, dl: &mut D) -> bool { if self.pos > 0 && !self.buf.is_empty() { let start = self.start_of_line(); let end = self.pos; if end == start { self.backspace(1, dl) } else { self.drain(start..end, Direction::Backward, dl); self.pos = start; true } } else { false } } /// Kill backward from point to the beginning of the buffer. pub fn discard_buffer(&mut self, dl: &mut D) -> bool { if self.pos > 0 && !self.buf.is_empty() { let end = self.pos; self.drain(0..end, Direction::Backward, dl); self.pos = 0; true } else { false } } /// Exchange the char before cursor with the character at cursor. pub fn transpose_chars(&mut self, cl: &mut C) -> 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, cl).unwrap(); self.move_backward(1); self.yank(&chars, 1, cl); 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, dl: &mut D, ) -> bool { if let Some(pos) = self.prev_word_pos(self.pos, word_def, n) { let end = self.pos; self.drain(pos..end, Direction::Backward, dl); 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 } } /// Moves the cursor to the same column in the line above pub fn move_to_line_up(&mut self, n: RepeatCount) -> bool { match self.buf[..self.pos].rfind('\n') { Some(off) => { let column = self.buf[off + 1..self.pos].graphemes(true).count(); let mut dest_start = self.buf[..off].rfind('\n').map_or(0, |n| n + 1); let mut dest_end = off; for _ in 1..n { if dest_start == 0 { break; } dest_end = dest_start - 1; dest_start = self.buf[..dest_end].rfind('\n').map_or(0, |n| n + 1); } let gidx = self.buf[dest_start..dest_end] .grapheme_indices(true) .nth(column); self.pos = gidx.map_or(off, |(idx, _)| dest_start + idx); // if there's no enough columns true } None => false, } } /// N lines up starting from the current one /// /// Fails if the cursor is on the first line fn n_lines_up(&self, n: RepeatCount) -> Option<(usize, usize)> { let mut start = if let Some(off) = self.buf[..self.pos].rfind('\n') { off + 1 } else { return None; }; let end = self.buf[self.pos..] .find('\n') .map_or_else(|| self.buf.len(), |x| self.pos + x + 1); for _ in 0..n { if let Some(off) = self.buf[..start - 1].rfind('\n') { start = off + 1; } else { start = 0; break; } } Some((start, end)) } /// N lines down starting from the current one /// /// Fails if the cursor is on the last line fn n_lines_down(&self, n: RepeatCount) -> Option<(usize, usize)> { let mut end = if let Some(off) = self.buf[self.pos..].find('\n') { self.pos + off + 1 } else { return None; }; let start = self.buf[..self.pos].rfind('\n').unwrap_or(0); for _ in 0..n { if let Some(off) = self.buf[end..].find('\n') { end = end + off + 1; } else { end = self.buf.len(); break; }; } Some((start, end)) } /// Moves the cursor to the same column in the line above pub fn move_to_line_down(&mut self, n: RepeatCount) -> bool { match self.buf[self.pos..].find('\n') { Some(off) => { let line_start = self.buf[..self.pos].rfind('\n').map_or(0, |n| n + 1); let column = self.buf[line_start..self.pos].graphemes(true).count(); let mut dest_start = self.pos + off + 1; let mut dest_end = self.buf[dest_start..] .find('\n') .map_or_else(|| self.buf.len(), |v| dest_start + v); for _ in 1..n { if dest_end == self.buf.len() { break; } dest_start = dest_end + 1; dest_end = self.buf[dest_start..] .find('\n') .map_or_else(|| self.buf.len(), |v| dest_start + v); } self.pos = self.buf[dest_start..dest_end] .grapheme_indices(true) .nth(column) .map_or(dest_end, |(idx, _)| dest_start + idx); // if there's no enough columns debug_assert!(self.pos <= self.buf.len()); true } None => false, } } fn search_char_pos(&self, cs: CharSearch, n: RepeatCount) -> Option { let mut shift = 0; let search_result = match cs { 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 } } }; search_result.map(|pos| match cs { CharSearch::Backward(_) => pos, CharSearch::BackwardAfter(c) => pos + c.len_utf8(), CharSearch::Forward(_) => shift + pos, CharSearch::ForwardBefore(_) => { shift + pos - self.buf[..shift + pos] .chars() .next_back() .unwrap() .len_utf8() } }) } /// Move cursor to the matching character position. /// 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, dl: &mut D, ) -> 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, dl); true } else { false } } /// Delete range specified by `cs` search. pub fn delete_to( &mut self, cs: CharSearch, n: RepeatCount, dl: &mut D, ) -> 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, dl); } CharSearch::ForwardBefore(_) => { let start = self.pos; self.drain(start..pos, Direction::Forward, dl); } CharSearch::Forward(c) => { let start = self.pos; self.drain(start..pos + c.len_utf8(), Direction::Forward, dl); } }; true } else { false } } fn skip_whitespace(&self) -> Option { if self.pos == self.buf.len() { return None; } self.buf[self.pos..] .grapheme_indices(true) .find_map(|(i, ch)| { if ch.chars().all(char::is_alphanumeric) { Some(i) } else { None } }) .map(|i| i + self.pos) } /// Alter the next word. pub fn edit_word(&mut self, a: WordAction, cl: &mut C) -> 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(), cl) .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, cl); self.pos = start + result.len(); return true; } } false } /// Transpose two words pub fn transpose_words(&mut self, n: RepeatCount, cl: &mut C) -> 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(), cl) .collect::(); self.insert_str(w2_beg, &w1, cl); self.drain(w1_beg..w1_end, Direction::default(), cl); self.insert_str(w1_beg, &w2, cl); 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, cl: &mut C) { let start = range.start; cl.replace(start, self.buf.index(range.clone()), text); 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, cl: &mut C) -> bool { cl.insert_str(idx, s); 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, dl: &mut D) { self.set_pos(range.start); self.drain(range, Direction::default(), dl); } fn drain( &mut self, range: Range, dir: Direction, dl: &mut D, ) -> Drain<'_> { dl.delete(range.start, &self.buf[range.start..range.end], dir); self.buf.drain(range) } /// Return the content between current cursor position and `mvt` position. /// Return `None` when the buffer is empty or when the movement fails. #[must_use] pub fn copy(&self, mvt: &Movement) -> Option { if self.is_empty() { return None; } match *mvt { Movement::WholeLine => { let start = self.start_of_line(); let end = self.end_of_line(); if start == end { None } else { Some(self.buf[start..self.pos].to_owned()) } } Movement::BeginningOfLine => { let start = self.start_of_line(); if self.pos == start { None } else { Some(self.buf[start..self.pos].to_owned()) } } Movement::ViFirstPrint => { if self.pos == 0 { None } else { self.next_word_pos(0, At::Start, Word::Big, 1) .map(|pos| self.buf[pos..self.pos].to_owned()) } } Movement::EndOfLine => { let end = self.end_of_line(); if self.pos == end { None } else { Some(self.buf[self.pos..end].to_owned()) } } Movement::EndOfBuffer => { if self.pos == self.buf.len() { None } else { Some(self.buf[self.pos..].to_owned()) } } Movement::WholeBuffer => { if self.buf.is_empty() { None } else { Some(self.buf.clone()) } } Movement::BeginningOfBuffer => { if self.pos == 0 { None } else { Some(self.buf[..self.pos].to_owned()) } } Movement::BackwardWord(n, word_def) => self .prev_word_pos(self.pos, word_def, n) .map(|pos| self.buf[pos..self.pos].to_owned()), Movement::ForwardWord(n, at, word_def) => self .next_word_pos(self.pos, at, word_def, n) .map(|pos| self.buf[self.pos..pos].to_owned()), Movement::ViCharSearch(n, cs) => { let search_result = match cs { CharSearch::ForwardBefore(c) => self.search_char_pos(CharSearch::Forward(c), n), _ => self.search_char_pos(cs, n), }; search_result.map(|pos| match cs { CharSearch::Backward(_) | CharSearch::BackwardAfter(_) => { self.buf[pos..self.pos].to_owned() } CharSearch::ForwardBefore(_) => self.buf[self.pos..pos].to_owned(), CharSearch::Forward(c) => self.buf[self.pos..pos + c.len_utf8()].to_owned(), }) } Movement::BackwardChar(n) => self .prev_pos(n) .map(|pos| self.buf[pos..self.pos].to_owned()), Movement::ForwardChar(n) => self .next_pos(n) .map(|pos| self.buf[self.pos..pos].to_owned()), Movement::LineUp(n) => { if let Some((start, end)) = self.n_lines_up(n) { Some(self.buf[start..end].to_owned()) } else { None } } Movement::LineDown(n) => { if let Some((start, end)) = self.n_lines_down(n) { Some(self.buf[start..end].to_owned()) } else { None } } } } /// Kill range specified by `mvt`. pub fn kill(&mut self, mvt: &Movement, dl: &mut D) -> bool { let notify = !matches!(*mvt, Movement::ForwardChar(_) | Movement::BackwardChar(_)); if notify { dl.start_killing(); } let killed = match *mvt { Movement::ForwardChar(n) => { // Delete (forward) `n` characters at point. self.delete(n, dl).is_some() } Movement::BackwardChar(n) => { // Delete `n` characters backward. self.backspace(n, dl) } Movement::EndOfLine => { // Kill the text from point to the end of the line. self.kill_line(dl) } Movement::WholeLine => { self.move_home(); self.kill_line(dl) } Movement::BeginningOfLine => { // Kill backward from point to the beginning of the line. self.discard_line(dl) } Movement::BackwardWord(n, word_def) => { // kill `n` words backward (until start of word) self.delete_prev_word(word_def, n, dl) } Movement::ForwardWord(n, at, word_def) => { // kill `n` words forward (until start/end of word) self.delete_word(at, word_def, n, dl) } Movement::ViCharSearch(n, cs) => self.delete_to(cs, n, dl), Movement::LineUp(n) => { if let Some((start, end)) = self.n_lines_up(n) { self.delete_range(start..end, dl); true } else { false } } Movement::LineDown(n) => { if let Some((start, end)) = self.n_lines_down(n) { self.delete_range(start..end, dl); true } else { false } } Movement::ViFirstPrint => { false // TODO } Movement::EndOfBuffer => { // Kill the text from point to the end of the buffer. self.kill_buffer(dl) } Movement::BeginningOfBuffer => { // Kill backward from point to the beginning of the buffer. self.discard_buffer(dl) } Movement::WholeBuffer => { self.move_buffer_start(); self.kill_buffer(dl) } }; if notify { dl.stop_killing(); } killed } /// Indent range specified by `mvt`. pub fn indent( &mut self, mvt: &Movement, amount: usize, dedent: bool, cl: &mut C, ) -> bool { let pair = match *mvt { // All inline operators are the same: indent current line Movement::WholeLine | Movement::BeginningOfLine | Movement::ViFirstPrint | Movement::EndOfLine | Movement::BackwardChar(..) | Movement::ForwardChar(..) | Movement::ViCharSearch(..) => Some((self.pos, self.pos)), Movement::EndOfBuffer => Some((self.pos, self.buf.len())), Movement::WholeBuffer => Some((0, self.buf.len())), Movement::BeginningOfBuffer => Some((0, self.pos)), Movement::BackwardWord(n, word_def) => self .prev_word_pos(self.pos, word_def, n) .map(|pos| (pos, self.pos)), Movement::ForwardWord(n, at, word_def) => self .next_word_pos(self.pos, at, word_def, n) .map(|pos| (self.pos, pos)), Movement::LineUp(n) => self.n_lines_up(n), Movement::LineDown(n) => self.n_lines_down(n), }; let (start, end) = pair.unwrap_or((self.pos, self.pos)); let start = self.buf[..start].rfind('\n').map_or(0, |pos| pos + 1); let end = self.buf[end..] .rfind('\n') .map_or_else(|| self.buf.len(), |pos| end + pos); let mut index = start; if dedent { for line in self.buf[start..end].to_string().split('\n') { let max = line.len() - line.trim_start().len(); let deleting = min(max, amount); self.drain(index..index + deleting, Direction::default(), cl); if self.pos >= index { if self.pos.saturating_sub(index) < deleting { // don't wrap into the previous line self.pos = index; } else { self.pos -= deleting; } } index += line.len() + 1 - deleting; } } else { for line in self.buf[start..end].to_string().split('\n') { for off in (0..amount).step_by(INDENT.len()) { self.insert_str(index, &INDENT[..min(amount - off, INDENT.len())], cl); } if self.pos >= index { self.pos += amount; } index += amount + line.len() + 1; } } true } } 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, NoListener, WordAction, MAX_LINE, }; use crate::keymap::{At, CharSearch, Word}; struct Listener { deleted_str: Option, } impl Listener { fn new() -> Listener { Listener { deleted_str: None } } 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 delete(&mut self, _: usize, string: &str, _: Direction) { self.deleted_str = Some(string.to_owned()); } } 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); assert_eq!(7, s.len()); let pos = s.next_pos(1); assert_eq!(Some(4), pos); let s = LineBuffer::init("ö̲g̈", 4); let pos = s.next_pos(1); assert_eq!(Some(7), pos); } #[test] fn prev_pos() { let s = LineBuffer::init("ö̲g̈", 4); assert_eq!(7, s.len()); let pos = s.prev_pos(1); assert_eq!(Some(0), pos); let s = LineBuffer::init("ö̲g̈", 7); 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, &mut NoListener).unwrap(); assert_eq!("α", s.buf); assert_eq!(2, s.pos); assert!(push); let push = s.insert('ß', 1, &mut NoListener).unwrap(); assert_eq!("αß", s.buf); assert_eq!(4, s.pos); assert!(push); s.pos = 0; let push = s.insert('γ', 1, &mut NoListener).unwrap(); assert_eq!("γαß", s.buf); assert_eq!(2, s.pos); assert!(!push); } #[test] fn yank_after() { let mut s = LineBuffer::init("αß", 2); s.move_forward(1); let ok = s.yank("γδε", 1, &mut NoListener); assert_eq!(Some(true), ok); assert_eq!("αßγδε", s.buf); assert_eq!(10, s.pos); } #[test] fn yank_before() { let mut s = LineBuffer::init("αε", 2); let ok = s.yank("ßγδ", 1, &mut NoListener); assert_eq!(Some(false), ok); assert_eq!("αßγδε", s.buf); assert_eq!(8, s.pos); } #[test] fn moves() { let mut s = LineBuffer::init("αß", 4); let ok = s.move_backward(1); assert_eq!("αß", s.buf); assert_eq!(2, s.pos); assert!(ok); let ok = s.move_forward(1); assert_eq!("αß", s.buf); assert_eq!(4, s.pos); assert!(ok); let ok = s.move_home(); assert_eq!("αß", s.buf); assert_eq!(0, s.pos); assert!(ok); let ok = s.move_end(); assert_eq!("αß", s.buf); assert_eq!(4, s.pos); assert!(ok); } #[test] fn move_home_end_multiline() { let text = "αa\nsdf ßc\nasdf"; let mut s = LineBuffer::init(text, 7); let ok = s.move_home(); assert_eq!(text, s.buf); assert_eq!(4, s.pos); assert!(ok); let ok = s.move_home(); assert_eq!(text, s.buf); assert_eq!(4, s.pos); assert!(!ok); let ok = s.move_end(); assert_eq!(text, s.buf); assert_eq!(11, s.pos); assert!(ok); let ok = s.move_end(); assert_eq!(text, s.buf); assert_eq!(11, s.pos); assert!(!ok); } #[test] fn move_buffer_multiline() { let text = "αa\nsdf ßc\nasdf"; let mut s = LineBuffer::init(text, 7); let ok = s.move_buffer_start(); assert_eq!(text, s.buf); assert_eq!(0, s.pos); assert!(ok); let ok = s.move_buffer_start(); assert_eq!(text, s.buf); assert_eq!(0, s.pos); assert!(!ok); let ok = s.move_buffer_end(); assert_eq!(text, s.buf); assert_eq!(text.len(), s.pos); assert!(ok); let ok = s.move_buffer_end(); assert_eq!(text, s.buf); assert_eq!(text.len(), s.pos); assert!(!ok); } #[test] fn move_grapheme() { let mut s = LineBuffer::init("ag̈", 4); assert_eq!(4, s.len()); let ok = s.move_backward(1); assert!(ok); assert_eq!(1, s.pos); let ok = s.move_forward(1); assert!(ok); assert_eq!(4, s.pos); } #[test] fn delete() { let mut cl = Listener::new(); let mut s = LineBuffer::init("αß", 2); let chars = s.delete(1, &mut cl); assert_eq!("α", s.buf); assert_eq!(2, s.pos); assert_eq!(Some("ß".to_owned()), chars); let ok = s.backspace(1, &mut cl); assert_eq!("", s.buf); assert_eq!(0, s.pos); assert!(ok); cl.assert_deleted_str_eq("α"); } #[test] fn kill() { let mut cl = Listener::new(); let mut s = LineBuffer::init("αßγδε", 6); let ok = s.kill_line(&mut cl); assert_eq!("αßγ", s.buf); assert_eq!(6, s.pos); assert!(ok); cl.assert_deleted_str_eq("δε"); s.pos = 4; let ok = s.discard_line(&mut cl); assert_eq!("γ", s.buf); assert_eq!(0, s.pos); assert!(ok); cl.assert_deleted_str_eq("αß"); } #[test] fn kill_multiline() { let mut cl = Listener::new(); let mut s = LineBuffer::init("αß\nγδ 12\nε f4", 7); let ok = s.kill_line(&mut cl); assert_eq!("αß\nγ\nε f4", s.buf); assert_eq!(7, s.pos); assert!(ok); cl.assert_deleted_str_eq("δ 12"); let ok = s.kill_line(&mut cl); assert_eq!("αß\nγε f4", s.buf); assert_eq!(7, s.pos); assert!(ok); cl.assert_deleted_str_eq("\n"); let ok = s.kill_line(&mut cl); assert_eq!("αß\nγ", s.buf); assert_eq!(7, s.pos); assert!(ok); cl.assert_deleted_str_eq("ε f4"); let ok = s.kill_line(&mut cl); assert_eq!(7, s.pos); assert!(!ok); } #[test] fn discard_multiline() { let mut cl = Listener::new(); let mut s = LineBuffer::init("αß\nc γδε", 9); let ok = s.discard_line(&mut cl); assert_eq!("αß\nδε", s.buf); assert_eq!(5, s.pos); assert!(ok); cl.assert_deleted_str_eq("c γ"); let ok = s.discard_line(&mut cl); assert_eq!("αßδε", s.buf); assert_eq!(4, s.pos); assert!(ok); cl.assert_deleted_str_eq("\n"); let ok = s.discard_line(&mut cl); assert_eq!("δε", s.buf); assert_eq!(0, s.pos); assert!(ok); cl.assert_deleted_str_eq("αß"); let ok = s.discard_line(&mut cl); assert_eq!(0, s.pos); assert!(!ok); } #[test] fn transpose() { let mut s = LineBuffer::init("aßc", 1); let ok = s.transpose_chars(&mut NoListener); assert_eq!("ßac", s.buf); assert_eq!(3, s.pos); assert!(ok); s.buf = String::from("aßc"); s.pos = 3; let ok = s.transpose_chars(&mut NoListener); assert_eq!("acß", s.buf); assert_eq!(4, s.pos); assert!(ok); s.buf = String::from("aßc"); s.pos = 4; let ok = s.transpose_chars(&mut NoListener); assert_eq!("acß", s.buf); assert_eq!(4, s.pos); assert!(ok); } #[test] fn move_to_prev_word() { let mut s = LineBuffer::init("a ß c", 6); // 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!(ok); assert!(s.move_end()); // after 'c' assert_eq!(7, s.pos); let ok = s.move_to_prev_word(Word::Emacs, 1); assert!(ok); assert_eq!(6, s.pos); // before 'c' let ok = s.move_to_prev_word(Word::Emacs, 2); assert!(ok); assert_eq!(0, s.pos); } #[test] fn move_to_prev_vi_word() { let mut s = LineBuffer::init("alpha ,beta/rho; mu", 19); 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); 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); let ok = s.move_to(CharSearch::ForwardBefore('ε'), 1); assert!(ok); assert_eq!(6, s.pos); let mut s = LineBuffer::init("αßγδε", 2); let ok = s.move_to(CharSearch::Forward('ε'), 1); assert!(ok); assert_eq!(8, s.pos); let mut s = LineBuffer::init("αßγδε", 2); let ok = s.move_to(CharSearch::Forward('ε'), 10); assert!(ok); assert_eq!(8, s.pos); } #[test] fn move_to_backward() { let mut s = LineBuffer::init("αßγδε", 8); let ok = s.move_to(CharSearch::BackwardAfter('ß'), 1); assert!(ok); assert_eq!(4, s.pos); let mut s = LineBuffer::init("αßγδε", 8); let ok = s.move_to(CharSearch::Backward('ß'), 1); assert!(ok); assert_eq!(2, s.pos); } #[test] fn delete_prev_word() { let mut cl = Listener::new(); let mut s = LineBuffer::init("a ß c", 6); let ok = s.delete_prev_word(Word::Big, 1, &mut cl); assert_eq!("a c", s.buf); assert_eq!(2, s.pos); assert!(ok); cl.assert_deleted_str_eq("ß "); } #[test] fn move_to_next_word() { let mut s = LineBuffer::init("a ß c", 1); // after 'a' let ok = s.move_to_next_word(At::AfterEnd, Word::Emacs, 1); assert_eq!("a ß c", s.buf); assert!(ok); assert_eq!(4, s.pos); // after 'ß' let ok = s.move_to_next_word(At::AfterEnd, Word::Emacs, 1); assert!(ok); assert_eq!(7, s.pos); // after 'c' s.move_home(); let ok = s.move_to_next_word(At::AfterEnd, Word::Emacs, 1); assert!(ok); assert_eq!(1, s.pos); // after 'a' let ok = s.move_to_next_word(At::AfterEnd, Word::Emacs, 2); assert!(ok); assert_eq!(7, s.pos); // after 'c' } #[test] fn move_to_end_of_word() { let mut s = LineBuffer::init("a ßeta c", 1); let ok = s.move_to_next_word(At::BeforeEnd, Word::Vi, 1); assert_eq!("a ßeta c", s.buf); assert_eq!(6, s.pos); assert!(ok); } #[test] fn move_to_end_of_vi_word() { let mut s = LineBuffer::init("alpha ,beta/rho; mu", 0); 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); 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); let ok = s.move_to_next_word(At::Start, Word::Emacs, 1); assert_eq!("a ß c", s.buf); assert_eq!(6, s.pos); assert!(ok); } #[test] fn move_to_start_of_vi_word() { let mut s = LineBuffer::init("alpha ,beta/rho; mu", 0); 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); 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 mut cl = Listener::new(); let mut s = LineBuffer::init("a ß c", 1); let ok = s.delete_word(At::AfterEnd, Word::Emacs, 1, &mut cl); assert_eq!("a c", s.buf); assert_eq!(1, s.pos); assert!(ok); cl.assert_deleted_str_eq(" ß"); let mut s = LineBuffer::init("test", 0); let ok = s.delete_word(At::AfterEnd, Word::Vi, 1, &mut cl); assert_eq!("", s.buf); assert_eq!(0, s.pos); assert!(ok); cl.assert_deleted_str_eq("test"); } #[test] fn delete_til_start_of_word() { let mut cl = Listener::new(); let mut s = LineBuffer::init("a ß c", 2); let ok = s.delete_word(At::Start, Word::Emacs, 1, &mut cl); assert_eq!("a c", s.buf); assert_eq!(2, s.pos); assert!(ok); cl.assert_deleted_str_eq("ß "); } #[test] fn delete_to_forward() { let mut cl = Listener::new(); let mut s = LineBuffer::init("αßγδε", 2); let ok = s.delete_to(CharSearch::ForwardBefore('ε'), 1, &mut cl); assert!(ok); cl.assert_deleted_str_eq("ßγδ"); assert_eq!("αε", s.buf); assert_eq!(2, s.pos); let mut s = LineBuffer::init("αßγδε", 2); let ok = s.delete_to(CharSearch::Forward('ε'), 1, &mut cl); assert!(ok); cl.assert_deleted_str_eq("ßγδε"); assert_eq!("α", s.buf); assert_eq!(2, s.pos); } #[test] fn delete_to_backward() { let mut cl = Listener::new(); let mut s = LineBuffer::init("αßγδε", 8); let ok = s.delete_to(CharSearch::BackwardAfter('α'), 1, &mut cl); assert!(ok); cl.assert_deleted_str_eq("ßγδ"); assert_eq!("αε", s.buf); assert_eq!(2, s.pos); let mut s = LineBuffer::init("αßγδε", 8); let ok = s.delete_to(CharSearch::Backward('ß'), 1, &mut cl); assert!(ok); cl.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); assert!(s.edit_word(WordAction::Uppercase, &mut NoListener)); assert_eq!("a SSETA c", s.buf); assert_eq!(7, s.pos); let mut s = LineBuffer::init("a ßetA c", 1); assert!(s.edit_word(WordAction::Lowercase, &mut NoListener)); assert_eq!("a ßeta c", s.buf); assert_eq!(7, s.pos); let mut s = LineBuffer::init("a ßETA c", 1); assert!(s.edit_word(WordAction::Capitalize, &mut NoListener)); assert_eq!("a SSeta c", s.buf); assert_eq!(7, s.pos); let mut s = LineBuffer::init("test", 1); assert!(s.edit_word(WordAction::Capitalize, &mut NoListener)); assert_eq!("tEst", s.buf); assert_eq!(4, s.pos); } #[test] fn transpose_words() { let mut s = LineBuffer::init("ßeta / δelta__", 15); assert!(s.transpose_words(1, &mut NoListener)); assert_eq!("δelta__ / ßeta", s.buf); assert_eq!(16, s.pos); let mut s = LineBuffer::init("ßeta / δelta", 14); assert!(s.transpose_words(1, &mut NoListener)); assert_eq!("δelta / ßeta", s.buf); assert_eq!(14, s.pos); let mut s = LineBuffer::init(" / δelta", 8); assert!(!s.transpose_words(1, &mut NoListener)); let mut s = LineBuffer::init("ßeta / __", 9); assert!(!s.transpose_words(1, &mut NoListener)); } #[test] fn move_by_line() { let text = "aa123\nsdf bc\nasdf"; let mut s = LineBuffer::init(text, 14); // move up let ok = s.move_to_line_up(1); assert_eq!(7, s.pos); assert!(ok); let ok = s.move_to_line_up(1); assert_eq!(1, s.pos); assert!(ok); let ok = s.move_to_line_up(1); assert_eq!(1, s.pos); assert!(!ok); // move down let ok = s.move_to_line_down(1); assert_eq!(7, s.pos); assert!(ok); let ok = s.move_to_line_down(1); assert_eq!(14, s.pos); assert!(ok); let ok = s.move_to_line_down(1); assert_eq!(14, s.pos); assert!(!ok); // move by multiple steps let ok = s.move_to_line_up(2); assert_eq!(1, s.pos); assert!(ok); let ok = s.move_to_line_down(2); assert_eq!(14, s.pos); assert!(ok); } #[test] fn test_send() { fn assert_send() {} assert_send::(); } #[test] fn test_sync() { fn assert_sync() {} assert_sync::(); } } rustyline-13.0.0/src/sqlite_history.rs000064400000000000000000000517201046102023000161670ustar 00000000000000//! History impl. based on SQLite use std::borrow::Cow; use std::cell::Cell; use std::path::{Path, PathBuf}; use rusqlite::{Connection, DatabaseName, OptionalExtension}; use crate::history::SearchResult; use crate::{Config, History, HistoryDuplicates, ReadlineError, Result, SearchDirection}; /// History stored in an SQLite database. pub struct SQLiteHistory { max_len: usize, ignore_space: bool, ignore_dups: bool, path: Option, // None => memory conn: Connection, /* we need to keep a connection opened at least for in memory * database and also for cached statement(s) */ session_id: usize, // 0 means no new entry added row_id: Cell, // max entry id } /* https://sqlite.org/autoinc.html If no ROWID is specified on the insert, or if the specified ROWID has a value of NULL, then an appropriate ROWID is created automatically. The usual algorithm is to give the newly created row a ROWID that is one larger than the largest ROWID in the table prior to the insert. If the table is initially empty, then a ROWID of 1 is used. If the largest ROWID is equal to the largest possible integer (9223372036854775807) then the database engine starts picking positive candidate ROWIDs at random until it finds one that is not previously used. https://sqlite.org/lang_vacuum.html The VACUUM command may change the ROWIDs of entries in any tables that do not have an explicit INTEGER PRIMARY KEY. */ impl SQLiteHistory { /// Transient in-memory database pub fn with_config(config: Config) -> Result where Self: Sized, { Self::new(config, None) } /// Open specified database pub fn open + ?Sized>(config: Config, path: &P) -> Result { Self::new(config, normalize(path.as_ref())) } fn new(config: Config, path: Option) -> Result { let conn = conn(path.as_ref())?; let mut sh = SQLiteHistory { max_len: config.max_history_size(), ignore_space: config.history_ignore_space(), // not strictly consecutive... ignore_dups: config.history_duplicates() == HistoryDuplicates::IgnoreConsecutive, path, conn, session_id: 0, row_id: Cell::new(0), }; sh.check_schema()?; Ok(sh) } fn is_mem_or_temp(&self) -> bool { match self.path { None => true, Some(ref p) => is_mem_or_temp(p), } } fn reset(&mut self, path: &Path) -> Result { self.path = normalize(path); self.session_id = 0; self.row_id.set(0); Ok(std::mem::replace(&mut self.conn, conn(self.path.as_ref())?)) } fn update_row_id(&mut self) -> Result<()> { self.row_id.set(self.conn.query_row( "SELECT ifnull(max(rowid), 0) FROM history;", [], |r| r.get(0), )?); Ok(()) } fn check_schema(&mut self) -> Result<()> { let user_version: i32 = self .conn .pragma_query_value(None, "user_version", |r| r.get(0))?; if user_version <= 0 { self.conn.execute_batch( " BEGIN EXCLUSIVE; PRAGMA auto_vacuum = INCREMENTAL; CREATE TABLE session ( id INTEGER PRIMARY KEY NOT NULL, timestamp REAL NOT NULL DEFAULT (julianday('now')) ) STRICT; -- user, host, pid CREATE TABLE history ( --id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, session_id INTEGER NOT NULL, entry TEXT NOT NULL, timestamp REAL NOT NULL DEFAULT (julianday('now')), FOREIGN KEY (session_id) REFERENCES session(id) ON DELETE CASCADE ) STRICT; CREATE VIRTUAL TABLE fts USING fts4(content=history, entry); CREATE TRIGGER history_bu BEFORE UPDATE ON history BEGIN DELETE FROM fts WHERE docid=old.rowid; END; CREATE TRIGGER history_bd BEFORE DELETE ON history BEGIN DELETE FROM fts WHERE docid=old.rowid; END; CREATE TRIGGER history_au AFTER UPDATE ON history BEGIN INSERT INTO fts (docid, entry) VALUES (new.rowid, new.entry); END; CREATE TRIGGER history_ai AFTER INSERT ON history BEGIN INSERT INTO fts (docid, entry) VALUES(new.rowid, new.entry); END; PRAGMA user_version = 1; COMMIT; ", )? } self.conn.pragma_update(None, "foreign_keys", 1)?; if self.ignore_dups || user_version > 0 { self.set_ignore_dups()?; } if self.row_id.get() == 0 && user_version > 0 { self.update_row_id()?; } Ok(()) } fn set_ignore_dups(&mut self) -> Result<()> { if self.ignore_dups { // TODO Validate: ignore dups only in the same session_id ? self.conn.execute_batch( "CREATE UNIQUE INDEX IF NOT EXISTS ignore_dups ON history(entry, session_id);", )?; } else { self.conn .execute_batch("DROP INDEX IF EXISTS ignore_dups;")?; } Ok(()) } fn create_session(&mut self) -> Result<()> { if self.session_id == 0 { self.check_schema()?; self.session_id = self.conn.query_row( "INSERT INTO session (id) VALUES (NULL) RETURNING id;", [], |r| r.get(0), )?; } Ok(()) } fn ignore(&self, line: &str) -> bool { if self.max_len == 0 { return true; } if line.is_empty() || (self.ignore_space && line.chars().next().map_or(true, char::is_whitespace)) { return true; } // ignore_dups => SQLITE_CONSTRAINT_UNIQUE false } fn add_entry(&mut self, line: &str) -> Result { // ignore SQLITE_CONSTRAINT_UNIQUE let mut stmt = self.conn.prepare_cached( "INSERT OR REPLACE INTO history (session_id, entry) VALUES (?1, ?2) RETURNING rowid;", )?; if let Some(row_id) = stmt .query_row((self.session_id, line), |r| r.get(0)) .optional()? { self.row_id.set(row_id); Ok(true) } else { Ok(false) } } fn search_match( &self, term: &str, start: usize, dir: SearchDirection, start_with: bool, ) -> Result> { if term.is_empty() || start >= self.len() { return Ok(None); } let start = start + 1; // first rowid is 1 let query = match (dir, start_with) { (SearchDirection::Forward, true) => { "SELECT docid, entry FROM fts WHERE entry MATCH '^' || ?1 || '*' AND docid >= ?2 \ ORDER BY docid ASC LIMIT 1;" } (SearchDirection::Forward, false) => { "SELECT docid, entry, offsets(fts) FROM fts WHERE entry MATCH ?1 || '*' AND docid \ >= ?2 ORDER BY docid ASC LIMIT 1;" } (SearchDirection::Reverse, true) => { "SELECT docid, entry FROM fts WHERE entry MATCH '^' || ?1 || '*' AND docid <= ?2 \ ORDER BY docid DESC LIMIT 1;" } (SearchDirection::Reverse, false) => { "SELECT docid, entry, offsets(fts) FROM fts WHERE entry MATCH ?1 || '*' AND docid \ <= ?2 ORDER BY docid DESC LIMIT 1;" } }; let mut stmt = self.conn.prepare_cached(query)?; stmt.query_row((term, start), |r| { let rowid = r.get::<_, usize>(0)?; if rowid > self.row_id.get() { self.row_id.set(rowid); } Ok(SearchResult { entry: Cow::Owned(r.get(1)?), idx: rowid - 1, // rowid - 1 pos: if start_with { term.len() } else { offset(r.get(2)?) }, }) }) .optional() .map_err(ReadlineError::from) } } impl History for SQLiteHistory { /// rowid <> index fn get(&self, index: usize, dir: SearchDirection) -> Result> { let rowid = index + 1; // first rowid is 1 if self.is_empty() { return Ok(None); } // rowid may not be sequential let query = match dir { SearchDirection::Forward => { "SELECT rowid, entry FROM history WHERE rowid >= ?1 ORDER BY rowid ASC LIMIT 1;" } SearchDirection::Reverse => { "SELECT rowid, entry FROM history WHERE rowid <= ?1 ORDER BY rowid DESC LIMIT 1;" } }; let mut stmt = self.conn.prepare_cached(query)?; stmt.query_row([rowid], |r| { let rowid = r.get::<_, usize>(0)?; if rowid > self.row_id.get() { self.row_id.set(rowid); } Ok(SearchResult { entry: Cow::Owned(r.get(1)?), idx: rowid - 1, pos: 0, }) }) .optional() .map_err(ReadlineError::from) } fn add(&mut self, line: &str) -> Result { if self.ignore(line) { return Ok(false); } // Do not create a session until the first entry is added. self.create_session()?; self.add_entry(line) } fn add_owned(&mut self, line: String) -> Result { self.add(line.as_str()) } /// This is not really the length fn len(&self) -> usize { self.row_id.get() } fn is_empty(&self) -> bool { self.row_id.get() == 0 } fn set_max_len(&mut self, len: usize) -> Result<()> { // TODO call this method on save ? before append ? // FIXME rowid may not be sequential let count: usize = self .conn .query_row("SELECT count(1) FROM history;", [], |r| r.get(0))?; if count > len { self.conn.execute( "DELETE FROM history WHERE rowid IN (SELECT rowid FROM history ORDER BY rowid ASC \ LIMIT ?1);", [count - len], )?; } self.max_len = len; Ok(()) } fn ignore_dups(&mut self, yes: bool) -> Result<()> { if self.ignore_dups != yes { self.ignore_dups = yes; self.set_ignore_dups()?; } Ok(()) } fn ignore_space(&mut self, yes: bool) { self.ignore_space = yes; } fn save(&mut self, path: &Path) -> Result<()> { if self.session_id == 0 { // nothing to save return Ok(()); } else if is_same(self.path.as_ref(), path) { if !self.is_mem_or_temp() { self.conn.execute_batch( " PRAGMA optimize; PRAGMA incremental_vacuum; ", )?; } } else { // TODO Validate: backup whole history self.conn.backup(DatabaseName::Main, path, None)?; // TODO Validate: keep using original path } Ok(()) } fn append(&mut self, path: &Path) -> Result<()> { if is_same(self.path.as_ref(), path) { return Ok(()); // no entry in memory } else if self.session_id == 0 { self.reset(path)?; self.check_schema()?; return Ok(()); // no entry to append } let old_id = self.session_id; { let old = self.reset(path)?; // keep connection alive in case of in-memory database self.create_session()?; // TODO preserve session.timestamp old.execute("ATTACH DATABASE ?1 AS new;", [path.to_string_lossy()])?; // TODO empty path / temporary database old.execute( "INSERT OR IGNORE INTO new.history (session_id, entry) SELECT ?1, entry FROM \ history WHERE session_id = ?2;", [self.session_id, old_id], )?; // TODO Validate: only current session entries old.execute("DETACH DATABASE new;", [])?; } self.update_row_id()?; Ok(()) } fn load(&mut self, path: &Path) -> Result<()> { #[allow(clippy::if_same_then_else)] if is_same(self.path.as_ref(), path) { return Ok(()); } else if self.path.is_none() { // TODO check that there is no memory entries (session_id == 0) ? self.reset(path)?; self.check_schema()?; } else if self.path.as_ref().map_or(true, |p| p != path) { self.reset(path)?; self.check_schema()?; } // Keep all on disk Ok(()) } fn clear(&mut self) -> Result<()> { if self.session_id == 0 { return Ok(()); } else if self.is_mem_or_temp() { // ON DELETE CASCADE... self.conn .execute("DELETE FROM session WHERE id = ?1;", [self.session_id])?; self.session_id = 0; self.update_row_id()?; } // else nothing in memory, TODO Validate: no delete ? Ok(()) } fn search( &self, term: &str, start: usize, dir: SearchDirection, ) -> Result> { self.search_match(term, start, dir, false) } fn starts_with( &self, term: &str, start: usize, dir: SearchDirection, ) -> Result> { self.search_match(term, start, dir, true) } } fn conn(path: Option<&PathBuf>) -> rusqlite::Result { if let Some(ref path) = path { Connection::open(path) } else { Connection::open_in_memory() } } const MEMORY: &str = ":memory:"; fn normalize(path: &Path) -> Option { if path.as_os_str() == MEMORY { None } else { Some(path.to_path_buf()) } } fn is_mem_or_temp(path: &Path) -> bool { let os_str = path.as_os_str(); os_str.is_empty() || os_str == MEMORY } fn is_same(old: Option<&PathBuf>, new: &Path) -> bool { if let Some(old) = old { old == new // TODO canonicalize ? } else { new.as_os_str() == MEMORY } } fn offset(s: String) -> usize { s.split(' ') .nth(2) .and_then(|s| s.parse().ok()) .unwrap_or(0) } #[cfg(test)] mod tests { use super::SQLiteHistory; use crate::config::Config; use crate::history::{History, SearchDirection, SearchResult}; use crate::Result; use std::borrow::Cow; use std::path::Path; fn init() -> Result { let mut h = SQLiteHistory::with_config(Config::default())?; h.add("line1")?; h.add("line2")?; h.add("line3")?; Ok(h) } #[test] fn get() -> Result<()> { let mut h = SQLiteHistory::with_config(Config::default())?; assert_eq!(None, h.get(0, SearchDirection::Forward)?); h.add("line")?; assert_eq!( Some(Cow::Borrowed("line")), h.get(0, SearchDirection::Forward)?.map(|r| r.entry) ); h.add("line")?; // add duplicates => first entry removed assert_eq!( Some(Cow::Borrowed("line")), h.get(0, SearchDirection::Forward)?.map(|r| r.entry) ); Ok(()) } #[test] fn len() -> Result<()> { let mut h = SQLiteHistory::with_config(Config::default())?; assert_eq!(0, h.len()); h.add("line")?; assert_eq!(1, h.len()); Ok(()) } #[test] fn is_empty() -> Result<()> { let mut h = SQLiteHistory::with_config(Config::default())?; assert!(h.is_empty()); h.add("line")?; assert!(!h.is_empty()); Ok(()) } #[test] fn set_max_len() -> Result<()> { let mut h = SQLiteHistory::with_config(Config::default())?; h.add("l1")?; h.add("l2")?; h.add("l3")?; h.set_max_len(2)?; let (min, max) = h.conn .query_row("SELECT min(rowid), max(rowid) FROM history;", [], |r| { Ok((r.get::<_, i64>(0)?, r.get::<_, i64>(1)?)) })?; assert_eq!(2, min); assert_eq!(3, max); Ok(()) } #[test] fn ignore_dups() -> Result<()> { let mut h = SQLiteHistory::with_config(Config::default())?; h.ignore_dups(true)?; h.ignore_dups(false)?; h.ignore_dups(true) } #[test] fn save() -> Result<()> { let db1 = "file:db1?mode=memory"; let db2 = "file:db2?mode=memory"; let mut h = SQLiteHistory::open(Config::default(), db1)?; h.save(Path::new(db1))?; h.save(Path::new(db2))?; h.add("line")?; h.save(Path::new(db1))?; assert_eq!(db1, h.path.unwrap().as_os_str()); assert_eq!(1, h.session_id); assert_eq!(1, h.row_id.get()); Ok(()) } #[test] #[cfg_attr(miri, ignore)] // unsupported operation: `getcwd` not available when isolation is enabled fn append() -> Result<()> { let db1 = "file:db1?mode=memory"; // https://sqlite.org/forum/forumpost/c8b364331a8cac86 // let db2 = "file:db2?mode=memory&cache=shared"; let tf = tempfile::NamedTempFile::new()?; let db2 = tf.path(); let mut h = SQLiteHistory::open(Config::default(), db1)?; h.append(Path::new(db1))?; //h.append(Path::new(db2))?; h.add("line")?; h.append(Path::new(db2))?; assert_eq!(db2, h.path.unwrap().as_os_str()); assert_eq!(1, h.session_id); assert_eq!(1, h.row_id.get()); tf.close()?; Ok(()) } #[test] fn load() -> Result<()> { let db1 = "file:db1?mode=memory"; let db2 = "file:db2?mode=memory"; let mut h = SQLiteHistory::open(Config::default(), db1)?; h.load(Path::new(db1))?; h.add("line")?; h.load(Path::new(db2))?; assert_eq!(db2, h.path.unwrap().as_os_str()); assert_eq!(0, h.session_id); assert_eq!(0, h.row_id.get()); Ok(()) } #[test] fn clear() -> Result<()> { let mut h = SQLiteHistory::with_config(Config::default())?; h.clear()?; h.add("line")?; h.clear()?; assert_eq!(0, h.session_id); assert_eq!(0, h.row_id.get()); assert_eq!( 0, h.conn .query_row("SELECT count(1) FROM session;", [], |r| r.get::<_, i32>(0))? ); assert_eq!( 0, h.conn .query_row("SELECT count(1) FROM history;", [], |r| r.get::<_, i32>(0))? ); Ok(()) } #[test] fn search() -> Result<()> { let h = init()?; assert_eq!(None, h.search("", 0, SearchDirection::Forward)?); assert_eq!(None, h.search("none", 0, SearchDirection::Forward)?); assert_eq!(None, h.search("line", 3, SearchDirection::Forward)?); assert_eq!( Some(SearchResult { idx: 0, entry: h.get(0, SearchDirection::Forward)?.unwrap().entry, pos: 0 }), h.search("line", 0, SearchDirection::Forward)? ); assert_eq!( Some(SearchResult { idx: 1, entry: h.get(1, SearchDirection::Forward)?.unwrap().entry, pos: 0 }), h.search("line", 1, SearchDirection::Forward)? ); assert_eq!( Some(SearchResult { idx: 2, entry: h.get(2, SearchDirection::Forward)?.unwrap().entry, pos: 0 }), h.search("line3", 1, SearchDirection::Forward)? ); Ok(()) } #[test] fn reverse_search() -> Result<()> { let h = init()?; assert_eq!(None, h.search("", 2, SearchDirection::Reverse)?); assert_eq!(None, h.search("none", 2, SearchDirection::Reverse)?); assert_eq!(None, h.search("line", 3, SearchDirection::Reverse)?); assert_eq!( Some(SearchResult { idx: 2, entry: h.get(2, SearchDirection::Reverse)?.unwrap().entry, pos: 0 }), h.search("line", 2, SearchDirection::Reverse)? ); assert_eq!( Some(SearchResult { idx: 1, entry: h.get(1, SearchDirection::Reverse)?.unwrap().entry, pos: 0 }), h.search("line", 1, SearchDirection::Reverse)? ); assert_eq!( Some(SearchResult { idx: 0, entry: h.get(0, SearchDirection::Reverse)?.unwrap().entry, pos: 0 }), h.search("line1", 1, SearchDirection::Reverse)? ); Ok(()) } #[test] fn starts_with() -> Result<()> { let h = init()?; assert_eq!( Some(SearchResult { idx: 2, entry: h.get(2, SearchDirection::Reverse)?.unwrap().entry, pos: 4 }), h.starts_with("LiNe", 2, SearchDirection::Reverse)? ); assert_eq!(None, h.starts_with("iNe", 2, SearchDirection::Reverse)?); Ok(()) } } rustyline-13.0.0/src/test/common.rs000064400000000000000000000226471046102023000153620ustar 00000000000000//! 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::{KeyCode as K, KeyEvent as E, Modifiers as M}; #[test] fn home_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_cursor(*mode, ("", ""), &[E(K::Home, M::NONE), E::ENTER], ("", "")); assert_cursor( *mode, ("Hi", ""), &[E(K::Home, M::NONE), E::ENTER], ("", "Hi"), ); if *mode == EditMode::Vi { // vi command mode assert_cursor( *mode, ("Hi", ""), &[E::ESC, E(K::Home, M::NONE), E::ENTER], ("", "Hi"), ); } } } #[test] fn end_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_cursor(*mode, ("", ""), &[E(K::End, M::NONE), E::ENTER], ("", "")); assert_cursor( *mode, ("H", "i"), &[E(K::End, M::NONE), E::ENTER], ("Hi", ""), ); assert_cursor( *mode, ("", "Hi"), &[E(K::End, M::NONE), E::ENTER], ("Hi", ""), ); if *mode == EditMode::Vi { // vi command mode assert_cursor( *mode, ("", "Hi"), &[E::ESC, E(K::End, M::NONE), E::ENTER], ("Hi", ""), ); } } } #[test] fn left_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_cursor( *mode, ("Hi", ""), &[E(K::Left, M::NONE), E::ENTER], ("H", "i"), ); assert_cursor( *mode, ("H", "i"), &[E(K::Left, M::NONE), E::ENTER], ("", "Hi"), ); assert_cursor( *mode, ("", "Hi"), &[E(K::Left, M::NONE), E::ENTER], ("", "Hi"), ); if *mode == EditMode::Vi { // vi command mode assert_cursor( *mode, ("Bye", ""), &[E::ESC, E(K::Left, M::NONE), E::ENTER], ("B", "ye"), ); } } } #[test] fn right_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_cursor(*mode, ("", ""), &[E(K::Right, M::NONE), E::ENTER], ("", "")); assert_cursor( *mode, ("", "Hi"), &[E(K::Right, M::NONE), E::ENTER], ("H", "i"), ); assert_cursor( *mode, ("B", "ye"), &[E(K::Right, M::NONE), E::ENTER], ("By", "e"), ); assert_cursor( *mode, ("H", "i"), &[E(K::Right, M::NONE), E::ENTER], ("Hi", ""), ); if *mode == EditMode::Vi { // vi command mode assert_cursor( *mode, ("", "Hi"), &[E::ESC, E(K::Right, M::NONE), E::ENTER], ("H", "i"), ); } } } #[test] fn enter_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_line(*mode, &[E::ENTER], ""); assert_line(*mode, &[E::from('a'), E::ENTER], "a"); assert_line_with_initial(*mode, ("Hi", ""), &[E::ENTER], "Hi"); assert_line_with_initial(*mode, ("", "Hi"), &[E::ENTER], "Hi"); assert_line_with_initial(*mode, ("H", "i"), &[E::ENTER], "Hi"); if *mode == EditMode::Vi { // vi command mode assert_line(*mode, &[E::ESC, E::ENTER], ""); assert_line(*mode, &[E::from('a'), E::ESC, E::ENTER], "a"); assert_line_with_initial(*mode, ("Hi", ""), &[E::ESC, E::ENTER], "Hi"); assert_line_with_initial(*mode, ("", "Hi"), &[E::ESC, E::ENTER], "Hi"); assert_line_with_initial(*mode, ("H", "i"), &[E::ESC, E::ENTER], "Hi"); } } } #[test] fn newline_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_line(*mode, &[E::ctrl('J')], ""); assert_line(*mode, &[E::from('a'), E::ctrl('J')], "a"); if *mode == EditMode::Vi { // vi command mode assert_line(*mode, &[E::ESC, E::ctrl('J')], ""); assert_line(*mode, &[E::from('a'), E::ESC, E::ctrl('J')], "a"); } } } #[test] fn eof_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { let mut editor = init_editor(*mode, &[E::ctrl('D')]); let err = editor.readline(">>"); assert_matches!(err, Err(ReadlineError::Eof)); } assert_line( EditMode::Emacs, &[E::from('a'), E::ctrl('D'), E::ENTER], "a", ); assert_line(EditMode::Vi, &[E::from('a'), E::ctrl('D')], "a"); assert_line(EditMode::Vi, &[E::from('a'), E::ESC, E::ctrl('D')], "a"); assert_line_with_initial(EditMode::Emacs, ("", "Hi"), &[E::ctrl('D'), E::ENTER], "i"); assert_line_with_initial(EditMode::Vi, ("", "Hi"), &[E::ctrl('D')], "Hi"); assert_line_with_initial(EditMode::Vi, ("", "Hi"), &[E::ESC, E::ctrl('D')], "Hi"); } #[test] fn interrupt_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { let mut editor = init_editor(*mode, &[E::ctrl('C')]); let err = editor.readline(">>"); assert_matches!(err, Err(ReadlineError::Interrupted)); let mut editor = init_editor(*mode, &[E::ctrl('C')]); let err = editor.readline_with_initial(">>", ("Hi", "")); assert_matches!(err, Err(ReadlineError::Interrupted)); if *mode == EditMode::Vi { // vi command mode let mut editor = init_editor(*mode, &[E::ESC, E::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", ""), &[E(K::Delete, M::NONE), E::ENTER], ("a", ""), ); assert_cursor( *mode, ("", "a"), &[E(K::Delete, M::NONE), E::ENTER], ("", ""), ); if *mode == EditMode::Vi { // vi command mode assert_cursor( *mode, ("", "a"), &[E::ESC, E(K::Delete, M::NONE), E::ENTER], ("", ""), ); } } } #[test] fn ctrl_t() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_cursor(*mode, ("a", "b"), &[E::ctrl('T'), E::ENTER], ("ba", "")); assert_cursor(*mode, ("ab", "cd"), &[E::ctrl('T'), E::ENTER], ("acb", "d")); if *mode == EditMode::Vi { // vi command mode assert_cursor( *mode, ("ab", ""), &[E::ESC, E::ctrl('T'), E::ENTER], ("ba", ""), ); } } } #[test] fn ctrl_u() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_cursor( *mode, ("start of line ", "end"), &[E::ctrl('U'), E::ENTER], ("", "end"), ); assert_cursor(*mode, ("", "end"), &[E::ctrl('U'), E::ENTER], ("", "end")); if *mode == EditMode::Vi { // vi command mode assert_cursor( *mode, ("start of line ", "end"), &[E::ESC, E::ctrl('U'), E::ENTER], ("", " end"), ); } } } #[cfg(unix)] #[test] fn ctrl_v() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_cursor( *mode, ("", ""), &[E::ctrl('V'), E(K::Char('\t'), M::NONE), E::ENTER], ("\t", ""), ); if *mode == EditMode::Vi { // vi command mode assert_cursor( *mode, ("", ""), &[E::ESC, E::ctrl('V'), E(K::Char('\t'), M::NONE), E::ENTER], ("\t", ""), ); } } } #[test] fn ctrl_w() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_cursor( *mode, ("Hello, ", "world"), &[E::ctrl('W'), E::ENTER], ("", "world"), ); assert_cursor( *mode, ("Hello, world.", ""), &[E::ctrl('W'), E::ENTER], ("Hello, ", ""), ); if *mode == EditMode::Vi { // vi command mode assert_cursor( *mode, ("Hello, world.", ""), &[E::ESC, E::ctrl('W'), E::ENTER], ("Hello, ", "."), ); } } } #[test] fn ctrl_y() { for mode in &[EditMode::Emacs /* FIXME, EditMode::Vi */] { assert_cursor( *mode, ("Hello, ", "world"), &[E::ctrl('W'), E::ctrl('Y'), E::ENTER], ("Hello, ", "world"), ); } } #[test] fn ctrl__() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_cursor( *mode, ("Hello, ", "world"), &[E::ctrl('W'), E::ctrl('_'), E::ENTER], ("Hello, ", "world"), ); if *mode == EditMode::Vi { // vi command mode assert_cursor( *mode, ("Hello, ", "world"), &[E::ESC, E::ctrl('W'), E::ctrl('_'), E::ENTER], ("Hello,", " world"), ); } } } rustyline-13.0.0/src/test/emacs.rs000064400000000000000000000202651046102023000151540ustar 00000000000000//! Emacs specific key bindings use super::{assert_cursor, assert_history}; use crate::config::EditMode; use crate::keys::{KeyCode as K, KeyEvent as E, Modifiers as M}; #[test] fn ctrl_a() { assert_cursor( EditMode::Emacs, ("Hi", ""), &[E::ctrl('A'), E::ENTER], ("", "Hi"), ); assert_cursor( EditMode::Emacs, ("test test\n123", "foo"), &[E::ctrl('A'), E::ENTER], ("test test\n", "123foo"), ); } #[test] fn ctrl_e() { assert_cursor( EditMode::Emacs, ("", "Hi"), &[E::ctrl('E'), E::ENTER], ("Hi", ""), ); assert_cursor( EditMode::Emacs, ("foo", "test test\n123"), &[E::ctrl('E'), E::ENTER], ("footest test", "\n123"), ); } #[test] fn ctrl_b() { assert_cursor( EditMode::Emacs, ("Hi", ""), &[E::ctrl('B'), E::ENTER], ("H", "i"), ); assert_cursor( EditMode::Emacs, ("Hi", ""), &[E::alt('2'), E::ctrl('B'), E::ENTER], ("", "Hi"), ); assert_cursor( EditMode::Emacs, ("", "Hi"), &[E::alt('-'), E::alt('2'), E::ctrl('B'), E::ENTER], ("Hi", ""), ); } #[test] fn ctrl_f() { assert_cursor( EditMode::Emacs, ("", "Hi"), &[E::ctrl('F'), E::ENTER], ("H", "i"), ); assert_cursor( EditMode::Emacs, ("", "Hi"), &[E::alt('2'), E::ctrl('F'), E::ENTER], ("Hi", ""), ); assert_cursor( EditMode::Emacs, ("Hi", ""), &[E::alt('-'), E::alt('2'), E::ctrl('F'), E::ENTER], ("", "Hi"), ); } #[test] fn ctrl_h() { assert_cursor( EditMode::Emacs, ("Hi", ""), &[E::ctrl('H'), E::ENTER], ("H", ""), ); assert_cursor( EditMode::Emacs, ("Hi", ""), &[E::alt('2'), E::ctrl('H'), E::ENTER], ("", ""), ); assert_cursor( EditMode::Emacs, ("", "Hi"), &[E::alt('-'), E::alt('2'), E::ctrl('H'), E::ENTER], ("", ""), ); } #[test] fn backspace() { assert_cursor( EditMode::Emacs, ("", ""), &[E::BACKSPACE, E::ENTER], ("", ""), ); assert_cursor( EditMode::Emacs, ("Hi", ""), &[E::BACKSPACE, E::ENTER], ("H", ""), ); assert_cursor( EditMode::Emacs, ("", "Hi"), &[E::BACKSPACE, E::ENTER], ("", "Hi"), ); } #[test] fn ctrl_k() { assert_cursor( EditMode::Emacs, ("Hi", ""), &[E::ctrl('K'), E::ENTER], ("Hi", ""), ); assert_cursor( EditMode::Emacs, ("", "Hi"), &[E::ctrl('K'), E::ENTER], ("", ""), ); assert_cursor( EditMode::Emacs, ("B", "ye"), &[E::ctrl('K'), E::ENTER], ("B", ""), ); assert_cursor( EditMode::Emacs, ("Hi", "foo\nbar"), &[E::ctrl('K'), E::ENTER], ("Hi", "\nbar"), ); assert_cursor( EditMode::Emacs, ("Hi", "\nbar"), &[E::ctrl('K'), E::ENTER], ("Hi", "bar"), ); assert_cursor( EditMode::Emacs, ("Hi", "bar"), &[E::ctrl('K'), E::ENTER], ("Hi", ""), ); } #[test] fn ctrl_u() { assert_cursor( EditMode::Emacs, ("", "Hi"), &[E::ctrl('U'), E::ENTER], ("", "Hi"), ); assert_cursor( EditMode::Emacs, ("Hi", ""), &[E::ctrl('U'), E::ENTER], ("", ""), ); assert_cursor( EditMode::Emacs, ("B", "ye"), &[E::ctrl('U'), E::ENTER], ("", "ye"), ); assert_cursor( EditMode::Emacs, ("foo\nbar", "Hi"), &[E::ctrl('U'), E::ENTER], ("foo\n", "Hi"), ); assert_cursor( EditMode::Emacs, ("foo\n", "Hi"), &[E::ctrl('U'), E::ENTER], ("foo", "Hi"), ); assert_cursor( EditMode::Emacs, ("foo", "Hi"), &[E::ctrl('U'), E::ENTER], ("", "Hi"), ); } #[test] fn ctrl_n() { assert_history( EditMode::Emacs, &["line1", "line2"], &[E::ctrl('P'), E::ctrl('P'), E::ctrl('N'), E::ENTER], "", ("line2", ""), ); } #[test] fn ctrl_p() { assert_history( EditMode::Emacs, &["line1"], &[E::ctrl('P'), E::ENTER], "", ("line1", ""), ); } #[test] fn ctrl_t() { /* FIXME assert_cursor( ("ab", "cd"), &[E::alt('2'), E::ctrl('T'), E::ENTER], ("acdb", ""), );*/ } #[test] fn ctrl_x_ctrl_u() { assert_cursor( EditMode::Emacs, ("Hello, ", "world"), &[E::ctrl('W'), E::ctrl('X'), E::ctrl('U'), E::ENTER], ("Hello, ", "world"), ); } #[test] fn meta_b() { assert_cursor( EditMode::Emacs, ("Hello, world!", ""), &[E::alt('B'), E::ENTER], ("Hello, ", "world!"), ); assert_cursor( EditMode::Emacs, ("Hello, world!", ""), &[E::alt('2'), E::alt('B'), E::ENTER], ("", "Hello, world!"), ); assert_cursor( EditMode::Emacs, ("", "Hello, world!"), &[E::alt('-'), E::alt('B'), E::ENTER], ("Hello", ", world!"), ); } #[test] fn meta_f() { assert_cursor( EditMode::Emacs, ("", "Hello, world!"), &[E::alt('F'), E::ENTER], ("Hello", ", world!"), ); assert_cursor( EditMode::Emacs, ("", "Hello, world!"), &[E::alt('2'), E::alt('F'), E::ENTER], ("Hello, world", "!"), ); assert_cursor( EditMode::Emacs, ("Hello, world!", ""), &[E::alt('-'), E::alt('F'), E::ENTER], ("Hello, ", "world!"), ); } #[test] fn meta_c() { assert_cursor( EditMode::Emacs, ("hi", ""), &[E::alt('C'), E::ENTER], ("hi", ""), ); assert_cursor( EditMode::Emacs, ("", "hi"), &[E::alt('C'), E::ENTER], ("Hi", ""), ); /* FIXME assert_cursor( ("", "hi test"), &[E::alt('2'), E::alt('C'), E::ENTER], ("Hi Test", ""), );*/ } #[test] fn meta_l() { assert_cursor( EditMode::Emacs, ("Hi", ""), &[E::alt('L'), E::ENTER], ("Hi", ""), ); assert_cursor( EditMode::Emacs, ("", "HI"), &[E::alt('L'), E::ENTER], ("hi", ""), ); /* FIXME assert_cursor( ("", "HI TEST"), &[E::alt('2'), E::alt('L'), E::ENTER], ("hi test", ""), );*/ } #[test] fn meta_u() { assert_cursor( EditMode::Emacs, ("hi", ""), &[E::alt('U'), E::ENTER], ("hi", ""), ); assert_cursor( EditMode::Emacs, ("", "hi"), &[E::alt('U'), E::ENTER], ("HI", ""), ); /* FIXME assert_cursor( ("", "hi test"), &[E::alt('2'), E::alt('U'), E::ENTER], ("HI TEST", ""), );*/ } #[test] fn meta_d() { assert_cursor( EditMode::Emacs, ("Hello", ", world!"), &[E::alt('D'), E::ENTER], ("Hello", "!"), ); assert_cursor( EditMode::Emacs, ("Hello", ", world!"), &[E::alt('2'), E::alt('D'), E::ENTER], ("Hello", ""), ); } #[test] fn meta_t() { assert_cursor( EditMode::Emacs, ("Hello", ", world!"), &[E::alt('T'), E::ENTER], ("world, Hello", "!"), ); /* FIXME assert_cursor( ("One Two", " Three Four"), &[E::alt('T'), E::ENTER], ("One Four Three Two", ""), );*/ } #[test] fn meta_y() { assert_cursor( EditMode::Emacs, ("Hello, world", "!"), &[ E::ctrl('W'), E(K::Left, M::NONE), E::ctrl('W'), E::ctrl('Y'), E::alt('Y'), E::ENTER, ], ("world", " !"), ); } #[test] fn meta_backspace() { assert_cursor( EditMode::Emacs, ("Hello, wor", "ld!"), &[E(K::Backspace, M::ALT), E::ENTER], ("Hello, ", "ld!"), ); } #[test] fn meta_digit() { assert_cursor( EditMode::Emacs, ("", ""), &[E::alt('3'), E::from('h'), E::ENTER], ("hhh", ""), ); } rustyline-13.0.0/src/test/history.rs000064400000000000000000000130641046102023000155640ustar 00000000000000//! History related commands tests use super::assert_history; use crate::config::EditMode; use crate::keys::{KeyCode as K, KeyEvent as E, Modifiers as M}; #[test] fn down_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_history( *mode, &["line1"], &[E(K::Down, M::NONE), E::ENTER], "", ("", ""), ); assert_history( *mode, &["line1", "line2"], &[ E(K::Up, M::NONE), E(K::Up, M::NONE), E(K::Down, M::NONE), E::ENTER, ], "", ("line2", ""), ); assert_history( *mode, &["line1"], &[ E::from('a'), E(K::Up, M::NONE), E(K::Down, M::NONE), // restore original line E::ENTER, ], "", ("a", ""), ); assert_history( *mode, &["line1"], &[ E::from('a'), E(K::Down, M::NONE), // noop E::ENTER, ], "", ("a", ""), ); } } #[test] fn up_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_history(*mode, &[], &[E(K::Up, M::NONE), E::ENTER], "", ("", "")); assert_history( *mode, &["line1"], &[E(K::Up, M::NONE), E::ENTER], "", ("line1", ""), ); assert_history( *mode, &["line1", "line2"], &[E(K::Up, M::NONE), E(K::Up, M::NONE), E::ENTER], "", ("line1", ""), ); } } #[test] fn ctrl_r() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_history( *mode, &[], &[E::ctrl('R'), E::from('o'), E::ENTER], "", ("o", ""), ); assert_history( *mode, &["rustc", "cargo"], &[ E::ctrl('R'), E::from('o'), E(K::Right, M::NONE), // just to assert cursor pos E::ENTER, ], "", ("cargo", ""), ); assert_history( *mode, &["rustc", "cargo"], &[ E::ctrl('R'), E::from('u'), E(K::Right, M::NONE), // just to assert cursor pos E::ENTER, ], "", ("ru", "stc"), ); assert_history( *mode, &["rustc", "cargo"], &[ E::ctrl('R'), E::from('r'), E::from('u'), E(K::Right, M::NONE), // just to assert cursor pos E::ENTER, ], "", ("r", "ustc"), ); assert_history( *mode, &["rustc", "cargo"], &[ E::ctrl('R'), E::from('r'), E::ctrl('R'), E(K::Right, M::NONE), // just to assert cursor pos E::ENTER, ], "", ("r", "ustc"), ); assert_history( *mode, &["rustc", "cargo"], &[ E::ctrl('R'), E::from('r'), E::from('z'), // no match E(K::Right, M::NONE), // just to assert cursor pos E::ENTER, ], "", ("car", "go"), ); assert_history( EditMode::Emacs, &["rustc", "cargo"], &[ E::from('a'), E::ctrl('R'), E::from('r'), E::ctrl('G'), // abort (FIXME: doesn't work with vi mode) E::ENTER, ], "", ("a", ""), ); } } #[test] fn ctrl_r_with_long_prompt() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_history( *mode, &["rustc", "cargo"], &[E::ctrl('R'), E::from('o'), E::ENTER], ">>>>>>>>>>>>>>>>>>>>>>>>>>> ", ("cargo", ""), ); } } #[test] fn ctrl_s() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_history( *mode, &["rustc", "cargo"], &[ E::ctrl('R'), E::from('r'), E::ctrl('R'), E::ctrl('S'), E(K::Right, M::NONE), // just to assert cursor pos E::ENTER, ], "", ("car", "go"), ); } } #[test] fn meta_lt() { assert_history( EditMode::Emacs, &[""], &[E::alt('<'), E::ENTER], "", ("", ""), ); assert_history( EditMode::Emacs, &["rustc", "cargo"], &[E::alt('<'), E::ENTER], "", ("rustc", ""), ); } #[test] fn meta_gt() { assert_history( EditMode::Emacs, &[""], &[E::alt('>'), E::ENTER], "", ("", ""), ); assert_history( EditMode::Emacs, &["rustc", "cargo"], &[E::alt('<'), E::alt('>'), E::ENTER], "", ("", ""), ); assert_history( EditMode::Emacs, &["rustc", "cargo"], &[ E::from('a'), E::alt('<'), E::alt('>'), // restore original line E::ENTER, ], "", ("a", ""), ); } rustyline-13.0.0/src/test/mod.rs000064400000000000000000000134641046102023000146460ustar 00000000000000use std::vec::IntoIter; use crate::completion::Completer; use crate::config::{CompletionType, Config, EditMode}; use crate::edit::init_state; use crate::highlight::Highlighter; use crate::hint::Hinter; use crate::history::History; use crate::keymap::{Bindings, Cmd, InputState}; use crate::keys::{KeyCode as K, KeyEvent, KeyEvent as E, Modifiers as M}; use crate::tty::Sink; use crate::validate::Validator; use crate::{apply_backspace_direct, readline_direct, Context, DefaultEditor, Helper, Result}; mod common; mod emacs; mod history; mod vi_cmd; mod vi_insert; fn init_editor(mode: EditMode, keys: &[KeyEvent]) -> DefaultEditor { let config = Config::builder().edit_mode(mode).build(); let mut editor = DefaultEditor::with_config(config).unwrap(); 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, if line == "rus" { vec![line.to_owned() + "t"] } else if line == "\\hbar" { vec!["ℏ".to_owned()] } else { vec![] }, )) } } impl Hinter for SimpleCompleter { type Hint = String; fn hint(&self, _line: &str, _pos: usize, _ctx: &Context<'_>) -> Option { None } } impl Helper for SimpleCompleter {} impl Highlighter for SimpleCompleter {} impl Validator for SimpleCompleter {} #[test] fn complete_line() { let mut out = Sink::default(); let history = crate::history::DefaultHistory::new(); let helper = Some(SimpleCompleter); let mut s = init_state(&mut out, "rus", 3, helper.as_ref(), &history); let config = Config::default(); let bindings = Bindings::new(); let mut input_state = InputState::new(&config, &bindings); let keys = vec![E::ENTER]; let mut rdr: IntoIter = keys.into_iter(); let cmd = super::complete_line(&mut rdr, &mut s, &mut input_state, &config).unwrap(); assert_eq!( Some(Cmd::AcceptOrInsertLine { accept_in_the_middle: true }), cmd ); assert_eq!("rust", s.line.as_str()); assert_eq!(4, s.line.pos()); } #[test] fn complete_symbol() { let mut out = Sink::default(); let history = crate::history::DefaultHistory::new(); let helper = Some(SimpleCompleter); let mut s = init_state(&mut out, "\\hbar", 5, helper.as_ref(), &history); let config = Config::builder() .completion_type(CompletionType::List) .build(); let bindings = Bindings::new(); let mut input_state = InputState::new(&config, &bindings); let keys = vec![E::ENTER]; let mut rdr: IntoIter = keys.into_iter(); let cmd = super::complete_line(&mut rdr, &mut s, &mut input_state, &config).unwrap(); assert_eq!(None, cmd); assert_eq!("ℏ", s.line.as_str()); assert_eq!(3, s.line.pos()); } // `keys`: keys to press // `expected_line`: line after enter key fn assert_line(mode: EditMode, keys: &[KeyEvent], expected_line: &str) { let mut editor = init_editor(mode, keys); let actual_line = editor.readline(">>").unwrap(); assert_eq!(expected_line, actual_line); } // `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: &[KeyEvent], 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: &[KeyEvent], expected: (&str, &str)) { let mut editor = init_editor(mode, keys); let actual_line = editor.readline_with_initial("", initial).unwrap(); assert_eq!(expected.0.to_owned() + expected.1, actual_line); 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: &[KeyEvent], prompt: &str, expected: (&str, &str), ) { let mut editor = init_editor(mode, keys); for entry in entries { editor.history.add(entry).unwrap(); } 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, &[E(K::UnknownEscSeq, M::NONE), E::ENTER], ""); } } #[test] fn test_send() { fn assert_send() {} assert_send::(); } #[test] fn test_sync() { fn assert_sync() {} assert_sync::(); } #[test] fn test_apply_backspace_direct() { assert_eq!( &apply_backspace_direct("Hel\u{0008}\u{0008}el\u{0008}llo ☹\u{0008}☺"), "Hello ☺" ); } #[test] fn test_readline_direct() { use std::io::Cursor; let mut write_buf = vec![]; let output = readline_direct( Cursor::new("([)\n\u{0008}\n\n\r\n])".as_bytes()), Cursor::new(&mut write_buf), &Some(crate::validate::MatchingBracketValidator::new()), ); assert_eq!( &write_buf, b"Mismatched brackets: '[' is not properly closed" ); assert_eq!(&output.unwrap(), "([\n\n\r\n])"); } rustyline-13.0.0/src/test/vi_cmd.rs000064400000000000000000000315071046102023000153260ustar 00000000000000//! Vi command mode specific key bindings use super::{assert_cursor, assert_history}; use crate::config::EditMode; use crate::keys::KeyEvent as E; #[test] fn dollar() { assert_cursor( EditMode::Vi, ("", "Hi"), &[E::ESC, E::from('$'), E::ENTER], ("Hi", ""), // FIXME ); } #[test] fn dot() { assert_cursor( EditMode::Vi, ("", ""), &[E::ESC, E::from('.'), E::ENTER], ("", ""), ); } #[test] fn semi_colon() { assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[E::ESC, E::from('f'), E::from('o'), E::from(';'), E::ENTER], ("Hello, w", "orld!"), ); } #[test] fn comma() { assert_cursor( EditMode::Vi, ("Hello, w", "orld!"), &[E::ESC, E::from('f'), E::from('l'), E::from(','), E::ENTER], ("Hel", "lo, world!"), ); } #[test] fn zero() { assert_cursor( EditMode::Vi, ("Hi", ""), &[E::ESC, E::from('0'), E::ENTER], ("", "Hi"), ); } #[test] fn caret() { assert_cursor( EditMode::Vi, (" Hi", ""), &[E::ESC, E::from('^'), E::ENTER], (" ", "Hi"), ); } #[test] fn caret_no_whitespace() { assert_cursor( EditMode::Vi, ("Hi", ""), &[E::ESC, E::from('^'), E::ENTER], ("", "Hi"), ); } #[test] fn a() { assert_cursor( EditMode::Vi, ("B", "e"), &[E::ESC, E::from('a'), E::from('y'), E::ENTER], ("By", "e"), ); } #[test] fn uppercase_a() { assert_cursor( EditMode::Vi, ("", "By"), &[E::ESC, E::from('A'), E::from('e'), E::ENTER], ("Bye", ""), ); } #[test] fn b() { assert_cursor( EditMode::Vi, ("Hello, world!", ""), &[E::ESC, E::from('b'), E::ENTER], ("Hello, ", "world!"), ); assert_cursor( EditMode::Vi, ("Hello, world!", ""), &[E::ESC, E::from('2'), E::from('b'), E::ENTER], ("Hello", ", world!"), ); } #[test] fn uppercase_b() { assert_cursor( EditMode::Vi, ("Hello, world!", ""), &[E::ESC, E::from('B'), E::ENTER], ("Hello, ", "world!"), ); assert_cursor( EditMode::Vi, ("Hello, world!", ""), &[E::ESC, E::from('2'), E::from('B'), E::ENTER], ("", "Hello, world!"), ); } #[test] fn uppercase_c() { assert_cursor( EditMode::Vi, ("Hello, w", "orld!"), &[E::ESC, E::from('C'), E::from('i'), E::ENTER], ("Hello, i", ""), ); } #[test] fn ctrl_k() { for key in &[E::from('D'), E::ctrl('K')] { assert_cursor( EditMode::Vi, ("Hi", ""), &[E::ESC, *key, E::ENTER], ("H", ""), ); assert_cursor( EditMode::Vi, ("", "Hi"), &[E::ESC, *key, E::ENTER], ("", ""), ); assert_cursor( EditMode::Vi, ("By", "e"), &[E::ESC, *key, E::ENTER], ("B", ""), ); } } #[test] fn e() { assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[E::ESC, E::from('e'), E::ENTER], ("Hell", "o, world!"), ); assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[E::ESC, E::from('2'), E::from('e'), E::ENTER], ("Hello, worl", "d!"), ); } #[test] fn uppercase_e() { assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[E::ESC, E::from('E'), E::ENTER], ("Hello", ", world!"), ); assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[E::ESC, E::from('2'), E::from('E'), E::ENTER], ("Hello, world", "!"), ); } #[test] fn f() { assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[E::ESC, E::from('f'), E::from('r'), E::ENTER], ("Hello, wo", "rld!"), ); assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[E::ESC, E::from('3'), E::from('f'), E::from('l'), E::ENTER], ("Hello, wor", "ld!"), ); } #[test] fn uppercase_f() { assert_cursor( EditMode::Vi, ("Hello, world!", ""), &[E::ESC, E::from('F'), E::from('r'), E::ENTER], ("Hello, wo", "rld!"), ); assert_cursor( EditMode::Vi, ("Hello, world!", ""), &[E::ESC, E::from('3'), E::from('F'), E::from('l'), E::ENTER], ("He", "llo, world!"), ); } #[test] fn i() { assert_cursor( EditMode::Vi, ("Be", ""), &[E::ESC, E::from('i'), E::from('y'), E::ENTER], ("By", "e"), ); } #[test] fn uppercase_i() { assert_cursor( EditMode::Vi, ("Be", ""), &[E::ESC, E::from('I'), E::from('y'), E::ENTER], ("y", "Be"), ); } #[test] fn u() { assert_cursor( EditMode::Vi, ("Hello, ", "world"), &[E::ESC, E::ctrl('W'), E::from('u'), E::ENTER], ("Hello,", " world"), ); } #[test] fn w() { assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[E::ESC, E::from('w'), E::ENTER], ("Hello", ", world!"), ); assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[E::ESC, E::from('2'), E::from('w'), E::ENTER], ("Hello, ", "world!"), ); } #[test] fn uppercase_w() { assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[E::ESC, E::from('W'), E::ENTER], ("Hello, ", "world!"), ); assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[E::ESC, E::from('2'), E::from('W'), E::ENTER], ("Hello, world", "!"), ); } #[test] fn x() { assert_cursor( EditMode::Vi, ("", "a"), &[E::ESC, E::from('x'), E::ENTER], ("", ""), ); } #[test] fn uppercase_x() { assert_cursor( EditMode::Vi, ("Hi", ""), &[E::ESC, E::from('X'), E::ENTER], ("", "i"), ); } #[test] fn h() { for key in &[E::from('h'), E::ctrl('H'), E::BACKSPACE] { assert_cursor( EditMode::Vi, ("Bye", ""), &[E::ESC, *key, E::ENTER], ("B", "ye"), ); assert_cursor( EditMode::Vi, ("Bye", ""), &[E::ESC, E::from('2'), *key, E::ENTER], ("", "Bye"), ); } } #[test] fn l() { for key in &[E::from('l'), E::from(' ')] { assert_cursor( EditMode::Vi, ("", "Hi"), &[E::ESC, *key, E::ENTER], ("H", "i"), ); assert_cursor( EditMode::Vi, ("", "Hi"), &[E::ESC, E::from('2'), *key, E::ENTER], ("Hi", ""), ); } } #[test] fn j() { for key in &[E::from('j'), E::from('+')] { assert_cursor( EditMode::Vi, ("Hel", "lo,\nworld!"), // NOTE: escape moves backwards on char &[E::ESC, *key, E::ENTER], ("Hello,\nwo", "rld!"), ); assert_cursor( EditMode::Vi, ("", "One\nTwo\nThree"), &[E::ESC, E::from('2'), *key, E::ENTER], ("One\nTwo\n", "Three"), ); assert_cursor( EditMode::Vi, ("Hel", "lo,\nworld!"), // NOTE: escape moves backwards on char &[E::ESC, E::from('7'), *key, E::ENTER], ("Hello,\nwo", "rld!"), ); } } #[test] fn k() { for key in &[E::from('k'), E::from('-')] { assert_cursor( EditMode::Vi, ("Hello,\nworl", "d!"), // NOTE: escape moves backwards on char &[E::ESC, *key, E::ENTER], ("Hel", "lo,\nworld!"), ); assert_cursor( EditMode::Vi, ("One\nTwo\nT", "hree"), // NOTE: escape moves backwards on char &[E::ESC, E::from('2'), *key, E::ENTER], ("", "One\nTwo\nThree"), ); assert_cursor( EditMode::Vi, ("Hello,\nworl", "d!"), // NOTE: escape moves backwards on char &[E::ESC, E::from('5'), *key, E::ENTER], ("Hel", "lo,\nworld!"), ); assert_cursor( EditMode::Vi, ("first line\nshort\nlong line", ""), &[E::ESC, *key, E::ENTER], ("first line\nshort", "\nlong line"), ); } } #[test] fn ctrl_n() { for key in &[E::ctrl('N')] { assert_history( EditMode::Vi, &["line1", "line2"], &[E::ESC, E::ctrl('P'), E::ctrl('P'), *key, E::ENTER], "", ("line2", ""), ); } } #[test] fn ctrl_p() { for key in &[E::ctrl('P')] { assert_history( EditMode::Vi, &["line1"], &[E::ESC, *key, E::ENTER], "", ("line1", ""), ); } } #[test] fn p() { assert_cursor( EditMode::Vi, ("Hello, ", "world"), &[E::ESC, E::ctrl('W'), E::from('p'), E::ENTER], (" Hello", ",world"), ); } #[test] fn uppercase_p() { assert_cursor( EditMode::Vi, ("Hello, ", "world"), &[E::ESC, E::ctrl('W'), E::from('P'), E::ENTER], ("Hello", ", world"), ); } #[test] fn r() { assert_cursor( EditMode::Vi, ("Hi", ", world!"), &[E::ESC, E::from('r'), E::from('o'), E::ENTER], ("H", "o, world!"), ); assert_cursor( EditMode::Vi, ("He", "llo, world!"), &[E::ESC, E::from('4'), E::from('r'), E::from('i'), E::ENTER], ("Hiii", "i, world!"), ); } #[test] fn s() { assert_cursor( EditMode::Vi, ("Hi", ", world!"), &[E::ESC, E::from('s'), E::from('o'), E::ENTER], ("Ho", ", world!"), ); assert_cursor( EditMode::Vi, ("He", "llo, world!"), &[E::ESC, E::from('4'), E::from('s'), E::from('i'), E::ENTER], ("Hi", ", world!"), ); } #[test] fn uppercase_s() { assert_cursor( EditMode::Vi, ("Hello, ", "world"), &[E::ESC, E::from('S'), E::ENTER], ("", ""), ); } #[test] fn t() { assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[E::ESC, E::from('t'), E::from('r'), E::ENTER], ("Hello, w", "orld!"), ); assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[E::ESC, E::from('3'), E::from('t'), E::from('l'), E::ENTER], ("Hello, wo", "rld!"), ); } #[test] fn uppercase_t() { assert_cursor( EditMode::Vi, ("Hello, world!", ""), &[E::ESC, E::from('T'), E::from('r'), E::ENTER], ("Hello, wor", "ld!"), ); assert_cursor( EditMode::Vi, ("Hello, world!", ""), &[E::ESC, E::from('3'), E::from('T'), E::from('l'), E::ENTER], ("Hel", "lo, world!"), ); } #[test] fn indent() { assert_cursor( EditMode::Vi, ("Hello, world!", ""), &[E::ESC, E::from('>'), E::from('>'), E::ENTER], (" Hello, world", "!"), // Esc moves to the left ); assert_cursor( EditMode::Vi, ("line1\nline2", ""), &[E::ESC, E::from('>'), E::from('>'), E::ENTER], ("line1\n line", "2"), // Esc moves to the left ); assert_cursor( EditMode::Vi, ("line1\nline2", ""), &[E::ESC, E::from('>'), E::from('k'), E::ENTER], (" line1\n line", "2"), // Esc moves to the left ); assert_cursor( EditMode::Vi, (" li", "ne1\n line2"), &[E::ESC, E::from('>'), E::from('j'), E::ENTER], (" l", "ine1\n line2"), // Esc moves to the left ); assert_cursor( EditMode::Vi, (" ", "line1\n line2"), &[E::ESC, E::from('>'), E::from('j'), E::ENTER], (" ", " line1\n line2"), // Esc moves to the left ); assert_cursor( EditMode::Vi, (" ", "line1\n line2"), &[E::ESC, E::from('>'), E::from('j'), E::ENTER], (" ", " line1\n line2"), // Esc moves to the left ); } #[test] fn dedent() { assert_cursor( EditMode::Vi, (" line1\n line2", ""), &[E::ESC, E::from('<'), E::from('<'), E::ENTER], (" line1\nline", "2"), ); assert_cursor( EditMode::Vi, (" line1\n line2", ""), &[E::ESC, E::from('<'), E::from('k'), E::ENTER], ("line1\nline", "2"), ); assert_cursor( EditMode::Vi, (" li", "ne1\n line2"), &[E::ESC, E::from('<'), E::from('j'), E::ENTER], ("l", "ine1\nline2"), ); assert_cursor( EditMode::Vi, (" ", "line1\n line2"), &[E::ESC, E::from('<'), E::from('j'), E::ENTER], ("", "line1\nline2"), ); assert_cursor( EditMode::Vi, ("line", "1\n line2"), &[E::ESC, E::from('<'), E::from('j'), E::ENTER], ("lin", "e1\nline2"), ); } rustyline-13.0.0/src/test/vi_insert.rs000064400000000000000000000016041046102023000160620ustar 00000000000000//! Vi insert mode specific key bindings use super::assert_cursor; use crate::config::EditMode; use crate::keys::KeyEvent as E; #[test] fn insert_mode_by_default() { assert_cursor(EditMode::Vi, ("", ""), &[E::from('a'), E::ENTER], ("a", "")); } #[test] fn ctrl_h() { assert_cursor( EditMode::Vi, ("Hi", ""), &[E::ctrl('H'), E::ENTER], ("H", ""), ); } #[test] fn backspace() { assert_cursor(EditMode::Vi, ("", ""), &[E::BACKSPACE, E::ENTER], ("", "")); assert_cursor( EditMode::Vi, ("Hi", ""), &[E::BACKSPACE, E::ENTER], ("H", ""), ); assert_cursor( EditMode::Vi, ("", "Hi"), &[E::BACKSPACE, E::ENTER], ("", "Hi"), ); } #[test] fn esc() { assert_cursor( EditMode::Vi, ("", ""), &[E::from('a'), E::ESC, E::ENTER], ("", "a"), ); } rustyline-13.0.0/src/tty/mod.rs000064400000000000000000000200561046102023000145020ustar 00000000000000//! This module implements and describes common TTY methods & traits use unicode_width::UnicodeWidthStr; use crate::config::{Behavior, BellStyle, ColorMode, Config}; use crate::highlight::Highlighter; use crate::keys::KeyEvent; use crate::layout::{Layout, Position}; use crate::line_buffer::LineBuffer; use crate::{Cmd, Result}; /// Terminal state pub trait RawMode: Sized { /// Disable RAW mode for the terminal. fn disable_raw_mode(&self) -> Result<()>; } /// Input event pub enum Event { KeyPress(KeyEvent), ExternalPrint(String), } /// Translate bytes read from stdin to keys. pub trait RawReader { /// Blocking wait for either a key press or an external print fn wait_for_input(&mut self, single_esc_abort: bool) -> Result; // TODO replace calls to `next_key` by `wait_for_input` where relevant /// 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; /// Check if `key` is bound to a peculiar command fn find_binding(&self, key: &KeyEvent) -> Option; } /// 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<()>; /// Compute layout for rendering prompt + line + some info (either hint, /// validation msg, ...). on the screen. Depending on screen width, line /// wrapping may be applied. fn compute_layout( &self, prompt_size: Position, default_prompt: bool, line: &LineBuffer, info: Option<&str>, ) -> Layout { // calculate the desired position of the cursor let pos = line.pos(); let cursor = self.calculate_position(&line[..pos], prompt_size); // calculate the position of the end of the input line let mut end = if pos == line.len() { cursor } else { self.calculate_position(&line[pos..], cursor) }; if let Some(info) = info { end = self.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); new_layout } /// Calculate the number of columns and rows used to display `s` on a /// `cols` width terminal starting at `orig`. fn calculate_position(&self, s: &str, orig: Position) -> Position; fn write_and_flush(&mut self, buf: &str) -> 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<()>; /// Clear rows used by prompt and edited line fn clear_rows(&mut self, layout: &Layout) -> Result<()>; /// 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(&mut self, buf: &str) -> Result<()> { (**self).write_and_flush(buf) } fn beep(&mut self) -> Result<()> { (**self).beep() } fn clear_screen(&mut self) -> Result<()> { (**self).clear_screen() } fn clear_rows(&mut self, layout: &Layout) -> Result<()> { (**self).clear_rows(layout) } 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) } } // ignore ANSI escape sequence fn width(s: &str, esc_seq: &mut u8) -> usize { if *esc_seq == 1 { if s == "[" { // CSI *esc_seq = 2; } else { // two-character sequence *esc_seq = 0; } 0 } else if *esc_seq == 2 { if s == ";" || (s.as_bytes()[0] >= b'0' && s.as_bytes()[0] <= b'9') { /*} else if s == "m" { // last *esc_seq = 0;*/ } else { // not supported *esc_seq = 0; } 0 } else if s == "\x1b" { *esc_seq = 1; 0 } else if s == "\n" { 0 } else { s.width() } } /// External printer pub trait ExternalPrinter { /// Print message to stdout fn print(&mut self, msg: String) -> Result<()>; } /// Terminal contract pub trait Term { type KeyMap; type Reader: RawReader; // rl_instream type Writer: Renderer; // rl_outstream type Mode: RawMode; type ExternalPrinter: ExternalPrinter; type CursorGuard; fn new( color_mode: ColorMode, behavior: Behavior, tab_stop: usize, bell_style: BellStyle, enable_bracketed_paste: bool, ) -> Result where Self: Sized; /// Check if current terminal can provide a rich line-editing user /// interface. fn is_unsupported(&self) -> bool; /// check if input stream is connected to a terminal. fn is_input_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<(Self::Mode, Self::KeyMap)>; /// Create a RAW reader fn create_reader(&self, config: &Config, key_map: Self::KeyMap) -> Self::Reader; /// Create a writer fn create_writer(&self) -> Self::Writer; fn writeln(&self) -> Result<()>; /// Create an external printer fn create_external_printer(&mut self) -> Result; /// Change cursor visibility fn set_cursor_visibility(&mut self, visible: bool) -> Result>; } // If on Windows platform import Windows TTY module // and re-export into mod.rs scope #[cfg(all(windows, not(target_arch = "wasm32")))] mod windows; #[cfg(all(windows, not(target_arch = "wasm32"), not(test)))] pub use self::windows::*; // If on Unix platform import Unix TTY module // and re-export into mod.rs scope #[cfg(all(unix, not(target_arch = "wasm32")))] mod unix; #[cfg(all(unix, not(target_arch = "wasm32"), not(test)))] pub use self::unix::*; #[cfg(any(test, target_arch = "wasm32"))] mod test; #[cfg(any(test, target_arch = "wasm32"))] pub use self::test::*; rustyline-13.0.0/src/tty/test.rs000064400000000000000000000121271046102023000147020ustar 00000000000000//! Tests specific definitions use std::iter::IntoIterator; use std::slice::Iter; use std::vec::IntoIter; use super::{Event, ExternalPrinter, RawMode, RawReader, Renderer, Term}; use crate::config::{Behavior, BellStyle, ColorMode, Config}; use crate::error::ReadlineError; use crate::highlight::Highlighter; use crate::keys::KeyEvent; use crate::layout::{Layout, Position}; use crate::line_buffer::LineBuffer; use crate::{Cmd, Result}; pub type KeyMap = (); pub type Mode = (); impl RawMode for Mode { fn disable_raw_mode(&self) -> Result<()> { Ok(()) } } impl<'a> RawReader for Iter<'a, KeyEvent> { fn wait_for_input(&mut self, single_esc_abort: bool) -> Result { self.next_key(single_esc_abort).map(Event::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!() } fn find_binding(&self, _: &KeyEvent) -> Option { None } } impl RawReader for IntoIter { fn wait_for_input(&mut self, single_esc_abort: bool) -> Result { self.next_key(single_esc_abort).map(Event::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 { use crate::keys::{KeyCode as K, KeyEvent as E, Modifiers as M}; match self.next() { Some(E(K::Char(c), M::NONE)) => Ok(c), None => Err(ReadlineError::Eof), _ => unimplemented!(), } } fn read_pasted_text(&mut self) -> Result { unimplemented!() } fn find_binding(&self, _: &KeyEvent) -> Option { None } } #[derive(Default)] pub struct 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(&mut self, _: &str) -> Result<()> { Ok(()) } fn beep(&mut self) -> Result<()> { Ok(()) } fn clear_screen(&mut self) -> Result<()> { Ok(()) } fn clear_rows(&mut self, _: &Layout) -> Result<()> { Ok(()) } 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 struct DummyExternalPrinter {} impl ExternalPrinter for DummyExternalPrinter { fn print(&mut self, _msg: String) -> 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 CursorGuard = (); type ExternalPrinter = DummyExternalPrinter; type KeyMap = KeyMap; type Mode = Mode; type Reader = IntoIter; type Writer = Sink; fn new( color_mode: ColorMode, _behavior: Behavior, _tab_stop: usize, bell_style: BellStyle, _enable_bracketed_paste: bool, ) -> Result { Ok(DummyTerminal { keys: Vec::new(), cursor: 0, color_mode, bell_style, }) } // Init checks: #[cfg(not(target_arch = "wasm32"))] fn is_unsupported(&self) -> bool { false } #[cfg(target_arch = "wasm32")] fn is_unsupported(&self) -> bool { true } fn is_input_tty(&self) -> bool { true } fn is_output_tty(&self) -> bool { false } // Interactive loop: fn enable_raw_mode(&mut self) -> Result<(Mode, KeyMap)> { Ok(((), ())) } fn create_reader(&self, _: &Config, _: KeyMap) -> Self::Reader { self.keys.clone().into_iter() } fn create_writer(&self) -> Sink { Sink::default() } fn create_external_printer(&mut self) -> Result { Ok(DummyExternalPrinter {}) } fn set_cursor_visibility(&mut self, _: bool) -> Result> { Ok(None) } fn writeln(&self) -> Result<()> { Ok(()) } } #[cfg(unix)] pub fn suspend() -> Result<()> { Ok(()) } rustyline-13.0.0/src/tty/unix.rs000064400000000000000000001712371046102023000147160ustar 00000000000000//! Unix specific definitions use std::cmp; use std::collections::HashMap; use std::fs::{File, OpenOptions}; use std::io::{self, BufReader, ErrorKind, Read, Write}; use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, IntoRawFd, RawFd}; use std::os::unix::net::UnixStream; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::{self, SyncSender}; use std::sync::{Arc, Mutex}; use log::{debug, warn}; use nix::errno::Errno; use nix::poll::{self, PollFlags}; use nix::sys::select::{self, FdSet}; #[cfg(not(feature = "termios"))] use nix::sys::termios::Termios; use nix::unistd::{close, isatty, read, write}; #[cfg(feature = "termios")] use termios::Termios; use unicode_segmentation::UnicodeSegmentation; use utf8parse::{Parser, Receiver}; use super::{width, Event, RawMode, RawReader, Renderer, Term}; use crate::config::{Behavior, BellStyle, ColorMode, Config}; use crate::highlight::Highlighter; use crate::keys::{KeyCode as K, KeyEvent, KeyEvent as E, Modifiers as M}; use crate::layout::{Layout, Position}; use crate::line_buffer::LineBuffer; use crate::{error, Cmd, ReadlineError, Result}; /// Unsupported Terminals that don't support RAW mode const UNSUPPORTED_TERM: [&str; 3] = ["dumb", "cons25", "emacs"]; const BRACKETED_PASTE_ON: &str = "\x1b[?2004h"; const BRACKETED_PASTE_OFF: &str = "\x1b[?2004l"; nix::ioctl_read_bad!(win_size, libc::TIOCGWINSZ, libc::winsize); #[allow(clippy::useless_conversion)] fn get_win_size(fd: RawFd) -> (usize, usize) { use std::mem::zeroed; if cfg!(test) { return (80, 24); } unsafe { let mut size: libc::winsize = zeroed(); match win_size(fd, &mut size) { Ok(0) => { // In linux pseudo-terminals are created with dimensions of // zero. If host application didn't initialize the correct // size before start we treat zero size as 80 columns and // infinite rows let cols = if size.ws_col == 0 { 80 } else { size.ws_col as usize }; let rows = if size.ws_row == 0 { usize::MAX } else { size.ws_row as usize }; (cols, rows) } _ => (80, 24), } } } /// 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 { isatty(fd).unwrap_or(false) } pub type PosixKeyMap = HashMap; #[cfg(not(test))] pub type KeyMap = PosixKeyMap; #[must_use = "You must restore default mode (disable_raw_mode)"] pub struct PosixMode { termios: Termios, tty_in: RawFd, tty_out: Option, raw_mode: Arc, } #[cfg(not(test))] pub type Mode = PosixMode; impl RawMode for PosixMode { /// Disable RAW mode for the terminal. fn disable_raw_mode(&self) -> Result<()> { termios_::disable_raw_mode(self.tty_in, &self.termios)?; // disable bracketed paste if let Some(out) = self.tty_out { write_all(out, BRACKETED_PASTE_OFF)?; } self.raw_mode.store(false, Ordering::SeqCst); Ok(()) } } // Rust std::io::Stdin is buffered with no way to know if bytes are available. // So we use low-level stuff instead... struct TtyIn { fd: RawFd, sigwinch_pipe: Option, } impl Read for TtyIn { fn read(&mut self, buf: &mut [u8]) -> io::Result { loop { let res = unsafe { libc::read( self.fd, 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() == ErrorKind::Interrupted && self.sigwinch()? { return Err(io::Error::new( ErrorKind::Interrupted, error::WindowResizedError, )); } else if error.kind() != ErrorKind::Interrupted { return Err(error); } } else { #[allow(clippy::cast_sign_loss)] return Ok(res as usize); } } } } impl TtyIn { /// Check if a SIGWINCH signal has been received fn sigwinch(&self) -> nix::Result { if let Some(pipe) = self.sigwinch_pipe { let mut buf = [0u8; 64]; match read(pipe, &mut buf) { Ok(0) => Ok(false), Ok(_) => Ok(true), Err(e) if e == Errno::EWOULDBLOCK || e == Errno::EINTR => Ok(false), Err(e) => Err(e), } } else { Ok(false) } } } // (native receiver with a selectable file descriptor, actual message receiver) type PipeReader = Arc)>>; // (native sender, actual message sender) type PipeWriter = (Arc>, SyncSender); /// Console input reader pub struct PosixRawReader { tty_in: BufReader, timeout_ms: i32, parser: Parser, key_map: PosixKeyMap, // external print reader pipe_reader: Option, } impl AsFd for PosixRawReader { fn as_fd(&self) -> BorrowedFd<'_> { let fd = self.tty_in.get_ref().fd; unsafe { BorrowedFd::borrow_raw(fd) } } } struct Utf8 { c: Option, valid: bool, } const UP: char = 'A'; // kcuu1, kUP* const DOWN: char = 'B'; // kcud1, kDN* const RIGHT: char = 'C'; // kcuf1, kRIT* const LEFT: char = 'D'; // kcub1, kLFT* const END: char = 'F'; // kend* const HOME: char = 'H'; // khom* const INSERT: char = '2'; // kic* const DELETE: char = '3'; // kdch1, kDC* const PAGE_UP: char = '5'; // kpp, kPRV* const PAGE_DOWN: char = '6'; // knp, kNXT* const RXVT_HOME: char = '7'; const RXVT_END: char = '8'; const SHIFT: char = '2'; const ALT: char = '3'; const ALT_SHIFT: char = '4'; const CTRL: char = '5'; const CTRL_SHIFT: char = '6'; const CTRL_ALT: char = '7'; const CTRL_ALT_SHIFT: char = '8'; const RXVT_SHIFT: char = '$'; const RXVT_CTRL: char = '\x1e'; const RXVT_CTRL_SHIFT: char = '@'; impl PosixRawReader { fn new( fd: RawFd, sigwinch_pipe: Option, config: &Config, key_map: PosixKeyMap, pipe_reader: Option, ) -> Self { Self { tty_in: BufReader::with_capacity(1024, TtyIn { fd, sigwinch_pipe }), timeout_ms: config.keyseq_timeout(), parser: Parser::new(), key_map, pipe_reader, } } /// Handle \E sequences // https://invisible-island.net/xterm/xterm-function-keys.html fn escape_sequence(&mut self) -> Result { self._do_escape_sequence(true) } /// Don't call directly, call `PosixRawReader::escape_sequence` instead fn _do_escape_sequence(&mut self, allow_recurse: bool) -> Result { // Read the next byte representing the escape sequence. let seq1 = self.next_char()?; if seq1 == '[' { // \E[ sequences. (CSI) self.escape_csi() } else if seq1 == 'O' { // xterm // \EO sequences. (SS3) self.escape_o() } else if seq1 == '\x1b' { // \E\E — used by rxvt, iTerm (under default config), etc. // ``` // \E\E[A => Alt-Up // \E\E[B => Alt-Down // \E\E[C => Alt-Right // \E\E[D => Alt-Left // ``` // // In general this more or less works just adding ALT to an existing // key, but has a wrinkle in that `ESC ESC` without anything // following should be interpreted as the the escape key. // // We handle this by polling to see if there's anything coming // within our timeout, and if so, recursing once, but adding alt to // what we read. if !allow_recurse { return Ok(E::ESC); } let timeout = if self.timeout_ms < 0 { 100 } else { self.timeout_ms }; match self.poll(timeout) { // Ignore poll errors, it's very likely we'll pick them up on // the next read anyway. Ok(0) | Err(_) => Ok(E::ESC), Ok(n) => { debug_assert!(n > 0, "{}", n); // recurse, and add the alt modifier. let E(k, m) = self._do_escape_sequence(false)?; Ok(E(k, m | M::ALT)) } } } else { Ok(E::alt(seq1)) } } /// Handle \E[ escape sequences fn escape_csi(&mut self) -> Result { let seq2 = self.next_char()?; if seq2.is_ascii_digit() { match seq2 { '0' | '9' => { debug!(target: "rustyline", "unsupported esc sequence: \\E[{:?}", seq2); Ok(E(K::UnknownEscSeq, M::NONE)) } _ => { // Extended escape, read additional byte. self.extended_escape(seq2) } } } else if seq2 == '[' { let seq3 = self.next_char()?; // Linux console Ok(match seq3 { 'A' => E(K::F(1), M::NONE), 'B' => E(K::F(2), M::NONE), 'C' => E(K::F(3), M::NONE), 'D' => E(K::F(4), M::NONE), 'E' => E(K::F(5), M::NONE), _ => { debug!(target: "rustyline", "unsupported esc sequence: \\E[[{:?}", seq3); E(K::UnknownEscSeq, M::NONE) } }) } else { // ANSI Ok(match seq2 { UP => E(K::Up, M::NONE), DOWN => E(K::Down, M::NONE), RIGHT => E(K::Right, M::NONE), LEFT => E(K::Left, M::NONE), //'E' => E(K::, M::), // Ignore END => E(K::End, M::NONE), //'G' => E(K::, M::), // Ignore HOME => E(K::Home, M::NONE), // khome //'J' => E(K::, M::), // clr_eos //'K' => E(K::, M::), // clr_eol //'L' => E(K::, M::), // il1 //'M' => E(K::, M::), // kmous //'P' => E(K::Delete, M::NONE), // dch1 'Z' => E(K::BackTab, M::NONE), 'a' => E(K::Up, M::SHIFT), // rxvt: kind or kUP 'b' => E(K::Down, M::SHIFT), // rxvt: kri or kDN 'c' => E(K::Right, M::SHIFT), // rxvt 'd' => E(K::Left, M::SHIFT), // rxvt _ => { debug!(target: "rustyline", "unsupported esc sequence: \\E[{:?}", seq2); E(K::UnknownEscSeq, M::NONE) } }) } } /// Handle \E[ 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' | RXVT_HOME => E(K::Home, M::NONE), // tmux, xrvt INSERT => E(K::Insert, M::NONE), DELETE => E(K::Delete, M::NONE), '4' | RXVT_END => E(K::End, M::NONE), // tmux, xrvt PAGE_UP => E(K::PageUp, M::NONE), PAGE_DOWN => E(K::PageDown, M::NONE), _ => { debug!(target: "rustyline", "unsupported esc sequence: \\E[{}~", seq2); E(K::UnknownEscSeq, M::NONE) } }) } else if seq3.is_ascii_digit() { let seq4 = self.next_char()?; if seq4 == '~' { Ok(match (seq2, seq3) { ('1', '1') => E(K::F(1), M::NONE), // rxvt-unicode ('1', '2') => E(K::F(2), M::NONE), // rxvt-unicode ('1', '3') => E(K::F(3), M::NONE), // rxvt-unicode ('1', '4') => E(K::F(4), M::NONE), // rxvt-unicode ('1', '5') => E(K::F(5), M::NONE), // kf5 ('1', '7') => E(K::F(6), M::NONE), // kf6 ('1', '8') => E(K::F(7), M::NONE), // kf7 ('1', '9') => E(K::F(8), M::NONE), // kf8 ('2', '0') => E(K::F(9), M::NONE), // kf9 ('2', '1') => E(K::F(10), M::NONE), // kf10 ('2', '3') => E(K::F(11), M::NONE), // kf11 ('2', '4') => E(K::F(12), M::NONE), // kf12 //('6', '2') => KeyCode::ScrollUp, //('6', '3') => KeyCode::ScrollDown, _ => { debug!(target: "rustyline", "unsupported esc sequence: \\E[{}{}~", seq2, seq3); E(K::UnknownEscSeq, M::NONE) } }) } else if seq4 == ';' { let seq5 = self.next_char()?; if seq5.is_ascii_digit() { let seq6 = self.next_char()?; if seq6.is_ascii_digit() { self.next_char()?; // 'R' expected Ok(E(K::UnknownEscSeq, M::NONE)) } else if seq6 == 'R' { Ok(E(K::UnknownEscSeq, M::NONE)) } else if seq6 == '~' { Ok(match (seq2, seq3, seq5) { ('1', '5', CTRL) => E(K::F(5), M::CTRL), //('1', '5', '6') => E(K::F(17), M::CTRL), ('1', '7', CTRL) => E(K::F(6), M::CTRL), //('1', '7', '6') => E(K::F(18), M::CTRL), ('1', '8', CTRL) => E(K::F(7), M::CTRL), ('1', '9', CTRL) => E(K::F(8), M::CTRL), //('1', '9', '6') => E(K::F(19), M::CTRL), ('2', '0', CTRL) => E(K::F(9), M::CTRL), //('2', '0', '6') => E(K::F(21), M::CTRL), ('2', '1', CTRL) => E(K::F(10), M::CTRL), //('2', '1', '6') => E(K::F(22), M::CTRL), ('2', '3', CTRL) => E(K::F(11), M::CTRL), //('2', '3', '6') => E(K::F(23), M::CTRL), ('2', '4', CTRL) => E(K::F(12), M::CTRL), //('2', '4', '6') => E(K::F(24), M::CTRL), _ => { debug!(target: "rustyline", "unsupported esc sequence: \\E[{}{};{}~", seq2, seq3, seq5); E(K::UnknownEscSeq, M::NONE) } }) } else { debug!(target: "rustyline", "unsupported esc sequence: \\E[{}{};{}{}", seq2, seq3, seq5, seq6); Ok(E(K::UnknownEscSeq, M::NONE)) } } else { debug!(target: "rustyline", "unsupported esc sequence: \\E[{}{};{:?}", seq2, seq3, seq5); Ok(E(K::UnknownEscSeq, M::NONE)) } } else if seq4.is_ascii_digit() { let seq5 = self.next_char()?; if seq5 == '~' { Ok(match (seq2, seq3, seq4) { ('2', '0', '0') => E(K::BracketedPasteStart, M::NONE), ('2', '0', '1') => E(K::BracketedPasteEnd, M::NONE), _ => { debug!(target: "rustyline", "unsupported esc sequence: \\E[{}{}{}~", seq2, seq3, seq4); E(K::UnknownEscSeq, M::NONE) } }) } else { debug!(target: "rustyline", "unsupported esc sequence: \\E[{}{}{}{}", seq2, seq3, seq4, seq5); Ok(E(K::UnknownEscSeq, M::NONE)) } } else { debug!(target: "rustyline", "unsupported esc sequence: \\E[{}{}{:?}", seq2, seq3, seq4); Ok(E(K::UnknownEscSeq, M::NONE)) } } else if seq3 == ';' { let seq4 = self.next_char()?; if seq4.is_ascii_digit() { let seq5 = self.next_char()?; if seq5.is_ascii_digit() { self.next_char()?; // 'R' expected //('1', '0', UP) => E(K::, M::), // Alt + Shift + Up Ok(E(K::UnknownEscSeq, M::NONE)) } else if seq2 == '1' { Ok(match (seq4, seq5) { (SHIFT, UP) => E(K::Up, M::SHIFT), // ~ key_sr (SHIFT, DOWN) => E(K::Down, M::SHIFT), // ~ key_sf (SHIFT, RIGHT) => E(K::Right, M::SHIFT), (SHIFT, LEFT) => E(K::Left, M::SHIFT), (SHIFT, END) => E(K::End, M::SHIFT), // kEND (SHIFT, HOME) => E(K::Home, M::SHIFT), // kHOM //('2', 'P') => E(K::F(13), M::NONE), //('2', 'Q') => E(K::F(14), M::NONE), //('2', 'S') => E(K::F(16), M::NONE), (ALT, UP) => E(K::Up, M::ALT), (ALT, DOWN) => E(K::Down, M::ALT), (ALT, RIGHT) => E(K::Right, M::ALT), (ALT, LEFT) => E(K::Left, M::ALT), (ALT, END) => E(K::End, M::ALT), (ALT, HOME) => E(K::Home, M::ALT), (ALT_SHIFT, UP) => E(K::Up, M::ALT_SHIFT), (ALT_SHIFT, DOWN) => E(K::Down, M::ALT_SHIFT), (ALT_SHIFT, RIGHT) => E(K::Right, M::ALT_SHIFT), (ALT_SHIFT, LEFT) => E(K::Left, M::ALT_SHIFT), (ALT_SHIFT, END) => E(K::End, M::ALT_SHIFT), (ALT_SHIFT, HOME) => E(K::Home, M::ALT_SHIFT), (CTRL, UP) => E(K::Up, M::CTRL), (CTRL, DOWN) => E(K::Down, M::CTRL), (CTRL, RIGHT) => E(K::Right, M::CTRL), (CTRL, LEFT) => E(K::Left, M::CTRL), (CTRL, END) => E(K::End, M::CTRL), (CTRL, HOME) => E(K::Home, M::CTRL), (CTRL, 'P') => E(K::F(1), M::CTRL), (CTRL, 'Q') => E(K::F(2), M::CTRL), (CTRL, 'S') => E(K::F(4), M::CTRL), (CTRL, 'p') => E(K::Char('0'), M::CTRL), (CTRL, 'q') => E(K::Char('1'), M::CTRL), (CTRL, 'r') => E(K::Char('2'), M::CTRL), (CTRL, 's') => E(K::Char('3'), M::CTRL), (CTRL, 't') => E(K::Char('4'), M::CTRL), (CTRL, 'u') => E(K::Char('5'), M::CTRL), (CTRL, 'v') => E(K::Char('6'), M::CTRL), (CTRL, 'w') => E(K::Char('7'), M::CTRL), (CTRL, 'x') => E(K::Char('8'), M::CTRL), (CTRL, 'y') => E(K::Char('9'), M::CTRL), (CTRL_SHIFT, UP) => E(K::Up, M::CTRL_SHIFT), (CTRL_SHIFT, DOWN) => E(K::Down, M::CTRL_SHIFT), (CTRL_SHIFT, RIGHT) => E(K::Right, M::CTRL_SHIFT), (CTRL_SHIFT, LEFT) => E(K::Left, M::CTRL_SHIFT), (CTRL_SHIFT, END) => E(K::End, M::CTRL_SHIFT), (CTRL_SHIFT, HOME) => E(K::Home, M::CTRL_SHIFT), //('6', 'P') => E(K::F(13), M::CTRL), //('6', 'Q') => E(K::F(14), M::CTRL), //('6', 'S') => E(K::F(16), M::CTRL), (CTRL_SHIFT, 'p') => E(K::Char('0'), M::CTRL_SHIFT), (CTRL_SHIFT, 'q') => E(K::Char('1'), M::CTRL_SHIFT), (CTRL_SHIFT, 'r') => E(K::Char('2'), M::CTRL_SHIFT), (CTRL_SHIFT, 's') => E(K::Char('3'), M::CTRL_SHIFT), (CTRL_SHIFT, 't') => E(K::Char('4'), M::CTRL_SHIFT), (CTRL_SHIFT, 'u') => E(K::Char('5'), M::CTRL_SHIFT), (CTRL_SHIFT, 'v') => E(K::Char('6'), M::CTRL_SHIFT), (CTRL_SHIFT, 'w') => E(K::Char('7'), M::CTRL_SHIFT), (CTRL_SHIFT, 'x') => E(K::Char('8'), M::CTRL_SHIFT), (CTRL_SHIFT, 'y') => E(K::Char('9'), M::CTRL_SHIFT), (CTRL_ALT, UP) => E(K::Up, M::CTRL_ALT), (CTRL_ALT, DOWN) => E(K::Down, M::CTRL_ALT), (CTRL_ALT, RIGHT) => E(K::Right, M::CTRL_ALT), (CTRL_ALT, LEFT) => E(K::Left, M::CTRL_ALT), (CTRL_ALT, END) => E(K::End, M::CTRL_ALT), (CTRL_ALT, HOME) => E(K::Home, M::CTRL_ALT), (CTRL_ALT, 'p') => E(K::Char('0'), M::CTRL_ALT), (CTRL_ALT, 'q') => E(K::Char('1'), M::CTRL_ALT), (CTRL_ALT, 'r') => E(K::Char('2'), M::CTRL_ALT), (CTRL_ALT, 's') => E(K::Char('3'), M::CTRL_ALT), (CTRL_ALT, 't') => E(K::Char('4'), M::CTRL_ALT), (CTRL_ALT, 'u') => E(K::Char('5'), M::CTRL_ALT), (CTRL_ALT, 'v') => E(K::Char('6'), M::CTRL_ALT), (CTRL_ALT, 'w') => E(K::Char('7'), M::CTRL_ALT), (CTRL_ALT, 'x') => E(K::Char('8'), M::CTRL_ALT), (CTRL_ALT, 'y') => E(K::Char('9'), M::CTRL_ALT), (CTRL_ALT_SHIFT, UP) => E(K::Up, M::CTRL_ALT_SHIFT), (CTRL_ALT_SHIFT, DOWN) => E(K::Down, M::CTRL_ALT_SHIFT), (CTRL_ALT_SHIFT, RIGHT) => E(K::Right, M::CTRL_ALT_SHIFT), (CTRL_ALT_SHIFT, LEFT) => E(K::Left, M::CTRL_ALT_SHIFT), (CTRL_ALT_SHIFT, END) => E(K::End, M::CTRL_ALT_SHIFT), (CTRL_ALT_SHIFT, HOME) => E(K::Home, M::CTRL_ALT_SHIFT), (CTRL_ALT_SHIFT, 'p') => E(K::Char('0'), M::CTRL_ALT_SHIFT), (CTRL_ALT_SHIFT, 'q') => E(K::Char('1'), M::CTRL_ALT_SHIFT), (CTRL_ALT_SHIFT, 'r') => E(K::Char('2'), M::CTRL_ALT_SHIFT), (CTRL_ALT_SHIFT, 's') => E(K::Char('3'), M::CTRL_ALT_SHIFT), (CTRL_ALT_SHIFT, 't') => E(K::Char('4'), M::CTRL_ALT_SHIFT), (CTRL_ALT_SHIFT, 'u') => E(K::Char('5'), M::CTRL_ALT_SHIFT), (CTRL_ALT_SHIFT, 'v') => E(K::Char('6'), M::CTRL_ALT_SHIFT), (CTRL_ALT_SHIFT, 'w') => E(K::Char('7'), M::CTRL_ALT_SHIFT), (CTRL_ALT_SHIFT, 'x') => E(K::Char('8'), M::CTRL_ALT_SHIFT), (CTRL_ALT_SHIFT, 'y') => E(K::Char('9'), M::CTRL_ALT_SHIFT), // Meta + arrow on (some?) Macs when using iTerm defaults ('9', UP) => E(K::Up, M::ALT), ('9', DOWN) => E(K::Down, M::ALT), ('9', RIGHT) => E(K::Right, M::ALT), ('9', LEFT) => E(K::Left, M::ALT), _ => { debug!(target: "rustyline", "unsupported esc sequence: \\E[1;{}{:?}", seq4, seq5); E(K::UnknownEscSeq, M::NONE) } }) } else if seq5 == '~' { Ok(match (seq2, seq4) { (INSERT, SHIFT) => E(K::Insert, M::SHIFT), (INSERT, ALT) => E(K::Insert, M::ALT), (INSERT, ALT_SHIFT) => E(K::Insert, M::ALT_SHIFT), (INSERT, CTRL) => E(K::Insert, M::CTRL), (INSERT, CTRL_SHIFT) => E(K::Insert, M::CTRL_SHIFT), (INSERT, CTRL_ALT) => E(K::Insert, M::CTRL_ALT), (INSERT, CTRL_ALT_SHIFT) => E(K::Insert, M::CTRL_ALT_SHIFT), (DELETE, SHIFT) => E(K::Delete, M::SHIFT), (DELETE, ALT) => E(K::Delete, M::ALT), (DELETE, ALT_SHIFT) => E(K::Delete, M::ALT_SHIFT), (DELETE, CTRL) => E(K::Delete, M::CTRL), (DELETE, CTRL_SHIFT) => E(K::Delete, M::CTRL_SHIFT), (DELETE, CTRL_ALT) => E(K::Delete, M::CTRL_ALT), (DELETE, CTRL_ALT_SHIFT) => E(K::Delete, M::CTRL_ALT_SHIFT), (PAGE_UP, SHIFT) => E(K::PageUp, M::SHIFT), (PAGE_UP, ALT) => E(K::PageUp, M::ALT), (PAGE_UP, ALT_SHIFT) => E(K::PageUp, M::ALT_SHIFT), (PAGE_UP, CTRL) => E(K::PageUp, M::CTRL), (PAGE_UP, CTRL_SHIFT) => E(K::PageUp, M::CTRL_SHIFT), (PAGE_UP, CTRL_ALT) => E(K::PageUp, M::CTRL_ALT), (PAGE_UP, CTRL_ALT_SHIFT) => E(K::PageUp, M::CTRL_ALT_SHIFT), (PAGE_DOWN, SHIFT) => E(K::PageDown, M::SHIFT), (PAGE_DOWN, ALT) => E(K::PageDown, M::ALT), (PAGE_DOWN, ALT_SHIFT) => E(K::PageDown, M::ALT_SHIFT), (PAGE_DOWN, CTRL) => E(K::PageDown, M::CTRL), (PAGE_DOWN, CTRL_SHIFT) => E(K::PageDown, M::CTRL_SHIFT), (PAGE_DOWN, CTRL_ALT) => E(K::PageDown, M::CTRL_ALT), (PAGE_DOWN, CTRL_ALT_SHIFT) => E(K::PageDown, M::CTRL_ALT_SHIFT), _ => { debug!(target: "rustyline", "unsupported esc sequence: \\E[{};{:?}~", seq2, seq4); E(K::UnknownEscSeq, M::NONE) } }) } else { debug!(target: "rustyline", "unsupported esc sequence: \\E[{};{}{:?}", seq2, seq4, seq5); Ok(E(K::UnknownEscSeq, M::NONE)) } } else { debug!(target: "rustyline", "unsupported esc sequence: \\E[{};{:?}", seq2, seq4); Ok(E(K::UnknownEscSeq, M::NONE)) } } else { Ok(match (seq2, seq3) { (DELETE, RXVT_CTRL) => E(K::Delete, M::CTRL), (DELETE, RXVT_CTRL_SHIFT) => E(K::Delete, M::CTRL_SHIFT), (CTRL, UP) => E(K::Up, M::CTRL), (CTRL, DOWN) => E(K::Down, M::CTRL), (CTRL, RIGHT) => E(K::Right, M::CTRL), (CTRL, LEFT) => E(K::Left, M::CTRL), (PAGE_UP, RXVT_CTRL) => E(K::PageUp, M::CTRL), (PAGE_UP, RXVT_SHIFT) => E(K::PageUp, M::SHIFT), (PAGE_UP, RXVT_CTRL_SHIFT) => E(K::PageUp, M::CTRL_SHIFT), (PAGE_DOWN, RXVT_CTRL) => E(K::PageDown, M::CTRL), (PAGE_DOWN, RXVT_SHIFT) => E(K::PageDown, M::SHIFT), (PAGE_DOWN, RXVT_CTRL_SHIFT) => E(K::PageDown, M::CTRL_SHIFT), (RXVT_HOME, RXVT_CTRL) => E(K::Home, M::CTRL), (RXVT_HOME, RXVT_SHIFT) => E(K::Home, M::SHIFT), (RXVT_HOME, RXVT_CTRL_SHIFT) => E(K::Home, M::CTRL_SHIFT), (RXVT_END, RXVT_CTRL) => E(K::End, M::CTRL), // kEND5 or kel (RXVT_END, RXVT_SHIFT) => E(K::End, M::SHIFT), (RXVT_END, RXVT_CTRL_SHIFT) => E(K::End, M::CTRL_SHIFT), _ => { debug!(target: "rustyline", "unsupported esc sequence: \\E[{}{:?}", seq2, seq3); E(K::UnknownEscSeq, M::NONE) } }) } } /// Handle \EO escape sequences fn escape_o(&mut self) -> Result { let seq2 = self.next_char()?; Ok(match seq2 { UP => E(K::Up, M::NONE), DOWN => E(K::Down, M::NONE), RIGHT => E(K::Right, M::NONE), LEFT => E(K::Left, M::NONE), //'E' => E(K::, M::),// key_b2, kb2 END => E(K::End, M::NONE), // kend HOME => E(K::Home, M::NONE), // khome 'M' => E::ENTER, // kent 'P' => E(K::F(1), M::NONE), // kf1 'Q' => E(K::F(2), M::NONE), // kf2 'R' => E(K::F(3), M::NONE), // kf3 'S' => E(K::F(4), M::NONE), // kf4 'a' => E(K::Up, M::CTRL), 'b' => E(K::Down, M::CTRL), 'c' => E(K::Right, M::CTRL), // rxvt 'd' => E(K::Left, M::CTRL), // rxvt 'l' => E(K::F(8), M::NONE), 't' => E(K::F(5), M::NONE), // kf5 or kb1 'u' => E(K::F(6), M::NONE), // kf6 or kb2 'v' => E(K::F(7), M::NONE), // kf7 or kb3 'w' => E(K::F(9), M::NONE), // kf9 or ka1 'x' => E(K::F(10), M::NONE), // kf10 or ka2 _ => { debug!(target: "rustyline", "unsupported esc sequence: \\EO{:?}", seq2); E(K::UnknownEscSeq, M::NONE) } }) } fn poll(&mut self, timeout_ms: i32) -> Result { let n = self.tty_in.buffer().len(); if n > 0 { return Ok(n as i32); } let mut fds = [poll::PollFd::new(self, PollFlags::POLLIN)]; let r = poll::poll(&mut fds, timeout_ms); match r { Ok(n) => Ok(n), Err(Errno::EINTR) => { if self.tty_in.get_ref().sigwinch()? { Err(ReadlineError::WindowResized) } else { Ok(0) // Ignore EINTR while polling } } Err(e) => Err(e.into()), } } fn select(&mut self, single_esc_abort: bool) -> Result { let tty_in = self.as_fd(); let sigwinch_pipe = self .tty_in .get_ref() .sigwinch_pipe .map(|fd| unsafe { BorrowedFd::borrow_raw(fd) }); let pipe_reader = self .pipe_reader .as_ref() .map(|pr| pr.lock().unwrap().0.as_raw_fd()) .map(|fd| unsafe { BorrowedFd::borrow_raw(fd) }); loop { let mut readfds = FdSet::new(); if let Some(ref sigwinch_pipe) = sigwinch_pipe { readfds.insert(sigwinch_pipe); } readfds.insert(&tty_in); if let Some(ref pipe_reader) = pipe_reader { readfds.insert(pipe_reader); } if let Err(err) = select::select(None, Some(&mut readfds), None, None, None) { if err == Errno::EINTR && self.tty_in.get_ref().sigwinch()? { return Err(ReadlineError::WindowResized); } else if err != Errno::EINTR { return Err(err.into()); } else { continue; } }; if sigwinch_pipe.map_or(false, |fd| readfds.contains(&fd)) { self.tty_in.get_ref().sigwinch()?; return Err(ReadlineError::WindowResized); } else if readfds.contains(&tty_in) { // prefer user input over external print return self.next_key(single_esc_abort).map(Event::KeyPress); } else if let Some(ref pipe_reader) = self.pipe_reader { let mut guard = pipe_reader.lock().unwrap(); let mut buf = [0; 1]; guard.0.read_exact(&mut buf)?; if let Ok(msg) = guard.1.try_recv() { return Ok(Event::ExternalPrint(msg)); } } } } } impl RawReader for PosixRawReader { #[cfg(not(feature = "signal-hook"))] fn wait_for_input(&mut self, single_esc_abort: bool) -> Result { match self.pipe_reader { Some(_) => self.select(single_esc_abort), None => self.next_key(single_esc_abort).map(Event::KeyPress), } } #[cfg(feature = "signal-hook")] fn wait_for_input(&mut self, single_esc_abort: bool) -> Result { self.select(single_esc_abort) } fn next_key(&mut self, single_esc_abort: bool) -> Result { let c = self.next_char()?; let mut key = KeyEvent::new(c, M::NONE); if key == E::ESC { if !self.tty_in.buffer().is_empty() { debug!(target: "rustyline", "read buffer {:?}", self.tty_in.buffer()); } let timeout_ms = if single_esc_abort && self.timeout_ms == -1 { 0 } else { self.timeout_ms }; match self.poll(timeout_ms) { Ok(0) => { // single escape } Ok(_) => { // escape sequence key = self.escape_sequence()? } // Err(ref e) if e.kind() == ErrorKind::Interrupted => continue, Err(e) => return Err(e), } } debug!(target: "rustyline", "c: {:?} => key: {:?}", c, key); Ok(key) } fn next_char(&mut self) -> Result { let mut buf = [0; 1]; let mut receiver = Utf8 { c: None, valid: true, }; loop { let n = self.tty_in.read(&mut buf)?; if n == 0 { return Err(ReadlineError::Eof); } let b = buf[0]; self.parser.advance(&mut receiver, b); if !receiver.valid { return Err(ReadlineError::from(ErrorKind::InvalidData)); } else if let Some(c) = 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 == E(K::BracketedPasteEnd, M::NONE) { break; } else { continue; // TODO validate } } c => buffer.push(c), }; } let buffer = buffer.replace("\r\n", "\n"); let buffer = buffer.replace('\r', "\n"); Ok(buffer) } fn find_binding(&self, key: &KeyEvent) -> Option { let cmd = self.key_map.get(key).cloned(); if let Some(ref cmd) = cmd { debug!(target: "rustyline", "terminal key binding: {:?} => {:?}", key, cmd); } cmd } } impl Receiver for Utf8 { /// 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: RawFd, cols: usize, // Number of columns in terminal buffer: String, tab_stop: usize, colors_enabled: bool, bell_style: BellStyle, } impl PosixRenderer { fn new(out: RawFd, 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, } } fn clear_old_rows(&mut self, layout: &Layout) { use std::fmt::Write; let current_row = layout.cursor.row; let old_rows = layout.end.row; // old_rows < cursor_row if the prompt spans multiple lines and if // this is the default State. let cursor_row_movement = old_rows.saturating_sub(current_row); // move the cursor down as required if cursor_row_movement > 0 { write!(self.buffer, "\x1b[{cursor_row_movement}B").unwrap(); } // clear old rows for _ in 0..old_rows { self.buffer.push_str("\r\x1b[K\x1b[A"); } // clear the line self.buffer.push_str("\r\x1b[K"); } } 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 == cmp::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[{row_shift}B")?; } } else if row_ordering == cmp::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[{row_shift}A")?; } } let col_ordering = new.col.cmp(&old.col); if col_ordering == cmp::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[{col_shift}C")?; } } else if col_ordering == cmp::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[{col_shift}D")?; } } write_all(self.out, self.buffer.as_str())?; Ok(()) } 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; self.clear_old_rows(old_layout); if let Some(highlighter) = highlighter { // display the prompt self.buffer .push_str(&highlighter.highlight_prompt(prompt, default_prompt)); // display the input line self.buffer .push_str(&highlighter.highlight(line, line.pos())); } else { // display the prompt self.buffer.push_str(prompt); // display the input line self.buffer.push_str(line); } // 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 && !hint .map(|h| h.ends_with('\n')) .unwrap_or_else(|| line.ends_with('\n')) { self.buffer.push('\n'); } // position the cursor let new_cursor_row_movement = end_pos.row - cursor.row; // move the cursor up as required if new_cursor_row_movement > 0 { write!(self.buffer, "\x1b[{new_cursor_row_movement}A")?; } // position the cursor within the line if cursor.col > 0 { write!(self.buffer, "\r\x1b[{}C", cursor.col).unwrap(); } else { self.buffer.push('\r'); } write_all(self.out, self.buffer.as_str())?; Ok(()) } fn write_and_flush(&mut self, buf: &str) -> Result<()> { write_all(self.out, buf)?; Ok(()) } /// 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 => self.write_and_flush("\x07"), _ => Ok(()), } } /// Clear the screen. Used to handle ctrl+l fn clear_screen(&mut self) -> Result<()> { self.write_and_flush("\x1b[H\x1b[J") } fn clear_rows(&mut self, layout: &Layout) -> Result<()> { self.buffer.clear(); self.clear_old_rows(layout); write_all(self.out, self.buffer.as_str())?; Ok(()) } /// 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("\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 != Some(1) { self.write_and_flush("\n")?; } Ok(()) } } 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)) } fn write_all(fd: RawFd, buf: &str) -> nix::Result<()> { let mut bytes = buf.as_bytes(); while !bytes.is_empty() { match write(fd, bytes) { Ok(0) => return Err(Errno::EIO), Ok(n) => bytes = &bytes[n..], Err(Errno::EINTR) => {} Err(r) => return Err(r), } } Ok(()) } pub struct PosixCursorGuard(RawFd); impl Drop for PosixCursorGuard { fn drop(&mut self) { let _ = set_cursor_visibility(self.0, true); } } fn set_cursor_visibility(fd: RawFd, visible: bool) -> Result> { write_all(fd, if visible { "\x1b[?25h" } else { "\x1b[?25l" })?; Ok(if visible { None } else { Some(PosixCursorGuard(fd)) }) } #[cfg(not(feature = "signal-hook"))] static mut SIGWINCH_PIPE: RawFd = -1; #[cfg(not(feature = "signal-hook"))] extern "C" fn sigwinch_handler(_: libc::c_int) { let _ = unsafe { write(SIGWINCH_PIPE, &[b's']) }; } #[derive(Clone, Debug)] struct SigWinCh { pipe: RawFd, #[cfg(not(feature = "signal-hook"))] original: nix::sys::signal::SigAction, #[cfg(feature = "signal-hook")] id: signal_hook::SigId, } impl SigWinCh { #[cfg(not(feature = "signal-hook"))] fn install_sigwinch_handler() -> Result { use nix::sys::signal; let (pipe, pipe_write) = UnixStream::pair()?; pipe.set_nonblocking(true)?; unsafe { SIGWINCH_PIPE = pipe_write.into_raw_fd() }; let sigwinch = signal::SigAction::new( signal::SigHandler::Handler(sigwinch_handler), signal::SaFlags::empty(), signal::SigSet::empty(), ); let original = unsafe { signal::sigaction(signal::SIGWINCH, &sigwinch)? }; Ok(SigWinCh { pipe: pipe.into_raw_fd(), original, }) } #[cfg(feature = "signal-hook")] fn install_sigwinch_handler() -> Result { let (pipe, pipe_write) = UnixStream::pair()?; pipe.set_nonblocking(true)?; let id = signal_hook::low_level::pipe::register(libc::SIGWINCH, pipe_write)?; Ok(SigWinCh { pipe: pipe.into_raw_fd(), id, }) } #[cfg(not(feature = "signal-hook"))] fn uninstall_sigwinch_handler(self) -> Result<()> { use nix::sys::signal; let _ = unsafe { signal::sigaction(signal::SIGWINCH, &self.original)? }; close(self.pipe)?; unsafe { close(SIGWINCH_PIPE)? }; unsafe { SIGWINCH_PIPE = -1 }; Ok(()) } #[cfg(feature = "signal-hook")] fn uninstall_sigwinch_handler(self) -> Result<()> { signal_hook::low_level::unregister(self.id); close(self.pipe)?; Ok(()) } } #[cfg(not(test))] pub type Terminal = PosixTerminal; #[derive(Clone, Debug)] pub struct PosixTerminal { unsupported: bool, tty_in: RawFd, is_in_a_tty: bool, tty_out: RawFd, is_out_a_tty: bool, close_on_drop: bool, pub(crate) color_mode: ColorMode, tab_stop: usize, bell_style: BellStyle, enable_bracketed_paste: bool, raw_mode: Arc, // external print reader pipe_reader: Option, // external print writer pipe_writer: Option, sigwinch: Option, } impl PosixTerminal { fn colors_enabled(&self) -> bool { match self.color_mode { ColorMode::Enabled => self.is_out_a_tty, ColorMode::Forced => true, ColorMode::Disabled => false, } } } impl Term for PosixTerminal { type CursorGuard = PosixCursorGuard; type ExternalPrinter = ExternalPrinter; type KeyMap = PosixKeyMap; type Mode = PosixMode; type Reader = PosixRawReader; type Writer = PosixRenderer; fn new( color_mode: ColorMode, behavior: Behavior, tab_stop: usize, bell_style: BellStyle, enable_bracketed_paste: bool, ) -> Result { let (tty_in, is_in_a_tty, tty_out, is_out_a_tty, close_on_drop) = if behavior == Behavior::PreferTerm { let tty = OpenOptions::new().read(true).write(true).open("/dev/tty"); if let Ok(tty) = tty { let fd = tty.into_raw_fd(); let is_a_tty = is_a_tty(fd); // TODO: useless ? (fd, is_a_tty, fd, is_a_tty, true) } else { ( libc::STDIN_FILENO, is_a_tty(libc::STDIN_FILENO), libc::STDOUT_FILENO, is_a_tty(libc::STDOUT_FILENO), false, ) } } else { ( libc::STDIN_FILENO, is_a_tty(libc::STDIN_FILENO), libc::STDOUT_FILENO, is_a_tty(libc::STDOUT_FILENO), false, ) }; let unsupported = is_unsupported_term(); #[allow(unused_variables)] let sigwinch = if !unsupported && is_in_a_tty && is_out_a_tty { Some(SigWinCh::install_sigwinch_handler()?) } else { None }; Ok(Self { unsupported, tty_in, is_in_a_tty, tty_out, is_out_a_tty, close_on_drop, color_mode, tab_stop, bell_style, enable_bracketed_paste, raw_mode: Arc::new(AtomicBool::new(false)), pipe_reader: None, pipe_writer: None, sigwinch, }) } // Init checks: /// Check if current terminal can provide a rich line-editing user /// interface. fn is_unsupported(&self) -> bool { self.unsupported } fn is_input_tty(&self) -> bool { self.is_in_a_tty } fn is_output_tty(&self) -> bool { self.is_out_a_tty } // Interactive loop: fn enable_raw_mode(&mut self) -> Result<(Self::Mode, PosixKeyMap)> { use nix::errno::Errno::ENOTTY; if !self.is_in_a_tty { return Err(ENOTTY.into()); } let (original_mode, key_map) = termios_::enable_raw_mode(self.tty_in)?; self.raw_mode.store(true, Ordering::SeqCst); // enable bracketed paste let out = if !self.enable_bracketed_paste { None } else if let Err(e) = write_all(self.tty_out, BRACKETED_PASTE_ON) { debug!(target: "rustyline", "Cannot enable bracketed paste: {}", e); None } else { Some(self.tty_out) }; // when all ExternalPrinter are dropped there is no need to use `pipe_reader` if Arc::strong_count(&self.raw_mode) == 1 { self.pipe_writer = None; self.pipe_reader = None; } Ok(( PosixMode { termios: original_mode, tty_in: self.tty_in, tty_out: out, raw_mode: self.raw_mode.clone(), }, key_map, )) } /// Create a RAW reader fn create_reader(&self, config: &Config, key_map: PosixKeyMap) -> PosixRawReader { PosixRawReader::new( self.tty_in, self.sigwinch.as_ref().map(|s| s.pipe), config, key_map, self.pipe_reader.clone(), ) } fn create_writer(&self) -> PosixRenderer { PosixRenderer::new( self.tty_out, self.tab_stop, self.colors_enabled(), self.bell_style, ) } fn writeln(&self) -> Result<()> { write_all(self.tty_out, "\n")?; Ok(()) } fn create_external_printer(&mut self) -> Result { if let Some(ref writer) = self.pipe_writer { return Ok(ExternalPrinter { writer: writer.clone(), raw_mode: self.raw_mode.clone(), tty_out: self.tty_out, }); } if self.unsupported || !self.is_input_tty() || !self.is_output_tty() { return Err(nix::Error::ENOTTY.into()); } use nix::unistd::pipe; use std::os::unix::io::FromRawFd; let (sender, receiver) = mpsc::sync_channel(1); // TODO validate: bound let (r, w) = pipe()?; let reader = Arc::new(Mutex::new((unsafe { File::from_raw_fd(r) }, receiver))); let writer = ( Arc::new(Mutex::new(unsafe { File::from_raw_fd(w) })), sender, ); self.pipe_reader.replace(reader); self.pipe_writer.replace(writer.clone()); Ok(ExternalPrinter { writer, raw_mode: self.raw_mode.clone(), tty_out: self.tty_out, }) } fn set_cursor_visibility(&mut self, visible: bool) -> Result> { if self.is_out_a_tty { set_cursor_visibility(self.tty_out, visible) } else { Ok(None) } } } #[allow(unused_must_use)] impl Drop for PosixTerminal { fn drop(&mut self) { if self.close_on_drop { close(self.tty_in); debug_assert_eq!(self.tty_in, self.tty_out); } if let Some(sigwinch) = self.sigwinch.take() { sigwinch.uninstall_sigwinch_handler(); } } } #[derive(Debug)] pub struct ExternalPrinter { writer: PipeWriter, raw_mode: Arc, tty_out: RawFd, } impl super::ExternalPrinter for ExternalPrinter { fn print(&mut self, msg: String) -> Result<()> { // write directly to stdout/stderr while not in raw mode if !self.raw_mode.load(Ordering::SeqCst) { write_all(self.tty_out, msg.as_str())?; } else if let Ok(mut writer) = self.writer.0.lock() { self.writer .1 .send(msg) .map_err(|_| io::Error::from(ErrorKind::Other))?; // FIXME writer.write_all(&[b'm'])?; writer.flush()?; } else { return Err(io::Error::from(ErrorKind::Other).into()); // FIXME } Ok(()) } } #[cfg(not(test))] pub fn suspend() -> Result<()> { use nix::sys::signal; use nix::unistd::Pid; // suspend the whole process group signal::kill(Pid::from_raw(0), signal::SIGTSTP)?; Ok(()) } #[cfg(not(feature = "termios"))] mod termios_ { use super::PosixKeyMap; use crate::keys::{KeyEvent, Modifiers as M}; use crate::{Cmd, Result}; use nix::sys::termios::{self, SetArg, SpecialCharacterIndices as SCI, Termios}; use std::collections::HashMap; use std::os::unix::io::{BorrowedFd, RawFd}; pub fn disable_raw_mode(tty_in: RawFd, termios: &Termios) -> Result<()> { let fd = unsafe { BorrowedFd::borrow_raw(tty_in) }; Ok(termios::tcsetattr(fd, SetArg::TCSADRAIN, termios)?) } pub fn enable_raw_mode(tty_in: RawFd) -> Result<(Termios, PosixKeyMap)> { use nix::sys::termios::{ControlFlags, InputFlags, LocalFlags}; let fd = unsafe { BorrowedFd::borrow_raw(tty_in) }; let original_mode = termios::tcgetattr(fd)?; 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[SCI::VMIN as usize] = 1; // One character-at-a-time input raw.control_chars[SCI::VTIME as usize] = 0; // with blocking read let mut key_map: HashMap = HashMap::with_capacity(4); map_key(&mut key_map, &raw, SCI::VEOF, "VEOF", Cmd::EndOfFile); map_key(&mut key_map, &raw, SCI::VINTR, "VINTR", Cmd::Interrupt); map_key(&mut key_map, &raw, SCI::VQUIT, "VQUIT", Cmd::Interrupt); map_key(&mut key_map, &raw, SCI::VSUSP, "VSUSP", Cmd::Suspend); termios::tcsetattr(fd, SetArg::TCSADRAIN, &raw)?; Ok((original_mode, key_map)) } fn map_key( key_map: &mut HashMap, raw: &Termios, index: SCI, name: &str, cmd: Cmd, ) { let cc = char::from(raw.control_chars[index as usize]); let key = KeyEvent::new(cc, M::NONE); log::debug!(target: "rustyline", "{}: {:?}", name, key); key_map.insert(key, cmd); } } #[cfg(feature = "termios")] mod termios_ { use super::PosixKeyMap; use crate::keys::{KeyEvent, Modifiers as M}; use crate::{Cmd, Result}; use std::collections::HashMap; use std::os::unix::io::RawFd; use termios::{self, Termios}; pub fn disable_raw_mode(tty_in: RawFd, termios: &Termios) -> Result<()> { Ok(termios::tcsetattr(tty_in, termios::TCSADRAIN, termios)?) } pub fn enable_raw_mode(tty_in: RawFd) -> Result<(Termios, PosixKeyMap)> { let original_mode = Termios::from_fd(tty_in)?; let mut raw = original_mode; // disable BREAK interrupt, CR to NL conversion on input, // input parity check, strip high bit (bit 8), output flow control raw.c_iflag &= !(termios::BRKINT | termios::ICRNL | termios::INPCK | termios::ISTRIP | termios::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.c_cflag |= termios::CS8; // disable echoing, canonical mode, extended input processing and signals raw.c_lflag &= !(termios::ECHO | termios::ICANON | termios::IEXTEN | termios::ISIG); raw.c_cc[termios::VMIN] = 1; // One character-at-a-time input raw.c_cc[termios::VTIME] = 0; // with blocking read let mut key_map: HashMap = HashMap::with_capacity(4); map_key(&mut key_map, &raw, termios::VEOF, "VEOF", Cmd::EndOfFile); map_key(&mut key_map, &raw, termios::VINTR, "VINTR", Cmd::Interrupt); map_key(&mut key_map, &raw, termios::VQUIT, "VQUIT", Cmd::Interrupt); map_key(&mut key_map, &raw, termios::VSUSP, "VSUSP", Cmd::Suspend); termios::tcsetattr(tty_in, termios::TCSADRAIN, &raw)?; Ok((original_mode, key_map)) } fn map_key( key_map: &mut HashMap, raw: &Termios, index: usize, name: &str, cmd: Cmd, ) { let cc = char::from(raw.c_cc[index]); let key = KeyEvent::new(cc, M::NONE); log::debug!(target: "rustyline", "{}: {:?}", name, key); key_map.insert(key, cmd); } } #[cfg(test)] mod test { use super::{Position, PosixRenderer, PosixTerminal, Renderer}; use crate::config::BellStyle; use crate::line_buffer::{LineBuffer, NoListener}; #[test] #[ignore] fn prompt_with_ansi_escape_codes() { let out = PosixRenderer::new(libc::STDOUT_FILENO, 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!(!super::is_unsupported_term()); std::env::set_var("TERM", "dumb"); assert!(super::is_unsupported_term()); } #[test] fn test_send() { fn assert_send() {} assert_send::(); } #[test] fn test_sync() { fn assert_sync() {} assert_sync::(); } #[test] fn test_line_wrap() { let mut out = PosixRenderer::new(libc::STDOUT_FILENO, 4, true, BellStyle::default()); let prompt = "> "; let default_prompt = true; let prompt_size = out.calculate_position(prompt, Position::default()); let mut line = LineBuffer::init("", 0); let old_layout = out.compute_layout(prompt_size, default_prompt, &line, None); assert_eq!(Position { col: 2, row: 0 }, old_layout.cursor); assert_eq!(old_layout.cursor, old_layout.end); assert_eq!( Some(true), line.insert('a', out.cols - prompt_size.col + 1, &mut NoListener) ); let new_layout = out.compute_layout(prompt_size, default_prompt, &line, None); assert_eq!(Position { col: 1, row: 1 }, new_layout.cursor); assert_eq!(new_layout.cursor, new_layout.end); out.refresh_line(prompt, &line, None, &old_layout, &new_layout, None) .unwrap(); #[rustfmt::skip] assert_eq!( "\r\u{1b}[K> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\u{1b}[1C", out.buffer ); } } rustyline-13.0.0/src/tty/windows.rs000064400000000000000000000740561046102023000154260ustar 00000000000000//! Windows specific definitions #![allow(clippy::try_err)] // suggested fix does not work (cannot infer...) use std::fs::OpenOptions; use std::io; use std::mem; use std::os::windows::io::IntoRawHandle; use std::ptr; use std::rc::Rc; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::{sync_channel, Receiver, SyncSender}; use std::sync::Arc; use log::{debug, warn}; use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; use winapi::shared::minwindef::{BOOL, DWORD, FALSE, TRUE, WORD}; use winapi::shared::winerror; use winapi::um::handleapi::{CloseHandle, INVALID_HANDLE_VALUE}; use winapi::um::synchapi::{CreateEventW, ResetEvent, SetEvent}; use winapi::um::wincon::{self, CONSOLE_SCREEN_BUFFER_INFO, COORD}; use winapi::um::winnt::{CHAR, HANDLE}; use winapi::um::{consoleapi, processenv, winbase, winuser}; use super::{width, Event, RawMode, RawReader, Renderer, Term}; use crate::config::{Behavior, BellStyle, ColorMode, Config}; use crate::highlight::Highlighter; use crate::keys::{KeyCode as K, KeyEvent, Modifiers as M}; use crate::layout::{Layout, Position}; use crate::line_buffer::LineBuffer; use crate::{error, Cmd, Result}; fn get_std_handle(fd: DWORD) -> Result { let handle = unsafe { processenv::GetStdHandle(fd) }; check_handle(handle) } fn check_handle(handle: HANDLE) -> Result { if handle == 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) } fn check(rc: BOOL) -> io::Result<()> { if rc == FALSE { Err(io::Error::last_os_error()) } else { Ok(()) } } fn get_win_size(handle: HANDLE) -> (usize, usize) { let mut info = unsafe { mem::zeroed() }; match unsafe { wincon::GetConsoleScreenBufferInfo(handle, &mut info) } { FALSE => (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(unsafe { consoleapi::GetConsoleMode(handle, &mut original_mode) })?; Ok(original_mode) } type ConsoleKeyMap = (); #[cfg(not(test))] pub type KeyMap = ConsoleKeyMap; #[cfg(not(test))] pub type Mode = ConsoleMode; #[must_use = "You must restore default mode (disable_raw_mode)"] #[derive(Clone, Debug)] pub struct ConsoleMode { original_conin_mode: DWORD, conin: HANDLE, original_conout_mode: Option, conout: HANDLE, raw_mode: Arc, } impl RawMode for ConsoleMode { /// Disable RAW mode for the terminal. fn disable_raw_mode(&self) -> Result<()> { check(unsafe { consoleapi::SetConsoleMode(self.conin, self.original_conin_mode) })?; if let Some(original_stdstream_mode) = self.original_conout_mode { check(unsafe { consoleapi::SetConsoleMode(self.conout, original_stdstream_mode) })?; } self.raw_mode.store(false, Ordering::SeqCst); Ok(()) } } /// Console input reader pub struct ConsoleRawReader { conin: HANDLE, // external print reader pipe_reader: Option>, } impl ConsoleRawReader { fn create(conin: HANDLE, pipe_reader: Option>) -> ConsoleRawReader { ConsoleRawReader { conin, pipe_reader } } fn select(&mut self) -> Result { use winapi::um::synchapi::WaitForMultipleObjects; use winapi::um::winbase::{INFINITE, WAIT_OBJECT_0}; let pipe_reader = self.pipe_reader.as_ref().unwrap(); let handles = [self.conin, pipe_reader.event.0]; let n = handles.len().try_into().unwrap(); loop { let rc = unsafe { WaitForMultipleObjects(n, handles.as_ptr(), FALSE, INFINITE) }; if rc == WAIT_OBJECT_0 { let mut count = 0; check(unsafe { consoleapi::GetNumberOfConsoleInputEvents(self.conin, &mut count) })?; match read_input(self.conin, count)? { KeyEvent(K::UnknownEscSeq, M::NONE) => continue, // no relevant key => return Ok(Event::KeyPress(key)), }; } else if rc == WAIT_OBJECT_0 + 1 { debug!(target: "rustyline", "ExternalPrinter::receive"); check(unsafe { ResetEvent(pipe_reader.event.0) })?; match pipe_reader.receiver.recv() { Ok(msg) => return Ok(Event::ExternalPrint(msg)), Err(e) => Err(io::Error::new(io::ErrorKind::InvalidInput, e))?, } } else { Err(io::Error::last_os_error())? } } } } impl RawReader for ConsoleRawReader { fn wait_for_input(&mut self, single_esc_abort: bool) -> Result { match self.pipe_reader { Some(_) => self.select(), None => self.next_key(single_esc_abort).map(Event::KeyPress), } } fn next_key(&mut self, _: bool) -> Result { read_input(self.conin, u32::MAX) } fn read_pasted_text(&mut self) -> Result { Ok(clipboard_win::get_clipboard_string()?) } fn find_binding(&self, _: &KeyEvent) -> Option { None } } fn read_input(handle: HANDLE, max_count: u32) -> 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 total = 0; let mut surrogate = 0; loop { if total >= max_count { return Ok(KeyEvent(K::UnknownEscSeq, M::NONE)); } // TODO GetNumberOfConsoleInputEvents check(unsafe { consoleapi::ReadConsoleInputW(handle, &mut rec, 1, &mut count) })?; total += count; if rec.EventType == wincon::WINDOW_BUFFER_SIZE_EVENT { debug!(target: "rustyline", "SIGWINCH"); return Err(error::ReadlineError::WindowResized); } 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 mut mods = M::NONE; if !alt_gr && key_event.dwControlKeyState & (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED) != 0 { mods |= M::CTRL; } if !alt_gr && key_event.dwControlKeyState & (LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED) != 0 { mods |= M::ALT; } if key_event.dwControlKeyState & SHIFT_PRESSED != 0 { mods |= M::SHIFT; } let utf16 = unsafe { *key_event.uChar.UnicodeChar() }; let key_code = match i32::from(key_event.wVirtualKeyCode) { winuser::VK_LEFT => K::Left, winuser::VK_RIGHT => K::Right, winuser::VK_UP => K::Up, winuser::VK_DOWN => K::Down, winuser::VK_DELETE => K::Delete, winuser::VK_HOME => K::Home, winuser::VK_END => K::End, winuser::VK_PRIOR => K::PageUp, winuser::VK_NEXT => K::PageDown, winuser::VK_INSERT => K::Insert, winuser::VK_F1 => K::F(1), winuser::VK_F2 => K::F(2), winuser::VK_F3 => K::F(3), winuser::VK_F4 => K::F(4), winuser::VK_F5 => K::F(5), winuser::VK_F6 => K::F(6), winuser::VK_F7 => K::F(7), winuser::VK_F8 => K::F(8), winuser::VK_F9 => K::F(9), winuser::VK_F10 => K::F(10), winuser::VK_F11 => K::F(11), winuser::VK_F12 => K::F(12), winuser::VK_BACK => K::Backspace, // vs Ctrl-h winuser::VK_RETURN => K::Enter, // vs Ctrl-m winuser::VK_ESCAPE => K::Esc, winuser::VK_TAB => { if mods.contains(M::SHIFT) { mods.remove(M::SHIFT); K::BackTab } else { K::Tab // vs Ctrl-i } } _ => { if utf16 == 0 { continue; } K::UnknownEscSeq } }; let key = if key_code != K::UnknownEscSeq { KeyEvent(key_code, mods) } else if utf16 == 27 { KeyEvent(K::Esc, mods) // FIXME dead code ? } else { if (0xD800..0xDC00).contains(&utf16) { surrogate = utf16; continue; } let orc = if surrogate == 0 { decode_utf16(Some(utf16)).next() } else { decode_utf16([surrogate, utf16].iter().copied()).next() }; let rc = if let Some(rc) = orc { rc } else { return Err(error::ReadlineError::Eof); }; let c = rc?; KeyEvent::new(c, mods) }; debug!(target: "rustyline", "wVirtualKeyCode: {:#x}, utf16: {:#x}, dwControlKeyState: {:#x} => key: {:?}", key_event.wVirtualKeyCode, utf16, key_event.dwControlKeyState,key); return Ok(key); } } pub struct ConsoleRenderer { conout: HANDLE, cols: usize, // Number of columns in terminal buffer: String, utf16: Vec, colors_enabled: bool, bell_style: BellStyle, } impl ConsoleRenderer { fn new(conout: HANDLE, colors_enabled: bool, bell_style: BellStyle) -> ConsoleRenderer { // Multi line editing is enabled by ENABLE_WRAP_AT_EOL_OUTPUT mode let (cols, _) = get_win_size(conout); ConsoleRenderer { conout, cols, buffer: String::with_capacity(1024), utf16: Vec::with_capacity(1024), colors_enabled, bell_style, } } fn get_console_screen_buffer_info(&self) -> Result { let mut info = unsafe { mem::zeroed() }; check(unsafe { wincon::GetConsoleScreenBufferInfo(self.conout, &mut info) })?; Ok(info) } fn set_console_cursor_position(&mut self, mut pos: COORD, size: COORD) -> Result { use std::cmp::{max, min}; // https://docs.microsoft.com/en-us/windows/console/setconsolecursorposition // > The coordinates must be within the boundaries of the console screen buffer. // pos.X = max(0, min(size.X - 1, pos.X)); pos.Y = max(0, min(size.Y - 1, pos.Y)); check(unsafe { wincon::SetConsoleCursorPosition(self.conout, pos) })?; Ok(pos) } fn clear(&mut self, length: DWORD, pos: COORD, attr: WORD) -> Result<()> { let mut _count = 0; check(unsafe { wincon::FillConsoleOutputCharacterA(self.conout, ' ' as CHAR, length, pos, &mut _count) })?; Ok(check(unsafe { wincon::FillConsoleOutputAttribute(self.conout, attr, length, pos, &mut _count) })?) } fn set_cursor_visibility(&mut self, visible: bool) -> Result> { set_cursor_visibility(self.conout, visible) } // You can't have both ENABLE_WRAP_AT_EOL_OUTPUT and // ENABLE_VIRTUAL_TERMINAL_PROCESSING. So we need to wrap manually. fn wrap_at_eol(&mut self, s: &str, mut col: usize) -> usize { let mut esc_seq = 0; for c in s.graphemes(true) { if c == "\n" { col = 0; } else { let cw = width(c, &mut esc_seq); col += cw; if col > self.cols { self.buffer.push('\n'); col = cw; } } self.buffer.push_str(c); } if col == self.cols { self.buffer.push('\n'); col = 0; } col } // position at the start of the prompt, clear to end of previous input fn clear_old_rows(&mut self, info: &CONSOLE_SCREEN_BUFFER_INFO, layout: &Layout) -> Result<()> { let current_row = layout.cursor.row; let old_rows = layout.end.row; let mut coord = info.dwCursorPosition; coord.X = 0; coord.Y -= current_row as i16; let coord = self.set_console_cursor_position(coord, info.dwSize)?; self.clear( (info.dwSize.X * (old_rows as i16 + 1)) as DWORD, coord, info.wAttributes, ) } } pub struct ConsoleCursorGuard(HANDLE); impl Drop for ConsoleCursorGuard { fn drop(&mut self) { let _ = set_cursor_visibility(self.0, true); } } fn set_cursor_visibility(handle: HANDLE, visible: bool) -> Result> { let mut info = unsafe { mem::zeroed() }; check(unsafe { wincon::GetConsoleCursorInfo(handle, &mut info) })?; let b = if visible { TRUE } else { FALSE }; if info.bVisible == b { return Ok(None); } info.bVisible = b; check(unsafe { wincon::SetConsoleCursorInfo(handle, &info) })?; Ok(if visible { None } else { Some(ConsoleCursorGuard(handle)) }) } impl Renderer for ConsoleRenderer { type Reader = ConsoleRawReader; fn move_cursor(&mut self, old: Position, new: Position) -> Result<()> { let info = self.get_console_screen_buffer_info()?; let mut cursor = 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, info.dwSize) .map(|_| ()) } 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; self.buffer.clear(); let mut col = 0; if let Some(highlighter) = highlighter { // TODO handle ansi escape code (SetConsoleTextAttribute) // append the prompt col = self.wrap_at_eol(&highlighter.highlight_prompt(prompt, default_prompt), col); // append the input line col = self.wrap_at_eol(&highlighter.highlight(line, line.pos()), col); } else if self.colors_enabled { // append the prompt col = self.wrap_at_eol(prompt, col); // append the input line col = self.wrap_at_eol(line, col); } 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.wrap_at_eol(&highlighter.highlight_hint(hint), col); } else if self.colors_enabled { self.wrap_at_eol(hint, col); } else { self.buffer.push_str(hint); } } let info = self.get_console_screen_buffer_info()?; // just to avoid flickering let mut guard = self.set_cursor_visibility(false)?; // position at the start of the prompt, clear to end of previous input self.clear_old_rows(&info, old_layout)?; // display prompt, input line and hint write_to_console(self.conout, self.buffer.as_str(), &mut self.utf16)?; // position the cursor let info = self.get_console_screen_buffer_info()?; let mut coord = info.dwCursorPosition; coord.X = cursor.col as i16; coord.Y -= (end_pos.row - cursor.row) as i16; self.set_console_cursor_position(coord, info.dwSize)?; guard.take(); Ok(()) } fn write_and_flush(&mut self, buf: &str) -> Result<()> { write_to_console(self.conout, buf, &mut self.utf16) } /// 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.graphemes(true) { if c == "\n" { pos.col = 0; pos.row += 1; } else { let cw = c.width(); 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 => write_all(self.conout, &[7; 1]), _ => 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 = COORD { X: 0, Y: 0 }; check(unsafe { wincon::SetConsoleCursorPosition(self.conout, coord) })?; let n = info.dwSize.X as DWORD * info.dwSize.Y as DWORD; self.clear(n, coord, info.wAttributes) } fn clear_rows(&mut self, layout: &Layout) -> Result<()> { let info = self.get_console_screen_buffer_info()?; self.clear_old_rows(&info, layout) } /// 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.conout); 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.conout); rows } fn colors_enabled(&self) -> bool { self.colors_enabled } fn move_cursor_at_leftmost(&mut self, _: &mut ConsoleRawReader) -> Result<()> { let info = self.get_console_screen_buffer_info()?; let mut cursor = info.dwCursorPosition; if cursor.X == 0 { return Ok(()); } debug!(target: "rustyline", "initial cursor location: {:?}, {:?}", cursor.X, cursor.Y); cursor.X = 0; cursor.Y += 1; let res = self.set_console_cursor_position(cursor, info.dwSize); if let Err(error::ReadlineError::Io(ref e)) = res { if e.raw_os_error() == Some(winerror::ERROR_INVALID_PARAMETER as i32) { warn!(target: "rustyline", "invalid cursor position: ({:?}, {:?}) in ({:?}, {:?})", cursor.X, cursor.Y, info.dwSize.X, info.dwSize.Y); write_all(self.conout, &[10; 1])?; return Ok(()); } } res.map(|_| ()) } } fn write_to_console(handle: HANDLE, s: &str, utf16: &mut Vec) -> Result<()> { utf16.clear(); utf16.extend(s.encode_utf16()); write_all(handle, utf16.as_slice()) } // See write_valid_utf8_to_console // /src/rust/library/std/src/sys/windows/stdio.rs:171 fn write_all(handle: HANDLE, mut data: &[u16]) -> Result<()> { use std::io::{Error, ErrorKind}; while !data.is_empty() { let slice = if data.len() < 8192 { data } else if (0xD800..0xDC00).contains(&data[8191]) { &data[..8191] } else { &data[..8192] }; let mut written = 0; check(unsafe { consoleapi::WriteConsoleW( handle, slice.as_ptr().cast::(), slice.len() as u32, &mut written, ptr::null_mut(), ) })?; if written == 0 { Err(Error::new(ErrorKind::WriteZero, "WriteConsoleW"))?; } data = &data[(written as usize)..]; } Ok(()) } #[cfg(not(test))] pub type Terminal = Console; #[derive(Clone, Debug)] pub struct Console { conin_isatty: bool, conin: HANDLE, conout_isatty: bool, conout: HANDLE, close_on_drop: bool, pub(crate) color_mode: ColorMode, ansi_colors_supported: bool, bell_style: BellStyle, raw_mode: Arc, // external print reader pipe_reader: Option>, // external print writer pipe_writer: Option>, } impl Console { fn colors_enabled(&self) -> bool { // TODO ANSI Colors & Windows <10 match self.color_mode { ColorMode::Enabled => self.conout_isatty && self.ansi_colors_supported, ColorMode::Forced => true, ColorMode::Disabled => false, } } } impl Term for Console { type CursorGuard = ConsoleCursorGuard; type ExternalPrinter = ExternalPrinter; type KeyMap = ConsoleKeyMap; type Mode = ConsoleMode; type Reader = ConsoleRawReader; type Writer = ConsoleRenderer; fn new( color_mode: ColorMode, behavior: Behavior, _tab_stop: usize, bell_style: BellStyle, _enable_bracketed_paste: bool, ) -> Result { let (conin, conout, close_on_drop) = if behavior == Behavior::PreferTerm { if let (Ok(conin), Ok(conout)) = ( OpenOptions::new().read(true).write(true).open("CONIN$"), OpenOptions::new().read(true).write(true).open("CONOUT$"), ) { ( Ok(conin.into_raw_handle()), Ok(conout.into_raw_handle()), true, ) } else { ( get_std_handle(winbase::STD_INPUT_HANDLE), get_std_handle(winbase::STD_OUTPUT_HANDLE), false, ) } } else { ( get_std_handle(winbase::STD_INPUT_HANDLE), get_std_handle(winbase::STD_OUTPUT_HANDLE), false, ) }; let conin_isatty = match conin { Ok(handle) => { // If this function doesn't fail then fd is a TTY get_console_mode(handle).is_ok() } Err(_) => false, }; let conout_isatty = match conout { Ok(handle) => { // If this function doesn't fail then fd is a TTY get_console_mode(handle).is_ok() } Err(_) => false, }; Ok(Console { conin_isatty, conin: conin.unwrap_or(ptr::null_mut()), conout_isatty, conout: conout.unwrap_or(ptr::null_mut()), close_on_drop, color_mode, ansi_colors_supported: false, bell_style, raw_mode: Arc::new(AtomicBool::new(false)), pipe_reader: None, pipe_writer: None, }) } /// Checking for an unsupported TERM in windows is a no-op fn is_unsupported(&self) -> bool { false } fn is_input_tty(&self) -> bool { self.conin_isatty } fn is_output_tty(&self) -> bool { self.conout_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<(ConsoleMode, ConsoleKeyMap)> { if !self.conin_isatty { Err(io::Error::new( io::ErrorKind::Other, "no stdio handle available for this process", ))?; } let original_conin_mode = get_console_mode(self.conin)?; // Disable these modes let mut raw = original_conin_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(unsafe { consoleapi::SetConsoleMode(self.conin, raw) })?; let original_conout_mode = if self.conout_isatty { let original_conout_mode = get_console_mode(self.conout)?; let mut mode = original_conout_mode; if mode & wincon::ENABLE_WRAP_AT_EOL_OUTPUT == 0 { mode |= wincon::ENABLE_WRAP_AT_EOL_OUTPUT; debug!(target: "rustyline", "activate ENABLE_WRAP_AT_EOL_OUTPUT"); unsafe { assert_ne!(consoleapi::SetConsoleMode(self.conout, mode), 0); } } // To enable ANSI colors (Windows 10 only): // https://docs.microsoft.com/en-us/windows/console/setconsolemode self.ansi_colors_supported = mode & wincon::ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0; if self.ansi_colors_supported { if self.color_mode == ColorMode::Disabled { mode &= !wincon::ENABLE_VIRTUAL_TERMINAL_PROCESSING; debug!(target: "rustyline", "deactivate ENABLE_VIRTUAL_TERMINAL_PROCESSING"); unsafe { assert_ne!(consoleapi::SetConsoleMode(self.conout, mode), 0); } } else { debug!(target: "rustyline", "ANSI colors already enabled"); } } else if self.color_mode != ColorMode::Disabled { mode |= wincon::ENABLE_VIRTUAL_TERMINAL_PROCESSING; self.ansi_colors_supported = unsafe { consoleapi::SetConsoleMode(self.conout, mode) != 0 }; debug!(target: "rustyline", "ansi_colors_supported: {}", self.ansi_colors_supported); } Some(original_conout_mode) } else { None }; self.raw_mode.store(true, Ordering::SeqCst); // when all ExternalPrinter are dropped there is no need to use `pipe_reader` if Arc::strong_count(&self.raw_mode) == 1 { self.pipe_writer = None; self.pipe_reader = None; } Ok(( ConsoleMode { original_conin_mode, conin: self.conin, original_conout_mode, conout: self.conout, raw_mode: self.raw_mode.clone(), }, (), )) } fn create_reader(&self, _: &Config, _: ConsoleKeyMap) -> ConsoleRawReader { ConsoleRawReader::create(self.conin, self.pipe_reader.clone()) } fn create_writer(&self) -> ConsoleRenderer { ConsoleRenderer::new(self.conout, self.colors_enabled(), self.bell_style) } fn writeln(&self) -> Result<()> { write_all(self.conout, &[10; 1]) } fn create_external_printer(&mut self) -> Result { if let Some(ref sender) = self.pipe_writer { return Ok(ExternalPrinter { event: self.pipe_reader.as_ref().unwrap().event.0, sender: sender.clone(), raw_mode: self.raw_mode.clone(), conout: self.conout, }); } if !self.is_input_tty() || !self.is_output_tty() { Err(io::Error::from(io::ErrorKind::Other))?; // FIXME } let event = unsafe { CreateEventW(ptr::null_mut(), TRUE, FALSE, ptr::null()) }; if event.is_null() { Err(io::Error::last_os_error())?; } let (sender, receiver) = sync_channel(1); let reader = Rc::new(AsyncPipe { event: Handle(event), receiver, }); self.pipe_reader.replace(reader); self.pipe_writer.replace(sender.clone()); Ok(ExternalPrinter { event, sender, raw_mode: self.raw_mode.clone(), conout: self.conout, }) } fn set_cursor_visibility(&mut self, visible: bool) -> Result> { if self.conout_isatty { set_cursor_visibility(self.conout, visible) } else { Ok(None) } } } impl Drop for Console { fn drop(&mut self) { if self.close_on_drop { unsafe { CloseHandle(self.conin) }; unsafe { CloseHandle(self.conout) }; } } } unsafe impl Send for Console {} unsafe impl Sync for Console {} #[derive(Debug)] struct AsyncPipe { event: Handle, receiver: Receiver, } #[derive(Debug)] pub struct ExternalPrinter { event: HANDLE, sender: SyncSender, raw_mode: Arc, conout: HANDLE, } unsafe impl Send for ExternalPrinter {} unsafe impl Sync for ExternalPrinter {} impl super::ExternalPrinter for ExternalPrinter { fn print(&mut self, msg: String) -> Result<()> { // write directly to stdout/stderr while not in raw mode if !self.raw_mode.load(Ordering::SeqCst) { let mut utf16 = Vec::new(); write_to_console(self.conout, msg.as_str(), &mut utf16) } else { self.sender .send(msg) .map_err(|_| io::Error::from(io::ErrorKind::Other))?; // FIXME Ok(check(unsafe { SetEvent(self.event) })?) } } } #[derive(Debug)] struct Handle(HANDLE); unsafe impl Send for Handle {} unsafe impl Sync for Handle {} impl Drop for Handle { fn drop(&mut self) { unsafe { CloseHandle(self.0) }; } } #[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-13.0.0/src/undo.rs000064400000000000000000000326261046102023000140560ustar 00000000000000//! Undo API use std::fmt::Debug; use crate::keymap::RepeatCount; use crate::line_buffer::{ChangeListener, DeleteListener, Direction, LineBuffer, NoListener}; 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(), &mut NoListener); } Change::Delete { idx, ref text } => { line.insert_str(idx, text, &mut NoListener); line.set_pos(idx + text.len()); } Change::Replace { idx, ref old, ref new, } => { line.replace(idx..idx + new.len(), old, &mut NoListener); } } } #[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, &mut NoListener); } Change::Delete { idx, ref text } => { line.delete_range(idx..idx + text.len(), &mut NoListener); } Change::Replace { idx, ref old, ref new, } => { line.replace(idx..idx + old.len(), new, &mut NoListener); } } } 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 } } } /// Undo manager pub struct Changeset { undo_group_level: u32, undos: Vec, // undoable changes redos: Vec, // undone changes, redoable } impl Changeset { pub(crate) fn new() -> Self { Self { undo_group_level: 0, undos: Vec::new(), redos: Vec::new(), } } pub(crate) 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(crate) 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(crate) 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(crate) 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(crate) 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(crate) 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(crate) 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; while 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); if waiting_for_begin <= 0 { count += 1; if count >= n { break; } } } undone } pub(crate) fn truncate(&mut self, len: usize) { debug!(target: "rustyline", "Changeset::truncate({})", len); self.undos.truncate(len); } #[cfg(test)] pub(crate) fn redo(&mut self, line: &mut LineBuffer) -> bool { let mut waiting_for_end = 0; let mut redone = false; while 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); if waiting_for_end <= 0 { break; } } redone } pub(crate) fn last_insert(&self) -> Option { for change in self.undos.iter().rev() { match change { Change::Insert { ref text, .. } => return Some(text.clone()), Change::Replace { ref new, .. } => return Some(new.clone()), Change::End => { continue; } _ => { return None; } } } None } } impl DeleteListener for Changeset { fn delete(&mut self, idx: usize, string: &str, _: Direction) { self.delete(idx, string); } } 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, NoListener}; #[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); buf.insert_str(0, "Hello", &mut NoListener); buf.insert_str(5, ", world!", &mut NoListener); 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); buf.insert_str(0, "Hello", &mut NoListener); 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); buf.insert_str(0, "Hlo", &mut NoListener); 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); buf.insert_str(0, "Hlo", &mut NoListener); 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); buf.insert_str(0, "Hello, world!", &mut NoListener); let mut cs = Changeset::new(); assert_eq!(buf.as_str(), "Hello, world!"); buf.replace(1..5, "i", &mut NoListener); 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-13.0.0/src/validate.rs000064400000000000000000000103251046102023000146720ustar 00000000000000//! Input validation API (Multi-line editing) use crate::keymap::Invoke; use crate::Result; /// Input validation result #[non_exhaustive] pub enum ValidationResult { /// Incomplete input Incomplete, /// Validation fails with an optional error message. User must fix the /// input. Invalid(Option), /// Validation succeeds with an optional message Valid(Option), } impl ValidationResult { pub(crate) fn is_valid(&self) -> bool { matches!(self, ValidationResult::Valid(_)) } pub(crate) fn has_message(&self) -> bool { matches!( self, ValidationResult::Valid(Some(_)) | ValidationResult::Invalid(Some(_)) ) } } /// Give access to user input. pub struct ValidationContext<'i> { i: &'i mut dyn Invoke, } impl<'i> ValidationContext<'i> { pub(crate) fn new(i: &'i mut dyn Invoke) -> Self { ValidationContext { i } } /// Returns user input. #[must_use] pub fn input(&self) -> &str { self.i.input() } // TODO //fn invoke(&mut self, cmd: Cmd) -> Result { // self.i.invoke(cmd) //} } /// This trait provides an extension interface for determining whether /// the current input buffer is valid. Rustyline uses the method /// provided by this trait to decide whether hitting the enter key /// will end the current editing session and return the current line /// buffer to the caller of `Editor::readline` or variants. pub trait Validator { /// Takes the currently edited `input` and returns a /// `ValidationResult` indicating whether it is valid or not along /// with an option message to display about the result. The most /// common validity check to implement is probably whether the /// input is complete or not, for instance ensuring that all /// delimiters are fully balanced. /// /// If you implement more complex validation checks it's probably /// a good idea to also implement a `Hinter` to provide feedback /// about what is invalid. /// /// For auto-correction like a missing closing quote or to reject invalid /// char while typing, the input will be mutable (TODO). fn validate(&self, ctx: &mut ValidationContext) -> Result { let _ = ctx; Ok(ValidationResult::Valid(None)) } /// Configure whether validation is performed while typing or only /// when user presses the Enter key. /// /// Default is `false`. /// /// This feature is not yet implemented, so this function is currently a /// no-op fn validate_while_typing(&self) -> bool { false } } impl Validator for () {} impl<'v, V: ?Sized + Validator> Validator for &'v V { fn validate(&self, ctx: &mut ValidationContext) -> Result { (**self).validate(ctx) } fn validate_while_typing(&self) -> bool { (**self).validate_while_typing() } } /// Simple matching bracket validator. #[derive(Default)] pub struct MatchingBracketValidator { _priv: (), } impl MatchingBracketValidator { /// Constructor #[must_use] pub fn new() -> Self { Self { _priv: () } } } impl Validator for MatchingBracketValidator { fn validate(&self, ctx: &mut ValidationContext) -> Result { Ok(validate_brackets(ctx.input())) } } fn validate_brackets(input: &str) -> ValidationResult { let mut stack = vec![]; for c in input.chars() { match c { '(' | '[' | '{' => stack.push(c), ')' | ']' | '}' => match (stack.pop(), c) { (Some('('), ')') | (Some('['), ']') | (Some('{'), '}') => {} (Some(wanted), _) => { return ValidationResult::Invalid(Some(format!( "Mismatched brackets: {wanted:?} is not properly closed" ))) } (None, c) => { return ValidationResult::Invalid(Some(format!( "Mismatched brackets: {c:?} is unpaired" ))) } }, _ => {} } } if stack.is_empty() { ValidationResult::Valid(None) } else { ValidationResult::Incomplete } }