rustyline-6.3.0/.cargo_vcs_info.json0000644000000001121372733122100131310ustar { "git": { "sha1": "cce7426920625c81f186c854138c84ea98ff7eac" } } rustyline-6.3.0/.github/workflows/rust.yml010064400007650000024000000014021372733102500170520ustar 00000000000000name: Rust on: push: branches: [master] paths: - '**.rs' - '**.toml' - '.github/workflows/rust.yml' pull_request: paths: - '**.rs' - '**.toml' - '.github/workflows/rust.yml' jobs: build: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, windows-latest] steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: default: true profile: minimal toolchain: stable override: true - name: Build run: cargo build --workspace --all-targets - name: Run tests run: cargo test --workspace --all-targets - name: Run doctests run: cargo test --workspace --doc env: RUST_BACKTRACE: 1 rustyline-6.3.0/.gitignore010064400007650000024000000002411346010623700137240ustar 00000000000000# Compiled files *.o *.so *.rlib *.dll # Executables *.exe # Generated by Cargo /target/ Cargo.lock # vim swap file *.swp # default history file history.txt rustyline-6.3.0/BUGS.md010064400007650000024000000026401365774656400130500ustar 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`). ## 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 verions >= 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)! rustyline-6.3.0/Cargo.lock0000644000000531631372733122100111220ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. [[package]] name = "aho-corasick" version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "043164d8ba5c4c3035fec9bbee8647c0261d788f3474306f93bb65901cae0e86" dependencies = [ "memchr", ] [[package]] name = "ansi_term" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" dependencies = [ "winapi", ] [[package]] name = "arrayref" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" [[package]] name = "arrayvec" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" [[package]] name = "assert_matches" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7deb0a829ca7bcfaf5da70b073a8d128619259a7be8216a355e23f00763059e5" [[package]] name = "atty" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi", "libc", "winapi", ] [[package]] name = "autocfg" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" [[package]] name = "base64" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" [[package]] name = "bitflags" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" [[package]] name = "blake2b_simd" version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a" dependencies = [ "arrayref", "arrayvec", "constant_time_eq", ] [[package]] name = "byteorder" version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" [[package]] name = "cc" version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66120af515773fb005778dc07c261bd201ec8ce50bd6e7144c927753fe013381" [[package]] name = "cfg-if" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" [[package]] name = "chrono" version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "942f72db697d8767c22d46a598e01f2d3b475501ea43d0db4f16d90259182d0b" dependencies = [ "num-integer", "num-traits", "time", ] [[package]] name = "clap" version = "2.33.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" dependencies = [ "ansi_term", "atty", "bitflags", "strsim 0.8.0", "textwrap", "unicode-width", "vec_map", ] [[package]] name = "constant_time_eq" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" [[package]] name = "crossbeam-channel" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87" dependencies = [ "crossbeam-utils", "maybe-uninit", ] [[package]] name = "crossbeam-deque" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f02af974daeee82218205558e51ec8768b48cf524bd01d550abe5573a608285" dependencies = [ "crossbeam-epoch", "crossbeam-utils", "maybe-uninit", ] [[package]] name = "crossbeam-epoch" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" dependencies = [ "autocfg", "cfg-if", "crossbeam-utils", "lazy_static", "maybe-uninit", "memoffset", "scopeguard", ] [[package]] name = "crossbeam-utils" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" dependencies = [ "autocfg", "cfg-if", "lazy_static", ] [[package]] name = "darling" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" dependencies = [ "darling_core", "darling_macro", ] [[package]] name = "darling_core" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim 0.9.3", "syn", ] [[package]] name = "darling_macro" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" dependencies = [ "darling_core", "quote", "syn", ] [[package]] name = "derive_builder" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2658621297f2cf68762a6f7dc0bb7e1ff2cfd6583daef8ee0fed6f7ec468ec0" dependencies = [ "darling", "derive_builder_core", "proc-macro2", "quote", "syn", ] [[package]] name = "derive_builder_core" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2791ea3e372c8495c0bc2033991d76b512cd799d07491fbd6890124db9458bef" dependencies = [ "darling", "proc-macro2", "quote", "syn", ] [[package]] name = "dirs" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901" dependencies = [ "libc", "redox_users", "winapi", ] [[package]] name = "dirs-next" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cbcf9241d9e8d106295bd496bbe2e9cffd5fa098f2a8c9e2bbcbf09773c11a8" dependencies = [ "cfg-if", "dirs-sys-next", ] [[package]] name = "dirs-sys-next" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c60f7b8a8953926148223260454befb50c751d3c50e1c178c4fd1ace4083c9a" 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.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd56b59865bce947ac5958779cfa508f6c3b9497cc762b7e24a12d11ccde2c4f" [[package]] name = "env_logger" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aafcde04e90a5226a6443b7aabdb016ba2f8307c847d524724bd9b346dd1a2d3" dependencies = [ "atty", "humantime", "log", "regex", "termcolor", ] [[package]] name = "env_logger" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" dependencies = [ "atty", "humantime", "log", "regex", "termcolor", ] [[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.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cda28c4acb13182d935e0ab6bc12329d1d22134d69801d0836d1ae4b47054f2a" dependencies = [ "thread_local", ] [[package]] name = "getrandom" version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6" dependencies = [ "cfg-if", "libc", "wasi 0.9.0+wasi-snapshot-preview1", ] [[package]] name = "hermit-abi" version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3deed196b6e7f9e44a2ae8d94225d80302d81208b1bb673fd21fe634645c85a9" dependencies = [ "libc", ] [[package]] name = "humantime" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" dependencies = [ "quick-error", ] [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[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.77" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f96b10ec2560088a8e76961b00d47107b3a625fecb76dedb29ee7ccbf98235" [[package]] name = "log" version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" dependencies = [ "cfg-if", ] [[package]] name = "maybe-uninit" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" [[package]] name = "memchr" version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" [[package]] name = "memoffset" version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c198b026e1bbf08a937e94c6c60f9ec4a2267f5b0d2eec9c1b21b061ce2be55f" dependencies = [ "autocfg", ] [[package]] name = "nix" version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c722bee1037d430d0f8e687bbdbf222f27cc6e4e68d5caf630857bb2b6dbdce" dependencies = [ "bitflags", "cc", "cfg-if", "libc", "void", ] [[package]] name = "nix" version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83450fe6a6142ddd95fb064b746083fc4ef1705fe81f64a64e1d4b39f54a1055" dependencies = [ "bitflags", "cc", "cfg-if", "libc", ] [[package]] name = "num-integer" version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b" dependencies = [ "autocfg", "num-traits", ] [[package]] name = "num-traits" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" dependencies = [ "autocfg", ] [[package]] name = "num_cpus" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" dependencies = [ "hermit-abi", "libc", ] [[package]] name = "ppv-lite86" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c36fa947111f5c62a733b652544dd0016a43ce89619538a8ef92724a6f501a20" [[package]] name = "proc-macro2" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36e28516df94f3dd551a587da5357459d9b36d945a7c37c3557928c1c2ff2a2c" dependencies = [ "unicode-xid", ] [[package]] name = "quick-error" version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" dependencies = [ "getrandom", "libc", "rand_chacha", "rand_core", "rand_hc", ] [[package]] name = "rand_chacha" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" dependencies = [ "getrandom", ] [[package]] name = "rand_hc" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" dependencies = [ "rand_core", ] [[package]] name = "rayon" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfd016f0c045ad38b5251be2c9c0ab806917f82da4d36b2a327e5166adad9270" dependencies = [ "autocfg", "crossbeam-deque", "either", "rayon-core", ] [[package]] name = "rayon-core" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91739a34c4355b5434ce54c9086c5895604a9c278586d1f1aa95e04f66b525a0" dependencies = [ "crossbeam-channel", "crossbeam-deque", "crossbeam-utils", "lazy_static", "num_cpus", ] [[package]] name = "redox_syscall" version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" [[package]] name = "redox_users" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" dependencies = [ "getrandom", "redox_syscall", "rust-argon2", ] [[package]] name = "regex" version = "1.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6" dependencies = [ "aho-corasick", "memchr", "regex-syntax", "thread_local", ] [[package]] name = "regex-syntax" version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" [[package]] name = "remove_dir_all" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" dependencies = [ "winapi", ] [[package]] name = "rust-argon2" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9dab61250775933275e84053ac235621dfb739556d5c54a2f2e9313b7cf43a19" dependencies = [ "base64", "blake2b_simd", "constant_time_eq", "crossbeam-utils", ] [[package]] name = "rustyline" version = "6.3.0" dependencies = [ "assert_matches", "cfg-if", "dirs-next", "doc-comment", "env_logger 0.7.1", "libc", "log", "memchr", "nix 0.18.0", "rustyline-derive", "scopeguard", "skim", "tempfile", "unicode-segmentation", "unicode-width", "utf8parse 0.2.0", "winapi", ] [[package]] name = "rustyline-derive" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54a50e29610a5be68d4a586a5cce3bfb572ed2c2a74227e4168444b7bf4e5235" dependencies = [ "quote", "syn", ] [[package]] name = "scopeguard" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "shlex" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" [[package]] name = "skim" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb226ef1f69c44352626532a14c0692be118eb9f93e91010150d165a9d693efa" dependencies = [ "bitflags", "chrono", "clap", "derive_builder", "env_logger 0.6.2", "fuzzy-matcher", "lazy_static", "log", "nix 0.14.1", "rayon", "regex", "shlex", "time", "timer", "tuikit", "unicode-width", "vte", ] [[package]] name = "strsim" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] name = "strsim" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" [[package]] name = "syn" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "963f7d3cc59b59b9325165add223142bbf1df27655d07789f109896d353d8350" dependencies = [ "proc-macro2", "quote", "unicode-xid", ] [[package]] name = "tempfile" version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" dependencies = [ "cfg-if", "libc", "rand", "redox_syscall", "remove_dir_all", "winapi", ] [[package]] name = "term" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edd106a334b7657c10b7c540a0106114feadeb4dc314513e97df481d5d966f42" dependencies = [ "byteorder", "dirs", "winapi", ] [[package]] name = "termcolor" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" dependencies = [ "winapi-util", ] [[package]] name = "textwrap" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" dependencies = [ "unicode-width", ] [[package]] name = "thread_local" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" dependencies = [ "lazy_static", ] [[package]] name = "time" version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" dependencies = [ "libc", "wasi 0.10.0+wasi-snapshot-preview1", "winapi", ] [[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.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "846bc187c93656d5ef5389809d1b6950fd1364906af5e9953a9de129c295c3a5" dependencies = [ "bitflags", "lazy_static", "log", "nix 0.14.1", "term", "unicode-width", ] [[package]] name = "unicode-segmentation" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" [[package]] name = "unicode-width" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" [[package]] name = "unicode-xid" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" [[package]] name = "utf8parse" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8772a4ccbb4e89959023bc5b7cb8623a795caa7092d99f3aa9501b9484d4557d" [[package]] name = "utf8parse" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" [[package]] name = "vec_map" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" [[package]] name = "void" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" [[package]] name = "vte" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f42f536e22f7fcbb407639765c8fd78707a33109301f834a594758bedd6e8cf" dependencies = [ "utf8parse 0.1.1", ] [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" [[package]] name = "wasi" version = "0.10.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" [[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-util" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" dependencies = [ "winapi", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" rustyline-6.3.0/Cargo.toml0000644000000043201372733122100111340ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies # # If you believe there's an error in this file please file an # issue against the rust-lang/cargo repository. If you're # editing this file be aware that the upstream Cargo.toml # will likely look very different (and much more reasonable) [package] edition = "2018" name = "rustyline" version = "6.3.0" authors = ["Katsu Kawakami "] description = "Rustyline, a readline implementation based on Antirez's Linenoise" documentation = "http://docs.rs/rustyline" readme = "README.md" keywords = ["readline"] categories = ["command-line-interface"] license = "MIT" repository = "https://github.com/kkawakam/rustyline" [package.metadata.docs.rs] all-features = false default-target = "x86_64-unknown-linux-gnu" features = ["with-dirs", "with-fuzzy"] no-default-features = true [dependencies.cfg-if] version = "0.1.6" [dependencies.dirs-next] version = "1.0" optional = true [dependencies.libc] version = "0.2" [dependencies.log] version = "0.4" [dependencies.memchr] version = "2.0" [dependencies.unicode-segmentation] version = "1.0" [dependencies.unicode-width] version = "0.1" [dev-dependencies.assert_matches] version = "1.2" [dev-dependencies.doc-comment] version = "0.3" [dev-dependencies.env_logger] version = "0.7" [dev-dependencies.rustyline-derive] version = "0.3.1" [dev-dependencies.tempfile] version = "3.1.0" [features] default = ["with-dirs"] with-dirs = ["dirs-next"] with-fuzzy = ["skim"] [target."cfg(unix)".dependencies.nix] version = "0.18" [target."cfg(unix)".dependencies.skim] version = "0.7" optional = true [target."cfg(unix)".dependencies.utf8parse] version = "0.2" [target."cfg(windows)".dependencies.scopeguard] version = "1.1" [target."cfg(windows)".dependencies.winapi] version = "0.3" features = ["consoleapi", "handleapi", "minwindef", "processenv", "winbase", "wincon", "winuser"] [badges.github-actions] repository = "kkawakam/rustyline" workflow = "Rust" [badges.maintenance] status = "actively-developed" rustyline-6.3.0/Cargo.toml.orig010064400007650000024000000026521372733102500146340ustar 00000000000000[package] name = "rustyline" version = "6.3.0" authors = ["Katsu Kawakami "] edition = "2018" description = "Rustyline, a readline implementation based on Antirez's Linenoise" documentation = "http://docs.rs/rustyline" repository = "https://github.com/kkawakam/rustyline" readme = "README.md" keywords = ["readline"] license = "MIT" categories = ["command-line-interface"] [badges] github-actions = { repository = "kkawakam/rustyline", workflow = "Rust" } maintenance = { status = "actively-developed" } [workspace] members = ["rustyline-derive"] [dependencies] cfg-if = "0.1.6" dirs-next = { version = "1.0", optional = true } libc = "0.2" log = "0.4" unicode-width = "0.1" unicode-segmentation = "1.0" memchr = "2.0" [target.'cfg(unix)'.dependencies] nix = "0.18" utf8parse = "0.2" skim = { version = "0.7", optional = true } [target.'cfg(windows)'.dependencies] winapi = { version = "0.3", features = ["consoleapi", "handleapi", "minwindef", "processenv", "winbase", "wincon", "winuser"] } scopeguard = "1.1" [dev-dependencies] doc-comment = "0.3" env_logger = "0.7" tempfile = "3.1.0" assert_matches = "1.2" rustyline-derive = { version = "0.3.1", path = "rustyline-derive" } [features] default = ["with-dirs"] with-dirs = ["dirs-next"] with-fuzzy = ["skim"] [package.metadata.docs.rs] features = ["with-dirs", "with-fuzzy"] all-features = false no-default-features = true default-target = "x86_64-unknown-linux-gnu" rustyline-6.3.0/Incremental.md010064400007650000024000000034731365774656400145560ustar 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 granurality: * PUSH_STRING at end of input * REFRESH_DIRTY only rows / cells rustyline-6.3.0/LICENSE010064400007650000024000000021161346010623700127440ustar 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-6.3.0/README.md010064400007650000024000000247041372733102500132260ustar 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) [![](http://meritbadge.herokuapp.com/rustyline)](https://crates.io/crates/rustyline) [![Docs](https://docs.rs/rustyline/badge.svg)](https://docs.rs/rustyline) Readline implementation in Rust that is based on [Antirez' Linenoise](https://github.com/antirez/linenoise) **Supported Platforms** * Unix (tested on FreeBSD, Linux and macOS) * Windows * cmd.exe * Powershell **Note**: * Powershell ISE is not supported, check [issue #56](https://github.com/kkawakam/rustyline/issues/56) * Mintty (Cygwin/MinGW) is not supported * Highlighting / Colors are not supported on Windows < Windows 10 except with ConEmu and `ColorMode::Forced`. ## Example ```rust use rustyline::error::ReadlineError; use rustyline::Editor; fn main() { // `()` can be used when no completer is required let mut rl = Editor::<()>::new(); if rl.load_history("history.txt").is_err() { println!("No previous history."); } loop { let readline = rl.readline(">> "); match readline { Ok(line) => { rl.add_history_entry(line.as_str()); println!("Line: {}", line); }, Err(ReadlineError::Interrupted) => { println!("CTRL-C"); break }, Err(ReadlineError::Eof) => { println!("CTRL-D"); break }, Err(err) => { println!("Error: {:?}", err); break } } } rl.save_history("history.txt").unwrap(); } ``` ## crates.io You can use this package in your project by adding the following to your `Cargo.toml`: ```toml [dependencies] rustyline = "6.3.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 | ? | ? | [linefeed][] | Rust | Ux/Win | Any | | Yes | any | Emacs/conf | Yes | No | ? | No | [linenoise][] | C | Ux | ANSI | No | Yes | only line | Emacs | No | No | Ux | Yes | [linenoise-ng][] | C | Ux/Win | ANSI | Yes | Yes | only line | Emacs | Yes | No | ? | ? | [Liner][] | Rust | Ux | ANSI | | No inc search | only word | Emacs/vi/prog | No | Yes | Ux | History based | [prompt_toolkit][] | Python | Ux/Win | ANSI | Yes | Yes | any | Emacs/vi/conf | Yes | Yes | Ux/Win | Yes | [rb-readline][] | Ruby | Ux/Win | ANSI | Yes | Yes | only word | Emacs/vi/conf | Yes | Yes | ? | No | [replxx][] | C/C++ | Ux/Win | ANSI | Yes | Yes | only line | Emacs | Yes | No | Ux/Win | Yes | Rustyline | Rust | Ux/Win | ANSI | Yes | Yes | any | Emacs/vi/bind | Yes | Yes | Ux/Win 10+ | Yes | [termwiz][] | Rust | Ux/Win | Any | ? | Yes | any | Emacs | No | No | Ux/Win | No | [go-prompt]: https://github.com/c-bata/go-prompt [Haskeline]: https://github.com/judah/haskeline [linefeed]: https://github.com/murarth/linefeed [linenoise]: https://github.com/antirez/linenoise [linenoise-ng]: https://github.com/arangodb/linenoise-ng [Liner]: https://github.com/redox-os/liner [prompt_toolkit]: https://github.com/jonathanslenders/python-prompt-toolkit [rb-readline]: https://github.com/ConnorAtherton/rb-readline [replxx]: https://github.com/AmokHuginnsson/replxx [termwiz]: https://github.com/wez/wezterm/tree/master/termwiz ## Multi line support This is a very simple feature that simply causes lines that are longer than the current terminal width to be displayed on the next visual line instead of horizontally scrolling as more characters are typed. Currently this feature is always enabled and there is no configuration option to disable it. This feature does not allow the end user to hit a special key sequence and enter a mode where hitting the return key will cause a literal newline to be added to the input buffer. The way to achieve multi-line editing is to implement the `Validator` trait. rustyline-6.3.0/TODO.md010064400007650000024000000055171365774656400130630ustar 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. - [ ] append_history - [ ] history_truncate_file Input - [ ] Password input (#58) (https://github.com/conradkdotcom/rpassword) (https://github.com/antirez/linenoise/issues/125) - [X] quoted insert (#65) - [ ] Overwrite mode (em-toggle-overwrite, vi-replace-mode, rl_insert_mode) - [ ] Encoding - [ ] [Ctrl-][Alt-][Shift-] (#121) Layout - [ ] Redraw perf (https://crates.io/crates/cassowary) Misc - [ ] fallible iterator (https://docs.rs/fallible-iterator/0.2.1/fallible_iterator/) Mouse - [ ] Mouse support Movement - [ ] Move to the corresponding opening/closing bracket Redo - [X] redo substitute Repeat - [X] dynamic prompt (arg: ?) - [ ] transpose chars Syntax - [ ] syntax specific tokenizer/parser - [ ] highlighting Undo - [ ] Merge consecutive Replace - [X] Undo group - [ ] Undo all changes made to this line. - [X] Kill+Insert (substitute/replace) - [X] Repeated undo `Undo(RepeatCount)` Unix - [ ] Terminfo (https://github.com/Stebalien/term) - [ ] [ncurses](https://crates.io/crates/ncurses) alternative backend ? - [X] [bracketed paste mode](https://cirw.in/blog/bracketed-paste) - [ ] async stdin (https://github.com/Rufflewind/tokio-file-unix) Windows - [ ] is_atty is not working with Cygwin/MSYS (https://github.com/softprops/atty works but then how to make `enable_raw_mode` works ?) (https://github.com/mitsuhiko/console/blob/master/src/windows_term.rs#L285) (https://github.com/mattn/go-isatty/blob/master/isatty_windows.go, https://github.com/mattn/go-tty/blob/master/tty_windows.go#L143) - [X] UTF-16 surrogate pair - [ ] handle ANSI escape code (#61) (https://github.com/DanielKeep/rust-ansi-interpreter) rustyline-6.3.0/examples/diy_hints.rs010064400007650000024000000030461372733102500161210ustar 00000000000000use std::collections::HashSet; use rustyline::Editor; use rustyline::{hint::Hinter, Context}; use rustyline_derive::{Completer, Helper, Highlighter, Validator}; #[derive(Completer, Helper, Validator, Highlighter)] struct DIYHinter { // It's simple example of rustyline, for more effecient, please use ** radix trie ** hints: HashSet, } impl Hinter for DIYHinter { fn hint(&self, line: &str, pos: usize, _ctx: &Context<'_>) -> Option { if 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 pos > 0 && hint.starts_with(&line[..pos]) { Some(hint[pos..].to_owned()) } else { None } }) .next() } } fn diy_hints() -> HashSet { let mut set = HashSet::new(); set.insert(String::from("help")); set.insert(String::from("get key")); set.insert(String::from("set key value")); set.insert(String::from("hget key field")); set.insert(String::from("hset key field value")); set } fn main() -> rustyline::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-6.3.0/examples/example.rs010064400007650000024000000074041372733102500155640ustar 00000000000000use std::borrow::Cow::{self, Borrowed, Owned}; use rustyline::completion::{Completer, FilenameCompleter, Pair}; use rustyline::config::OutputStreamType; use rustyline::error::ReadlineError; use rustyline::highlight::{Highlighter, MatchingBracketHighlighter}; use rustyline::hint::{Hinter, HistoryHinter}; use rustyline::validate::{self, MatchingBracketValidator, Validator}; use rustyline::{Cmd, CompletionType, Config, Context, EditMode, Editor, KeyPress}; use rustyline_derive::Helper; #[derive(Helper)] struct MyHelper { completer: FilenameCompleter, highlighter: MatchingBracketHighlighter, validator: MatchingBracketValidator, hinter: HistoryHinter, colored_prompt: String, } impl Completer for MyHelper { type Candidate = Pair; fn complete( &self, line: &str, pos: usize, ctx: &Context<'_>, ) -> Result<(usize, Vec), ReadlineError> { self.completer.complete(line, pos, ctx) } } impl Hinter for MyHelper { fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option { self.hinter.hint(line, pos, ctx) } } impl Highlighter for MyHelper { fn highlight_prompt<'b, 's: 'b, 'p: 'b>( &'s self, prompt: &'p str, default: bool, ) -> Cow<'b, str> { if default { Borrowed(&self.colored_prompt) } else { Borrowed(prompt) } } fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { Owned("\x1b[1m".to_owned() + hint + "\x1b[m") } fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> { self.highlighter.highlight(line, pos) } fn highlight_char(&self, line: &str, pos: usize) -> bool { self.highlighter.highlight_char(line, pos) } } impl Validator for MyHelper { fn validate( &self, ctx: &mut validate::ValidationContext, ) -> rustyline::Result { self.validator.validate(ctx) } fn validate_while_typing(&self) -> bool { self.validator.validate_while_typing() } } // To debug rustyline: // RUST_LOG=rustyline=debug cargo run --example example 2> debug.log fn main() -> rustyline::Result<()> { env_logger::init(); let config = Config::builder() .history_ignore_space(true) .completion_type(CompletionType::List) .edit_mode(EditMode::Emacs) .output_stream(OutputStreamType::Stdout) .build(); let h = MyHelper { completer: FilenameCompleter::new(), highlighter: MatchingBracketHighlighter::new(), hinter: HistoryHinter {}, colored_prompt: "".to_owned(), validator: MatchingBracketValidator::new(), }; let mut rl = Editor::with_config(config); rl.set_helper(Some(h)); rl.bind_sequence(KeyPress::Meta('N'), Cmd::HistorySearchForward); rl.bind_sequence(KeyPress::Meta('P'), Cmd::HistorySearchBackward); if rl.load_history("history.txt").is_err() { println!("No previous history."); } let mut count = 1; loop { let p = format!("{}> ", count); rl.helper_mut().expect("No helper").colored_prompt = format!("\x1b[1;32m{}\x1b[0m", p); let readline = rl.readline(&p); match readline { Ok(line) => { rl.add_history_entry(line.as_str()); println!("Line: {}", line); } Err(ReadlineError::Interrupted) => { println!("CTRL-C"); break; } Err(ReadlineError::Eof) => { println!("CTRL-D"); break; } Err(err) => { println!("Error: {:?}", err); break; } } count += 1; } rl.save_history("history.txt") } rustyline-6.3.0/examples/input_validation.rs010064400007650000024000000017231365774656400175240ustar 00000000000000use rustyline::error::ReadlineError; use rustyline::validate::{ValidationContext, ValidationResult, Validator}; use rustyline::Editor; use rustyline_derive::{Completer, Helper, Highlighter, Hinter}; #[derive(Completer, Helper, Highlighter, Hinter)] struct InputValidator {} impl Validator for InputValidator { fn validate(&self, ctx: &mut ValidationContext) -> Result { 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() -> rustyline::Result<()> { let h = InputValidator {}; let mut rl = Editor::new(); rl.set_helper(Some(h)); let input = rl.readline("> ")?; println!("Input: {}", input); Ok(()) } rustyline-6.3.0/examples/read_password.rs010064400007650000024000000024511360441142000167530ustar 00000000000000use std::borrow::Cow::{self, Borrowed, Owned}; use rustyline::config::Configurer; use rustyline::highlight::Highlighter; use rustyline::{ColorMode, Editor}; use rustyline_derive::{Completer, Helper, Hinter, Validator}; #[derive(Completer, Helper, Hinter, Validator)] struct MaskingHighlighter { masking: bool, } impl Highlighter for MaskingHighlighter { fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> { use unicode_width::UnicodeWidthStr; if self.masking { Owned("*".repeat(line.width())) } else { Borrowed(line) } } fn highlight_char(&self, _line: &str, _pos: usize) -> bool { self.masking } } fn main() -> rustyline::Result<()> { println!("This is just a hack. Reading passwords securely requires more than that."); let h = MaskingHighlighter { masking: false }; let mut rl = Editor::new(); rl.set_helper(Some(h)); let username = rl.readline("Username:")?; println!("Username: {}", username); rl.helper_mut().expect("No helper").masking = true; rl.set_color_mode(ColorMode::Forced); // force masking rl.set_auto_add_history(false); // make sure password is not added to history let passwd = rl.readline("Password:")?; println!("Secret: {}", passwd); Ok(()) } rustyline-6.3.0/rustfmt.toml010064400007650000024000000005161365774656400143670ustar 00000000000000wrap_comments = true format_strings = true error_on_unformatted = false reorder_impl_items = true condense_wildcard_suffixes = true format_code_in_doc_comments = true format_macro_matchers = true format_macro_bodies = true #merge_imports = true normalize_doc_attributes = true use_field_init_shorthand = true use_try_shorthand = truerustyline-6.3.0/src/completion.rs010064400007650000024000000447711372023615700152660ustar 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}; use memchr::memchr; // TODO: let the implementers choose/find word boundaries ??? // (line, pos) is like (rl_line_buffer, rl_point) to make contextual completion // ("select t.na| from tbl as t") // TODO: make &self &mut self ??? /// A completion candidate. pub trait Candidate { /// Text to display when listing alternatives. fn display(&self) -> &str; /// Text to insert in line. fn replacement(&self) -> &str; } impl Candidate for String { fn display(&self) -> &str { self.as_str() } fn replacement(&self) -> &str { self.as_str() } } /// Completion candidate pair 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() } } /// To be called for tab-completion. pub trait Completer { /// Specific completion candidate. type Candidate: Candidate; /// Takes the currently edited `line` with the cursor `pos`ition and /// returns the start position and the completion candidates for the /// partial word to be completed. /// /// ("ls /usr/loc", 11) => Ok((3, vec!["/usr/local/"])) fn complete( &self, line: &str, pos: usize, ctx: &Context<'_>, ) -> Result<(usize, Vec)> { let _ = (line, pos, ctx); Ok((0, Vec::with_capacity(0))) } /// Updates the edited `line` with the `elected` candidate. fn update(&self, line: &mut LineBuffer, start: usize, elected: &str) { let end = line.pos(); line.replace(start..end, elected) } } impl Completer for () { type Candidate = String; fn update(&self, _line: &mut LineBuffer, _start: usize, _elected: &str) { unreachable!() } } impl<'c, C: ?Sized + Completer> Completer for &'c C { type Candidate = C::Candidate; fn complete( &self, line: &str, pos: usize, ctx: &Context<'_>, ) -> Result<(usize, Vec)> { (**self).complete(line, pos, ctx) } fn update(&self, line: &mut LineBuffer, start: usize, elected: &str) { (**self).update(line, start, elected) } } macro_rules! box_completer { ($($id: ident)*) => { $( impl Completer for $id { type Candidate = C::Candidate; fn complete(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Result<(usize, Vec)> { (**self).complete(line, pos, ctx) } fn update(&self, line: &mut LineBuffer, start: usize, elected: &str) { (**self).update(line, start, elected) } } )* } } use std::rc::Rc; use std::sync::Arc; box_completer! { Box Rc Arc } /// A `Completer` for file and folder names. pub struct FilenameCompleter { break_chars: &'static [u8], double_quotes_special_chars: &'static [u8], } const DOUBLE_QUOTES_ESCAPE_CHAR: Option = Some('\\'); cfg_if::cfg_if! { if #[cfg(unix)] { // rl_basic_word_break_characters, rl_completer_word_break_characters const DEFAULT_BREAK_CHARS: [u8; 18] = [ b' ', b'\t', b'\n', b'"', b'\\', b'\'', b'`', b'@', b'$', b'>', b'<', b'=', b';', b'|', b'&', b'{', b'(', b'\0', ]; const ESCAPE_CHAR: Option = Some('\\'); // In double quotes, not all break_chars need to be escaped // https://www.gnu.org/software/bash/manual/html_node/Double-Quotes.html const DOUBLE_QUOTES_SPECIAL_CHARS: [u8; 4] = [b'"', b'$', b'\\', b'`']; } else if #[cfg(windows)] { // Remove \ to make file completion works on windows const DEFAULT_BREAK_CHARS: [u8; 17] = [ b' ', b'\t', b'\n', b'"', b'\'', b'`', b'@', b'$', b'>', b'<', b'=', b';', b'|', b'&', b'{', b'(', b'\0', ]; const ESCAPE_CHAR: Option = None; const DOUBLE_QUOTES_SPECIAL_CHARS: [u8; 1] = [b'"']; // TODO Validate: only '"' ? } else if #[cfg(target_arch = "wasm32")] { const DEFAULT_BREAK_CHARS: [u8; 0] = []; const ESCAPE_CHAR: Option = None; const DOUBLE_QUOTES_SPECIAL_CHARS: [u8; 0] = []; } } /// Kind of quote. #[derive(Clone, Copy, Debug, PartialEq)] pub enum Quote { /// Double quote: `"` Double, /// Single quote: `'` Single, /// No quote None, } impl FilenameCompleter { /// Constructor pub fn new() -> Self { Self { break_chars: &DEFAULT_BREAK_CHARS, double_quotes_special_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, 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 mut matches = filename_complete(&path, esc_char, break_chars, quote)?; #[allow(clippy::unnecessary_sort_by)] matches.sort_by(|a, b| a.display().cmp(b.display())); Ok((start, matches)) } } impl Default for FilenameCompleter { fn default() -> Self { Self::new() } } impl Completer for FilenameCompleter { type Candidate = Pair; fn complete(&self, line: &str, pos: usize, _ctx: &Context<'_>) -> Result<(usize, Vec)> { self.complete_path(line, pos) } } /// Remove escape char pub fn unescape(input: &str, esc_char: Option) -> Cow<'_, str> { let esc_char = if let Some(c) = esc_char { c } else { return Borrowed(input); }; if !input.chars().any(|c| c == esc_char) { return Borrowed(input); } let mut result = String::with_capacity(input.len()); let mut chars = input.chars(); while let Some(ch) = chars.next() { if ch == esc_char { if let Some(ch) = chars.next() { if cfg!(windows) && ch != '"' { // TODO Validate: only '"' ? result.push(esc_char); } result.push(ch); } else if cfg!(windows) { result.push(ch); } } else { result.push(ch); } } Owned(result) } /// Escape any `break_chars` in `input` string with `esc_char`. /// For example, '/User Information' becomes '/User\ Information' /// when space is a breaking char and '\\' the escape char. pub fn escape( mut input: String, esc_char: Option, break_chars: &[u8], quote: Quote, ) -> String { if quote == Quote::Single { return input; // no escape in single quotes } let n = input .bytes() .filter(|b| memchr(*b, break_chars).is_some()) .count(); if n == 0 { return input; // no need to escape } let esc_char = if let Some(c) = esc_char { c } else { if cfg!(windows) && quote == Quote::None { input.insert(0, '"'); // force double quote return input; } return input; }; let mut result = String::with_capacity(input.len() + n); for c in input.chars() { if c.is_ascii() && memchr(c as u8, break_chars).is_some() { result.push(esc_char); } result.push(c); } result } fn filename_complete( path: &str, esc_char: Option, break_chars: &[u8], quote: Quote, ) -> Result> { #[cfg(feature = "with-dirs")] use dirs_next::home_dir; use std::env::current_dir; let sep = path::MAIN_SEPARATOR; let (dir_name, file_name) = match path.rfind(sep) { Some(idx) => path.split_at(idx + sep.len_utf8()), None => ("", path), }; let dir_path = Path::new(dir_name); let dir = if dir_path.starts_with("~") { // ~[/...] #[cfg(feature = "with-dirs")] { if let Some(home) = home_dir() { match dir_path.strip_prefix("~") { Ok(rel_path) => home.join(rel_path), _ => home, } } else { dir_path.to_path_buf() } } #[cfg(not(feature = "with-dirs"))] { dir_path.to_path_buf() } } else if dir_path.is_relative() { // TODO ~user[/...] (https://crates.io/crates/users) if let Ok(cwd) = current_dir() { cwd.join(dir_path) } else { dir_path.to_path_buf() } } else { dir_path.to_path_buf() }; let mut entries: Vec = Vec::new(); // if dir doesn't exist, then don't offer any completions if !dir.exists() { return Ok(entries); } // if any of the below IO operations have errors, just ignore them if let Ok(read_dir) = dir.read_dir() { let file_name = normalize(file_name); for entry in read_dir { if let Ok(entry) = entry { if let Some(s) = entry.file_name().to_str() { let ns = normalize(s); if ns.starts_with(file_name.as_ref()) { if let Ok(metadata) = fs::metadata(entry.path()) { let mut path = String::from(dir_name) + s; if metadata.is_dir() { path.push(sep); } entries.push(Pair { display: String::from(s), replacement: escape(path, esc_char, break_chars, quote), }); } // else ignore PermissionDenied } } } } } Ok(entries) } #[cfg(any(windows, target_os = "macos"))] fn normalize(s: &str) -> Cow { // case insensitive Cow::Owned(s.to_lowercase()) } #[cfg(not(any(windows, target_os = "macos")))] fn normalize(s: &str) -> Cow { Cow::Borrowed(s) } /// Given a `line` and a cursor `pos`ition, /// try to find backward the start of a word. /// Return (0, `line[..pos]`) if no break char has been found. /// Return the word and its start position (idx, `line[idx..pos]`) otherwise. pub fn extract_word<'l>( line: &'l str, pos: usize, esc_char: Option, break_chars: &[u8], ) -> (usize, &'l str) { let line = &line[..pos]; if line.is_empty() { return (0, line); } let mut start = None; for (i, c) in line.char_indices().rev() { if let (Some(esc_char), true) = (esc_char, start.is_some()) { if esc_char == c { // escaped break char start = None; continue; } else { break; } } if c.is_ascii() && memchr(c as u8, break_chars).is_some() { start = Some(i + c.len_utf8()); if esc_char.is_none() { break; } // else maybe escaped... } } match start { Some(start) => (start, &line[start..]), None => (0, line), } } /// 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(PartialEq)] enum ScanMode { DoubleQuote, Escape, EscapeInDoubleQuote, Normal, SingleQuote, } /// try to find an unclosed single/double quote in `s`. /// Return `None` if no unclosed quote is found. /// Return the unclosed quote position and if it is a double quote. fn find_unclosed_quote(s: &str) -> Option<(usize, Quote)> { let char_indices = s.char_indices(); let mut mode = ScanMode::Normal; let mut quote_index = 0; for (index, char) in char_indices { match mode { ScanMode::DoubleQuote => { if char == '"' { mode = ScanMode::Normal; } else if char == '\\' { // both windows and unix support escape in double quote mode = ScanMode::EscapeInDoubleQuote; } } ScanMode::Escape => { mode = ScanMode::Normal; } ScanMode::EscapeInDoubleQuote => { mode = ScanMode::DoubleQuote; } ScanMode::Normal => { if char == '"' { mode = ScanMode::DoubleQuote; quote_index = index; } else if char == '\\' && cfg!(not(windows)) { mode = ScanMode::Escape; } else if char == '\'' && cfg!(not(windows)) { mode = ScanMode::SingleQuote; quote_index = index; } } ScanMode::SingleQuote => { if char == '\'' { mode = ScanMode::Normal; } // no escape in single quotes } }; } if ScanMode::DoubleQuote == mode || ScanMode::EscapeInDoubleQuote == mode { return Some((quote_index, Quote::Double)); } else if ScanMode::SingleQuote == mode { return Some((quote_index, Quote::Single)); } None } #[cfg(test)] mod tests { #[test] pub fn extract_word() { let break_chars: &[u8] = &super::DEFAULT_BREAK_CHARS; let line = "ls '/usr/local/b"; assert_eq!( (4, "/usr/local/b"), super::extract_word(line, line.len(), Some('\\'), &break_chars) ); let line = "ls /User\\ Information"; assert_eq!( (3, "/User\\ Information"), super::extract_word(line, line.len(), Some('\\'), &break_chars) ); } #[test] pub fn unescape() { use std::borrow::Cow::{self, Borrowed, Owned}; let input = "/usr/local/b"; assert_eq!(Borrowed(input), super::unescape(input, Some('\\'))); if cfg!(windows) { let input = "c:\\users\\All Users\\"; let result: Cow<'_, str> = Borrowed(input); assert_eq!(result, super::unescape(input, Some('\\'))); } else { let input = "/User\\ Information"; let result: Cow<'_, str> = Owned(String::from("/User Information")); assert_eq!(result, super::unescape(input, Some('\\'))); } } #[test] pub fn escape() { let break_chars: &[u8] = &super::DEFAULT_BREAK_CHARS; let input = String::from("/usr/local/b"); assert_eq!( input.clone(), super::escape(input, Some('\\'), &break_chars, super::Quote::None) ); let input = String::from("/User Information"); let result = String::from("/User\\ Information"); assert_eq!( result, super::escape(input, Some('\\'), &break_chars, super::Quote::None) ); } #[test] pub fn longest_common_prefix() { let mut candidates = vec![]; { let lcp = super::longest_common_prefix(&candidates); assert!(lcp.is_none()); } let s = "User"; let c1 = String::from(s); candidates.push(c1); { 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-6.3.0/src/config.rs010064400007650000024000000316461366177653000143670ustar 00000000000000//! Customize line editor use std::default::Default; /// User preferences #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Config { /// Maximum number of entries in History. max_history_size: usize, // history_max_entries history_duplicates: HistoryDuplicates, history_ignore_space: bool, completion_type: CompletionType, /// When listing completion alternatives, only display /// one screen of possibilities at a time. completion_prompt_limit: usize, /// Duration (milliseconds) Rustyline will wait for a character when /// reading an ambiguous key sequence. keyseq_timeout: i32, /// Emacs or Vi mode edit_mode: EditMode, /// If true, each nonblank line returned by `readline` will be /// automatically added to the history. auto_add_history: bool, /// Beep or Flash or nothing bell_style: BellStyle, /// if colors should be enabled. color_mode: ColorMode, /// Whether to use stdout or stderr output_stream: OutputStreamType, /// Horizontal space taken by a tab. tab_stop: usize, } impl Config { /// Returns a `Config` builder. pub fn builder() -> Builder { Builder::new() } /// Tell the maximum length (i.e. number of entries) for the history. pub fn max_history_size(&self) -> usize { self.max_history_size } pub(crate) fn set_max_history_size(&mut self, max_size: usize) { self.max_history_size = max_size; } /// Tell if lines which match the previous history entry are saved or not /// in the history list. /// /// By default, they are ignored. pub fn history_duplicates(&self) -> HistoryDuplicates { self.history_duplicates } pub(crate) fn set_history_ignore_dups(&mut self, yes: bool) { self.history_duplicates = if yes { HistoryDuplicates::IgnoreConsecutive } else { HistoryDuplicates::AlwaysAdd }; } /// Tell if lines which begin with a space character are saved or not in /// the history list. /// /// By default, they are saved. pub fn history_ignore_space(&self) -> bool { self.history_ignore_space } pub(crate) fn set_history_ignore_space(&mut self, yes: bool) { self.history_ignore_space = yes; } /// Completion behaviour. /// /// By default, `CompletionType::Circular`. pub fn completion_type(&self) -> CompletionType { self.completion_type } /// When listing completion alternatives, only display /// one screen of possibilities at a time (used for `CompletionType::List` /// mode). pub fn completion_prompt_limit(&self) -> usize { self.completion_prompt_limit } /// Duration (milliseconds) Rustyline will wait for a character when /// reading an ambiguous key sequence (used for `EditMode::Vi` mode on unix /// platform). /// /// By default, no timeout (-1) or 500ms if `EditMode::Vi` is activated. pub fn keyseq_timeout(&self) -> i32 { self.keyseq_timeout } /// Emacs or Vi mode pub fn edit_mode(&self) -> EditMode { self.edit_mode } /// Tell if lines are automatically added to the history. /// /// By default, they are not. pub fn auto_add_history(&self) -> bool { self.auto_add_history } /// Bell style: beep, flash or nothing. pub fn bell_style(&self) -> BellStyle { self.bell_style } /// Tell if colors should be enabled. /// /// By default, they are except if stdout is not a TTY. pub fn color_mode(&self) -> ColorMode { self.color_mode } pub(crate) fn set_color_mode(&mut self, color_mode: ColorMode) { self.color_mode = color_mode; } /// Tell which output stream should be used: stdout or stderr. /// /// By default, stdout is used. pub fn output_stream(&self) -> OutputStreamType { self.output_stream } pub(crate) fn set_output_stream(&mut self, stream: OutputStreamType) { self.output_stream = stream; } /// Horizontal space taken by a tab. /// /// By default, 8. pub fn tab_stop(&self) -> usize { self.tab_stop } pub(crate) fn set_tab_stop(&mut self, tab_stop: usize) { self.tab_stop = tab_stop; } } impl Default for Config { fn default() -> Self { Self { max_history_size: 100, history_duplicates: HistoryDuplicates::IgnoreConsecutive, history_ignore_space: false, completion_type: CompletionType::Circular, // TODO Validate completion_prompt_limit: 100, keyseq_timeout: -1, edit_mode: EditMode::Emacs, auto_add_history: false, bell_style: BellStyle::default(), color_mode: ColorMode::Enabled, output_stream: OutputStreamType::Stdout, tab_stop: 8, } } } /// Beep or flash or nothing #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum BellStyle { /// Beep Audible, /// Silent None, /// Flash screen (not supported) Visible, } /// `Audible` by default on unix (overriden by current Terminal settings). /// `None` on windows. impl Default for BellStyle { #[cfg(any(windows, target_arch = "wasm32"))] fn default() -> Self { BellStyle::None } #[cfg(unix)] fn default() -> Self { BellStyle::Audible } } /// 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 stdout or stderr // TODO console term::TermTarget #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum OutputStreamType { /// Use stderr Stderr, /// Use stdout Stdout, } /// Configuration builder #[derive(Clone, Debug, Default)] pub struct Builder { p: Config, } impl Builder { /// Returns a `Config` builder. pub fn new() -> Self { Self { p: Config::default(), } } /// Set the maximum length for the history. pub fn max_history_size(mut self, max_size: usize) -> Self { self.set_max_history_size(max_size); self } /// Tell if lines which match the previous history entry are saved or not /// in the history list. /// /// By default, they are ignored. pub fn history_ignore_dups(mut self, yes: bool) -> Self { self.set_history_ignore_dups(yes); self } /// Tell if lines which begin with a space character are saved or not in /// the history list. /// /// By default, they are saved. pub fn history_ignore_space(mut self, yes: bool) -> Self { self.set_history_ignore_space(yes); self } /// Set `completion_type`. pub fn completion_type(mut self, completion_type: CompletionType) -> Self { self.set_completion_type(completion_type); self } /// The number of possible completions that determines when the user is /// asked whether the list of possibilities should be displayed. pub fn completion_prompt_limit(mut self, completion_prompt_limit: usize) -> Self { self.set_completion_prompt_limit(completion_prompt_limit); self } /// Timeout for ambiguous key sequences in milliseconds. /// Currently, it is used only to distinguish a single ESC from an ESC /// sequence. /// After seeing an ESC key, wait at most `keyseq_timeout_ms` for another /// byte. pub fn keyseq_timeout(mut self, keyseq_timeout_ms: i32) -> Self { self.set_keyseq_timeout(keyseq_timeout_ms); self } /// Choose between Emacs or Vi mode. pub fn edit_mode(mut self, edit_mode: EditMode) -> Self { self.set_edit_mode(edit_mode); self } /// Tell if lines are automatically added to the history. /// /// By default, they are not. pub fn auto_add_history(mut self, yes: bool) -> Self { self.set_auto_add_history(yes); self } /// Set bell style: beep, flash or nothing. pub fn bell_style(mut self, bell_style: BellStyle) -> Self { self.set_bell_style(bell_style); self } /// Forces colorization on or off. /// /// By default, colorization is on except if stdout is not a TTY. pub fn color_mode(mut self, color_mode: ColorMode) -> Self { self.set_color_mode(color_mode); self } /// Whether to use stdout or stderr. /// /// Be default, use stdout pub fn output_stream(mut self, stream: OutputStreamType) -> Self { self.set_output_stream(stream); self } /// Horizontal space taken by a tab. /// /// By default, `8` pub fn tab_stop(mut self, tab_stop: usize) -> Self { self.set_tab_stop(tab_stop); self } /// Builds a `Config` with the settings specified so far. 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) { self.config_mut().set_max_history_size(max_size); } /// Tell if lines which match the previous history entry are saved or not /// in the history list. /// /// By default, they are ignored. fn set_history_ignore_dups(&mut self, yes: bool) { self.config_mut().set_history_ignore_dups(yes); } /// Tell if lines which begin with a space character are saved or not in /// the history list. /// /// By default, they are saved. fn set_history_ignore_space(&mut self, yes: bool) { self.config_mut().set_history_ignore_space(yes); } /// Set `completion_type`. fn set_completion_type(&mut self, completion_type: CompletionType) { self.config_mut().completion_type = completion_type; } /// The number of possible completions that determines when the user is /// asked whether the list of possibilities should be displayed. fn set_completion_prompt_limit(&mut self, completion_prompt_limit: usize) { self.config_mut().completion_prompt_limit = completion_prompt_limit; } /// Timeout for ambiguous key sequences in milliseconds. fn set_keyseq_timeout(&mut self, keyseq_timeout_ms: i32) { self.config_mut().keyseq_timeout = keyseq_timeout_ms; } /// Choose between Emacs or Vi mode. fn set_edit_mode(&mut self, edit_mode: EditMode) { self.config_mut().edit_mode = edit_mode; match edit_mode { EditMode::Emacs => self.set_keyseq_timeout(-1), // no timeout EditMode::Vi => self.set_keyseq_timeout(500), } } /// Tell if lines are automatically added to the history. /// /// By default, they are not. fn set_auto_add_history(&mut self, yes: bool) { self.config_mut().auto_add_history = yes; } /// Set bell style: beep, flash or nothing. fn set_bell_style(&mut self, bell_style: BellStyle) { self.config_mut().bell_style = bell_style; } /// Forces colorization on or off. /// /// By default, colorization is on except if stdout is not a TTY. fn set_color_mode(&mut self, color_mode: ColorMode) { self.config_mut().set_color_mode(color_mode); } /// Whether to use stdout or stderr /// /// By default, use stdout fn set_output_stream(&mut self, stream: OutputStreamType) { self.config_mut().set_output_stream(stream); } /// Horizontal space taken by a tab. /// /// By default, `8` fn set_tab_stop(&mut self, tab_stop: usize) { self.config_mut().set_tab_stop(tab_stop); } } rustyline-6.3.0/src/edit.rs010064400007650000024000000545651372733102500140410ustar 00000000000000//! Command processor use log::debug; use std::cell::RefCell; use std::fmt; use std::rc::Rc; use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthChar; use super::{Context, Helper, Result}; use crate::highlight::Highlighter; use crate::history::Direction; use crate::keymap::{Anchor, At, CharSearch, Cmd, Movement, RepeatCount, Word}; use crate::keymap::{InputState, Invoke, Refresher}; use crate::layout::{Layout, Position}; use crate::line_buffer::{LineBuffer, WordAction, MAX_LINE}; use crate::tty::{Renderer, Term, Terminal}; use crate::undo::Changeset; use crate::validate::{ValidationContext, ValidationResult}; /// Represent the state during line editing. /// Implement rendering. pub struct State<'out, 'prompt, H: Helper> { pub out: &'out mut ::Writer, prompt: &'prompt str, // Prompt to display (rl_prompt) prompt_size: Position, // Prompt Unicode/visible width and height pub line: LineBuffer, // Edited line buffer pub layout: Layout, saved_line_for_history: LineBuffer, // Current edited line before history browsing byte_buffer: [u8; 4], pub changes: Rc>, // changes to line, for undo/redo pub helper: Option<&'out H>, pub ctx: Context<'out>, // Give access to history for `hinter` pub hint: Option, // last hint displayed highlight_char: bool, // `true` if a char has been highlighted } enum Info<'m> { NoHint, Hint, Msg(Option<&'m str>), } impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { pub fn new( out: &'out mut ::Writer, prompt: &'prompt str, helper: Option<&'out H>, ctx: Context<'out>, ) -> State<'out, 'prompt, H> { let prompt_size = out.calculate_position(prompt, Position::default()); State { out, prompt, prompt_size, line: LineBuffer::with_capacity(MAX_LINE).can_growth(true), layout: Layout::default(), saved_line_for_history: LineBuffer::with_capacity(MAX_LINE).can_growth(true), byte_buffer: [0; 4], changes: Rc::new(RefCell::new(Changeset::new())), helper, ctx, hint: None, highlight_char: false, } } pub fn highlighter(&self) -> Option<&dyn Highlighter> { if self.out.colors_enabled() { self.helper.map(|h| h as &dyn Highlighter) } else { None } } pub fn next_cmd( &mut self, input_state: &mut InputState, rdr: &mut ::Reader, single_esc_abort: bool, ) -> Result { loop { let rc = input_state.next_cmd(rdr, self, single_esc_abort); if rc.is_err() && self.out.sigwinch() { self.out.update_size(); self.prompt_size = self .out .calculate_position(self.prompt, Position::default()); self.refresh_line()?; continue; } if let Ok(Cmd::Replace(..)) = rc { self.changes.borrow_mut().begin(); } return rc; } } pub fn backup(&mut self) { self.saved_line_for_history .update(self.line.as_str(), self.line.pos()); } pub fn restore(&mut self) { self.line.update( self.saved_line_for_history.as_str(), self.saved_line_for_history.pos(), ); } pub fn move_cursor(&mut self) -> Result<()> { // calculate the desired position of the cursor let cursor = self .out .calculate_position(&self.line[..self.line.pos()], self.prompt_size); if self.layout.cursor == cursor { return Ok(()); } if self.highlight_char() { let prompt_size = self.prompt_size; self.refresh(self.prompt, prompt_size, true, Info::NoHint)?; } else { self.out.move_cursor(self.layout.cursor, cursor)?; self.layout.prompt_size = self.prompt_size; self.layout.cursor = cursor; debug_assert!(self.layout.prompt_size <= self.layout.cursor); debug_assert!(self.layout.cursor <= self.layout.end); } Ok(()) } pub fn move_cursor_at_leftmost(&mut self, rdr: &mut ::Reader) -> Result<()> { self.out.move_cursor_at_leftmost(rdr) } fn refresh( &mut self, prompt: &str, prompt_size: Position, default_prompt: bool, info: Info<'_>, ) -> Result<()> { let info = match info { Info::NoHint => None, Info::Hint => self.hint.as_deref(), 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 = hint; } else { self.hint = None } } fn highlight_char(&mut self) -> bool { if let Some(highlighter) = self.highlighter() { let highlight_char = highlighter.highlight_char(&self.line, self.line.pos()); if highlight_char { self.highlight_char = true; true } else if self.highlight_char { // previously highlighted => force a full refresh self.highlight_char = false; true } else { false } } else { false } } pub fn is_default_prompt(&self) -> bool { self.layout.default_prompt } pub fn validate(&mut self) -> Result { if let Some(validator) = self.helper { self.changes.borrow_mut().begin(); let result = validator.validate(&mut ValidationContext::new(self))?; let corrected = self.changes.borrow_mut().end(); let validated = match result { ValidationResult::Incomplete => false, ValidationResult::Valid(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)?; } true } ValidationResult::Invalid(msg) => { if corrected || self.has_hint() || msg.is_some() { self.refresh_line_with_msg(msg)?; } false } }; Ok(validated) } else { Ok(true) } } } impl<'out, 'prompt, H: Helper> Invoke for State<'out, 'prompt, H> { fn input(&self) -> &str { self.line.as_str() } } impl<'out, 'prompt, H: Helper> Refresher for State<'out, 'prompt, H> { fn refresh_line(&mut self) -> Result<()> { let prompt_size = self.prompt_size; self.hint(); self.highlight_char(); self.refresh(self.prompt, prompt_size, true, Info::Hint) } fn refresh_line_with_msg(&mut self, msg: Option) -> Result<()> { let prompt_size = self.prompt_size; self.hint = None; self.highlight_char(); self.refresh(self.prompt, prompt_size, true, Info::Msg(msg.as_deref())) } fn refresh_prompt_and_line(&mut self, prompt: &str) -> Result<()> { let prompt_size = self.out.calculate_position(prompt, Position::default()); self.hint(); self.highlight_char(); self.refresh(prompt, prompt_size, false, Info::Hint) } fn doing_insert(&mut self) { self.changes.borrow_mut().begin(); } fn done_inserting(&mut self) { self.changes.borrow_mut().end(); } fn last_insert(&self) -> Option { self.changes.borrow().last_insert() } fn is_cursor_at_end(&self) -> bool { self.line.pos() == self.line.len() } fn has_hint(&self) -> bool { self.hint.is_some() } } impl<'out, 'prompt, H: Helper> fmt::Debug for State<'out, 'prompt, H> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("State") .field("prompt", &self.prompt) .field("prompt_size", &self.prompt_size) .field("buf", &self.line) .field("cols", &self.out.get_columns()) .field("layout", &self.layout) .field("saved_line_for_history", &self.saved_line_for_history) .finish() } } impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { pub fn clear_screen(&mut self) -> Result<()> { self.out.clear_screen()?; self.layout.cursor = Position::default(); self.layout.end = Position::default(); Ok(()) } /// Insert the character `ch` at cursor current position. pub fn edit_insert(&mut self, ch: char, n: RepeatCount) -> Result<()> { if let Some(push) = self.line.insert(ch, n) { if push { let prompt_size = self.prompt_size; let no_previous_hint = self.hint.is_none(); self.hint(); let width = ch.width().unwrap_or(0); if n == 1 && width != 0 // Ctrl-V + \t or \n ... && self.layout.cursor.col + width < self.out.get_columns() && (self.hint.is_none() && no_previous_hint) // TODO refresh only current line && !self.highlight_char() { // Avoid a full update of the line in the trivial case. self.layout.cursor.col += width; self.layout.end.col += width; debug_assert!(self.layout.prompt_size <= self.layout.cursor); debug_assert!(self.layout.cursor <= self.layout.end); let bits = ch.encode_utf8(&mut self.byte_buffer); let bits = bits.as_bytes(); self.out.write_and_flush(bits) } else { self.refresh(self.prompt, prompt_size, true, Info::Hint) } } else { self.refresh_line() } } else { Ok(()) } } /// Replace a single (or n) character(s) under the cursor (Vi mode) pub fn edit_replace_char(&mut self, ch: char, n: RepeatCount) -> Result<()> { self.changes.borrow_mut().begin(); let succeed = if let Some(chars) = self.line.delete(n) { let count = chars.graphemes(true).count(); self.line.insert(ch, count); self.line.move_backward(1); true } else { false }; self.changes.borrow_mut().end(); if succeed { self.refresh_line() } else { Ok(()) } } /// Overwrite the character under the cursor (Vi mode) pub fn edit_overwrite_char(&mut self, ch: char) -> Result<()> { if let Some(end) = self.line.next_pos(1) { { let text = ch.encode_utf8(&mut self.byte_buffer); let start = self.line.pos(); self.line.replace(start..end, text); } self.refresh_line() } else { Ok(()) } } // Yank/paste `text` at current position. pub fn edit_yank( &mut self, input_state: &InputState, text: &str, anchor: Anchor, n: RepeatCount, ) -> Result<()> { if let Anchor::After = anchor { self.line.move_forward(1); } if self.line.yank(text, n).is_some() { if !input_state.is_emacs_mode() { self.line.move_backward(1); } self.refresh_line() } else { Ok(()) } } // Delete previously yanked text and yank/paste `text` at current position. pub fn edit_yank_pop(&mut self, yank_size: usize, text: &str) -> Result<()> { self.changes.borrow_mut().begin(); let result = if self.line.yank_pop(yank_size, text).is_some() { self.refresh_line() } else { Ok(()) }; self.changes.borrow_mut().end(); result } /// Move cursor on the left. pub fn edit_move_backward(&mut self, n: RepeatCount) -> Result<()> { if self.line.move_backward(n) { self.move_cursor() } else { Ok(()) } } /// Move cursor on the right. pub fn edit_move_forward(&mut self, n: RepeatCount) -> Result<()> { if self.line.move_forward(n) { self.move_cursor() } else { Ok(()) } } /// Move cursor to the start of the line. pub fn edit_move_home(&mut self) -> Result<()> { if self.line.move_home() { self.move_cursor() } else { Ok(()) } } /// Move cursor to the end of the line. pub fn edit_move_end(&mut self) -> Result<()> { if self.line.move_end() { self.move_cursor() } else { Ok(()) } } /// Move cursor to the start of the buffer. pub fn edit_move_buffer_start(&mut self) -> Result<()> { if self.line.move_buffer_start() { self.move_cursor() } else { Ok(()) } } /// Move cursor to the end of the buffer. pub fn edit_move_buffer_end(&mut self) -> Result<()> { if self.line.move_buffer_end() { self.move_cursor() } else { Ok(()) } } pub fn edit_kill(&mut self, mvt: &Movement) -> Result<()> { if self.line.kill(mvt) { self.refresh_line() } else { Ok(()) } } pub fn edit_insert_text(&mut self, text: &str) -> Result<()> { if text.is_empty() { return Ok(()); } let cursor = self.line.pos(); self.line.insert_str(cursor, text); self.refresh_line() } pub fn edit_delete(&mut self, n: RepeatCount) -> Result<()> { if self.line.delete(n).is_some() { self.refresh_line() } else { Ok(()) } } /// Exchange the char before cursor with the character at cursor. pub fn edit_transpose_chars(&mut self) -> Result<()> { self.changes.borrow_mut().begin(); let succeed = self.line.transpose_chars(); self.changes.borrow_mut().end(); if succeed { self.refresh_line() } else { Ok(()) } } pub fn edit_move_to_prev_word(&mut self, word_def: Word, n: RepeatCount) -> Result<()> { if self.line.move_to_prev_word(word_def, n) { self.move_cursor() } else { Ok(()) } } pub fn edit_move_to_next_word(&mut self, at: At, word_def: Word, n: RepeatCount) -> Result<()> { if self.line.move_to_next_word(at, word_def, n) { self.move_cursor() } else { Ok(()) } } /// 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.borrow_mut().begin(); let succeed = self.line.edit_word(a); self.changes.borrow_mut().end(); if succeed { self.refresh_line() } else { Ok(()) } } pub fn edit_transpose_words(&mut self, n: RepeatCount) -> Result<()> { self.changes.borrow_mut().begin(); let succeed = self.line.transpose_words(n); self.changes.borrow_mut().end(); if succeed { self.refresh_line() } else { Ok(()) } } /// Substitute the currently edited line with the next or previous history /// entry. pub fn edit_history_next(&mut self, prev: bool) -> Result<()> { let history = self.ctx.history; if history.is_empty() { return Ok(()); } if self.ctx.history_index == history.len() { if prev { // Save the current edited line before overwriting it self.backup(); } else { return Ok(()); } } else if self.ctx.history_index == 0 && prev { return Ok(()); } if prev { self.ctx.history_index -= 1; } else { self.ctx.history_index += 1; } if self.ctx.history_index < history.len() { let buf = history.get(self.ctx.history_index).unwrap(); self.changes.borrow_mut().begin(); self.line.update(buf, buf.len()); self.changes.borrow_mut().end(); } else { // Restore current edited line self.restore(); } self.refresh_line() } // Non-incremental, anchored search pub fn edit_history_search(&mut self, dir: Direction) -> Result<()> { let history = self.ctx.history; if history.is_empty() { return self.out.beep(); } if self.ctx.history_index == history.len() && dir == Direction::Forward || self.ctx.history_index == 0 && dir == Direction::Reverse { return self.out.beep(); } if dir == Direction::Reverse { self.ctx.history_index -= 1; } else { self.ctx.history_index += 1; } if let Some(history_index) = history.starts_with( &self.line.as_str()[..self.line.pos()], self.ctx.history_index, dir, ) { self.ctx.history_index = history_index; let buf = history.get(history_index).unwrap(); self.changes.borrow_mut().begin(); self.line.update(buf, buf.len()); self.changes.borrow_mut().end(); self.refresh_line() } else { self.out.beep() } } /// Substitute the currently edited line with the first/last history entry. pub fn edit_history(&mut self, first: bool) -> Result<()> { let history = self.ctx.history; if history.is_empty() { return Ok(()); } if self.ctx.history_index == history.len() { if first { // Save the current edited line before overwriting it self.backup(); } else { return Ok(()); } } else if self.ctx.history_index == 0 && first { return Ok(()); } if first { self.ctx.history_index = 0; let buf = history.get(self.ctx.history_index).unwrap(); self.changes.borrow_mut().begin(); self.line.update(buf, buf.len()); self.changes.borrow_mut().end(); } else { self.ctx.history_index = history.len(); // Restore current edited line self.restore(); } self.refresh_line() } } #[cfg(test)] pub fn init_state<'out, H: Helper>( out: &'out mut ::Writer, line: &str, pos: usize, helper: Option<&'out H>, history: &'out crate::history::History, ) -> State<'out, 'static, H> { State { out, prompt: "", prompt_size: Position::default(), line: LineBuffer::init(line, pos, None), layout: Layout::default(), saved_line_for_history: LineBuffer::with_capacity(100), byte_buffer: [0; 4], changes: Rc::new(RefCell::new(Changeset::new())), helper, ctx: Context::new(history), hint: Some("hint".to_owned()), highlight_char: false, } } #[cfg(test)] mod test { use super::init_state; use crate::history::History; use crate::tty::Sink; #[test] fn edit_history_next() { let mut out = Sink::new(); let mut history = History::new(); history.add("line0"); history.add("line1"); let line = "current edited line"; let helper: Option<()> = None; let mut s = init_state(&mut out, line, 6, helper.as_ref(), &history); s.ctx.history_index = history.len(); for _ in 0..2 { s.edit_history_next(false).unwrap(); assert_eq!(line, s.line.as_str()); } s.edit_history_next(true).unwrap(); assert_eq!(line, s.saved_line_for_history.as_str()); assert_eq!(1, s.ctx.history_index); assert_eq!("line1", s.line.as_str()); for _ in 0..2 { s.edit_history_next(true).unwrap(); assert_eq!(line, s.saved_line_for_history.as_str()); assert_eq!(0, s.ctx.history_index); assert_eq!("line0", s.line.as_str()); } s.edit_history_next(false).unwrap(); assert_eq!(line, s.saved_line_for_history.as_str()); assert_eq!(1, s.ctx.history_index); assert_eq!("line1", s.line.as_str()); s.edit_history_next(false).unwrap(); // assert_eq!(line, s.saved_line_for_history); assert_eq!(2, s.ctx.history_index); assert_eq!(line, s.line.as_str()); } } rustyline-6.3.0/src/error.rs010064400007650000024000000041101366177653000142350ustar 00000000000000//! Contains error type for handling I/O and Errno errors #[cfg(windows)] use std::char; use std::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 (Ctrl-D) Eof, /// Ctrl-C Interrupted, /// Chars Error #[cfg(unix)] Utf8Error, /// Unix Error from syscall #[cfg(unix)] Errno(nix::Error), /// Error generated on WINDOW_BUFFER_SIZE_EVENT to mimic unix SIGWINCH /// signal #[cfg(windows)] WindowResize, /// Like Utf8Error on unix #[cfg(windows)] Decode(char::DecodeUtf16Error), } impl fmt::Display for ReadlineError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { ReadlineError::Io(ref err) => err.fmt(f), ReadlineError::Eof => write!(f, "EOF"), ReadlineError::Interrupted => write!(f, "Interrupted"), #[cfg(unix)] ReadlineError::Utf8Error => write!(f, "invalid utf-8: corrupt contents"), #[cfg(unix)] ReadlineError::Errno(ref err) => err.fmt(f), #[cfg(windows)] ReadlineError::WindowResize => write!(f, "WindowResize"), #[cfg(windows)] ReadlineError::Decode(ref err) => err.fmt(f), } } } impl error::Error for ReadlineError {} impl From for ReadlineError { fn from(err: io::Error) -> Self { ReadlineError::Io(err) } } impl From for ReadlineError { fn from(kind: io::ErrorKind) -> Self { ReadlineError::Io(io::Error::from(kind)) } } #[cfg(unix)] impl From for ReadlineError { fn from(err: nix::Error) -> Self { ReadlineError::Errno(err) } } #[cfg(windows)] impl From for ReadlineError { fn from(err: char::DecodeUtf16Error) -> Self { ReadlineError::Decode(err) } } rustyline-6.3.0/src/highlight.rs010064400007650000024000000210631366145270200150510ustar 00000000000000//! Syntax highlighting use crate::config::CompletionType; use memchr::memchr; use std::borrow::Cow::{self, Borrowed, Owned}; use std::cell::Cell; /// Syntax highlighter with [ANSI color](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters). /// Rustyline will try to handle escape sequence for ANSI color on windows /// when not supported natively (windows <10). /// /// Currently, the highlighted version *must* have the same display width as /// the original input. pub trait Highlighter { /// Takes the currently edited `line` with the cursor `pos`ition and /// returns the highlighted version (with ANSI color). /// /// For example, you can implement /// [blink-matching-paren](https://www.gnu.org/software/bash/manual/html_node/Readline-Init-File-Syntax.html). fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> { let _ = pos; Borrowed(line) } /// Takes the `prompt` and /// returns the highlighted version (with ANSI color). fn highlight_prompt<'b, 's: 'b, 'p: 'b>( &'s self, prompt: &'p str, default: bool, ) -> Cow<'b, str> { let _ = default; Borrowed(prompt) } /// Takes the `hint` and /// returns the highlighted version (with ANSI color). fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { Borrowed(hint) } /// Takes the completion `candidate` and /// returns the highlighted version (with ANSI color). /// /// Currently, used only with `CompletionType::List`. fn highlight_candidate<'c>( &self, candidate: &'c str, completion: CompletionType, ) -> Cow<'c, str> { let _ = completion; Borrowed(candidate) } /// Tells if `line` needs to be highlighted when a specific char is typed or /// when cursor is moved under a specific char. /// /// Used to optimize refresh when a character is inserted or the cursor is /// moved. fn highlight_char(&self, line: &str, pos: usize) -> bool { let _ = (line, pos); false } } impl Highlighter for () {} impl<'r, H: ?Sized + Highlighter> Highlighter for &'r H { fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> { (**self).highlight(line, pos) } fn highlight_prompt<'b, 's: 'b, 'p: 'b>( &'s self, prompt: &'p str, default: bool, ) -> Cow<'b, str> { (**self).highlight_prompt(prompt, default) } fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { (**self).highlight_hint(hint) } fn highlight_candidate<'c>( &self, candidate: &'c str, completion: CompletionType, ) -> Cow<'c, str> { (**self).highlight_candidate(candidate, completion) } fn highlight_char(&self, line: &str, pos: usize) -> bool { (**self).highlight_char(line, pos) } } const OPENS: &[u8; 3] = b"{[("; const CLOSES: &[u8; 3] = b"}])"; /// Highlight matching bracket when typed or cursor moved on. #[derive(Default)] pub struct MatchingBracketHighlighter { bracket: Cell>, // memorize the character to search... } impl MatchingBracketHighlighter { /// Constructor pub fn new() -> Self { Self { bracket: Cell::new(None), } } } impl Highlighter for MatchingBracketHighlighter { fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> { if line.len() <= 1 { return Borrowed(line); } // highlight matching brace/bracket/parenthesis if it exists if let Some((bracket, pos)) = self.bracket.get() { if let Some((matching, idx)) = find_matching_bracket(line, pos, bracket) { let mut copy = line.to_owned(); copy.replace_range(idx..=idx, &format!("\x1b[1;34m{}\x1b[0m", matching as char)); return Owned(copy); } } Borrowed(line) } fn highlight_char(&self, line: &str, pos: usize) -> bool { // will highlight matching brace/bracket/parenthesis if it exists self.bracket.set(check_bracket(line, pos)); self.bracket.get().is_some() } } fn find_matching_bracket(line: &str, pos: usize, bracket: u8) -> Option<(u8, usize)> { let matching = matching_bracket(bracket); let mut idx; let mut unmatched = 1; if is_open_bracket(bracket) { // forward search idx = pos + 1; let bytes = &line.as_bytes()[idx..]; for b in bytes { if *b == matching { unmatched -= 1; if unmatched == 0 { debug_assert_eq!(matching, line.as_bytes()[idx]); return Some((matching, idx)); } } else if *b == bracket { unmatched += 1; } idx += 1; } debug_assert_eq!(idx, line.len()); } else { // backward search idx = pos; let bytes = &line.as_bytes()[..idx]; for b in bytes.iter().rev() { if *b == matching { unmatched -= 1; if unmatched == 0 { debug_assert_eq!(matching, line.as_bytes()[idx - 1]); return Some((matching, idx - 1)); } } else if *b == bracket { unmatched += 1; } idx -= 1; } debug_assert_eq!(idx, 0); } None } // check under or before the cursor fn check_bracket(line: &str, pos: usize) -> Option<(u8, usize)> { if line.is_empty() { return None; } let mut pos = pos; if pos >= line.len() { pos = line.len() - 1; // before cursor let b = line.as_bytes()[pos]; // previous byte if is_close_bracket(b) { Some((b, pos)) } else { None } } else { let mut under_cursor = true; loop { let b = line.as_bytes()[pos]; if is_close_bracket(b) { if pos == 0 { return None; } else { return Some((b, pos)); } } else if is_open_bracket(b) { if pos + 1 == line.len() { return None; } else { return Some((b, pos)); } } else if under_cursor && pos > 0 { under_cursor = false; pos -= 1; // or before cursor } else { return None; } } } } fn matching_bracket(bracket: u8) -> u8 { match bracket { b'{' => b'}', b'}' => b'{', b'[' => b']', b']' => b'[', b'(' => b')', b')' => b'(', b => b, } } fn is_open_bracket(bracket: u8) -> bool { memchr(bracket, OPENS).is_some() } fn is_close_bracket(bracket: u8) -> bool { memchr(bracket, CLOSES).is_some() } #[cfg(test)] mod tests { #[test] pub fn find_matching_bracket() { use super::find_matching_bracket; assert_eq!(find_matching_bracket("(...", 0, b'('), None); assert_eq!(find_matching_bracket("...)", 3, b')'), None); assert_eq!(find_matching_bracket("()..", 0, b'('), Some((b')', 1))); assert_eq!(find_matching_bracket("(..)", 0, b'('), Some((b')', 3))); assert_eq!(find_matching_bracket("..()", 3, b')'), Some((b'(', 2))); assert_eq!(find_matching_bracket("(..)", 3, b')'), Some((b'(', 0))); assert_eq!(find_matching_bracket("(())", 0, b'('), Some((b')', 3))); assert_eq!(find_matching_bracket("(())", 3, b')'), Some((b'(', 0))); } #[test] pub fn check_bracket() { use super::check_bracket; assert_eq!(check_bracket(")...", 0), None); assert_eq!(check_bracket("(...", 2), None); assert_eq!(check_bracket("...(", 3), None); assert_eq!(check_bracket("...(", 4), None); assert_eq!(check_bracket("..).", 4), None); assert_eq!(check_bracket("(...", 0), Some((b'(', 0))); assert_eq!(check_bracket("(...", 1), Some((b'(', 0))); assert_eq!(check_bracket("...)", 3), Some((b')', 3))); assert_eq!(check_bracket("...)", 4), Some((b')', 3))); } #[test] pub fn matching_bracket() { use super::matching_bracket; assert_eq!(matching_bracket(b'('), b')'); assert_eq!(matching_bracket(b')'), b'('); } #[test] pub fn is_open_bracket() { use super::is_close_bracket; use super::is_open_bracket; assert!(is_open_bracket(b'(')); assert!(is_close_bracket(b')')); } } rustyline-6.3.0/src/hint.rs010064400007650000024000000040251372733102500140400ustar 00000000000000//! Hints (suggestions at the right of the prompt as you type). use crate::history::Direction; use crate::Context; /// Hints provider pub trait Hinter { /// Takes the currently edited `line` with the cursor `pos`ition and /// returns the string that should be displayed or `None` /// if no hint is available for the text the user currently typed. // TODO Validate: called while editing line but not while moving cursor. fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option { let _ = (line, pos, ctx); None } } impl Hinter for () {} impl<'r, H: ?Sized + Hinter> Hinter for &'r H { fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option { (**self).hint(line, pos, ctx) } } /// Add suggestion based on previous history entries matching current user /// input. pub struct HistoryHinter {} impl Hinter for HistoryHinter { fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option { if pos < line.len() { return None; } let start = if ctx.history_index() == ctx.history().len() { ctx.history_index().saturating_sub(1) } else { ctx.history_index() }; if let Some(history_index) = ctx.history .starts_with(&line[..pos], start, Direction::Reverse) { let entry = ctx.history.get(history_index); if let Some(entry) = entry { if entry == line || entry == &line[..pos] { return None; } } return entry.map(|s| s[pos..].to_owned()); } None } } #[cfg(test)] mod test { use super::{Hinter, HistoryHinter}; use crate::history::History; use crate::Context; #[test] pub fn empty_history() { let history = History::new(); let ctx = Context::new(&history); let hinter = HistoryHinter {}; let hint = hinter.hint("test", 4, &ctx); assert_eq!(None, hint); } } rustyline-6.3.0/src/history.rs010064400007650000024000000341571372733102500146100ustar 00000000000000//! History API use log::warn; use std::collections::vec_deque; use std::collections::VecDeque; use std::fs::File; use std::iter::DoubleEndedIterator; use std::ops::Index; use std::path::Path; use super::Result; use crate::config::{Config, HistoryDuplicates}; /// Search direction #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Direction { /// Search history forward Forward, /// Search history backward Reverse, } /// Current state of the history. #[derive(Default)] pub struct History { entries: VecDeque, max_len: usize, pub(crate) ignore_space: bool, pub(crate) ignore_dups: bool, } impl History { // New multiline-aware history files start with `#V2\n` and have newlines // and backslashes escaped in them. const FILE_VERSION_V2: &'static str = "#V2"; /// Default constructor pub fn new() -> Self { Self::with_config(Config::default()) } /// Customized constructor with: /// - `Config::max_history_size()`, /// - `Config::history_ignore_space()`, /// - `Config::history_duplicates()`. pub fn with_config(config: Config) -> Self { Self { entries: VecDeque::new(), max_len: config.max_history_size(), ignore_space: config.history_ignore_space(), ignore_dups: config.history_duplicates() == HistoryDuplicates::IgnoreConsecutive, } } /// Return the history entry at position `index`, starting from 0. pub fn get(&self, index: usize) -> Option<&String> { self.entries.get(index) } /// Return the last history entry (i.e. previous command) pub fn last(&self) -> Option<&String> { self.entries.back() } /// Add a new entry in the history. pub fn add + Into>(&mut self, line: S) -> bool { if self.max_len == 0 { return false; } if line.as_ref().is_empty() || (self.ignore_space && line .as_ref() .chars() .next() .map_or(true, char::is_whitespace)) { return false; } if self.ignore_dups { if let Some(s) = self.entries.back() { if s == line.as_ref() { return false; } } } if self.entries.len() == self.max_len { self.entries.pop_front(); } self.entries.push_back(line.into()); true } /// Return the number of entries in the history. pub fn len(&self) -> usize { self.entries.len() } /// Return true if the history has no entry. pub fn is_empty(&self) -> bool { self.entries.is_empty() } /// Set the maximum length for the history. This function can be called even /// if there is already some history, the function will make sure to retain /// just the latest `len` elements if the new history length value is /// smaller than the amount of items already inside the history. /// /// Like [stifle_history](http://cnswww.cns.cwru. /// edu/php/chet/readline/history.html#IDX11). pub fn set_max_len(&mut self, len: usize) { self.max_len = len; if len == 0 { self.entries.clear(); return; } loop { if self.entries.len() <= len { break; } self.entries.pop_front(); } } /// Save the history in the specified file. // TODO append_history // http://cnswww.cns.cwru.edu/php/chet/readline/history.html#IDX30 // TODO history_truncate_file // http://cnswww.cns.cwru.edu/php/chet/readline/history.html#IDX31 pub fn save + ?Sized>(&self, path: &P) -> Result<()> { use std::io::{BufWriter, Write}; if self.is_empty() { return Ok(()); } let old_umask = umask(); let f = File::create(path); restore_umask(old_umask); let file = f?; fix_perm(&file); let mut wtr = BufWriter::new(file); wtr.write_all(Self::FILE_VERSION_V2.as_bytes())?; for entry in &self.entries { wtr.write_all(b"\n")?; let mut bytes = entry.as_bytes(); while let Some(i) = memchr::memchr2(b'\\', b'\n', bytes) { wtr.write_all(&bytes[..i])?; if bytes[i] == b'\n' { wtr.write_all(b"\\n")?; // escaped line feed } else { debug_assert_eq!(bytes[i], b'\\'); wtr.write_all(b"\\\\")?; // escaped backslash } bytes = &bytes[i + 1..]; } wtr.write_all(bytes)?; // remaining bytes with no \n or \ } wtr.write_all(b"\n")?; // https://github.com/rust-lang/rust/issues/32677#issuecomment-204833485 wtr.flush()?; Ok(()) } /// Load the history from the specified file. /// /// # Errors /// Will return `Err` if path does not already exist or could not be read. pub fn load + ?Sized>(&mut self, path: &P) -> Result<()> { use std::io::{BufRead, BufReader}; let file = File::open(&path)?; let rdr = BufReader::new(file); let mut lines = rdr.lines(); let mut v2 = false; if let Some(first) = lines.next() { let line = first?; if line == Self::FILE_VERSION_V2 { v2 = true; } else { self.add(line); } } 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; } } self.add(line); // TODO truncate to MAX_LINE } Ok(()) } /// Clear history pub fn clear(&mut self) { self.entries.clear() } /// Search history (start position inclusive [0, len-1]). /// /// Return the absolute index of the nearest history entry that matches /// `term`. /// /// Return None if no entry contains `term` between [start, len -1] for /// forward search /// or between [0, start] for reverse search. pub fn search(&self, term: &str, start: usize, dir: Direction) -> Option { let test = |entry: &String| entry.contains(term); self.search_match(term, start, dir, test) } /// Anchored search pub fn starts_with(&self, term: &str, start: usize, dir: Direction) -> Option { let test = |entry: &String| entry.starts_with(term); self.search_match(term, start, dir, test) } fn search_match(&self, term: &str, start: usize, dir: Direction, test: F) -> Option where F: Fn(&String) -> bool, { if term.is_empty() || start >= self.len() { return None; } match dir { Direction::Reverse => { let index = self .entries .iter() .rev() .skip(self.entries.len() - 1 - start) .position(test); index.map(|index| start - index) } Direction::Forward => { let index = self.entries.iter().skip(start).position(test); index.map(|index| index + start) } } } /// Return a forward iterator. pub fn iter(&self) -> Iter<'_> { Iter(self.entries.iter()) } } impl Index for History { type Output = String; fn index(&self, index: usize) -> &String { &self.entries[index] } } impl<'a> IntoIterator for &'a History { type IntoIter = Iter<'a>; type Item = &'a String; fn into_iter(self) -> Iter<'a> { self.iter() } } /// History iterator. pub struct Iter<'a>(vec_deque::Iter<'a, String>); impl<'a> Iterator for Iter<'a> { type Item = &'a String; fn next(&mut self) -> Option<&'a String> { self.0.next() } fn size_hint(&self) -> (usize, Option) { self.0.size_hint() } } impl<'a> DoubleEndedIterator for Iter<'a> { fn next_back(&mut self) -> Option<&'a String> { self.0.next_back() } } cfg_if::cfg_if! { if #[cfg(any(windows, target_arch = "wasm32"))] { fn umask() -> u16 { 0 } fn restore_umask(_: u16) {} fn fix_perm(_: &File) {} } else if #[cfg(unix)] { fn umask() -> libc::mode_t { unsafe { libc::umask(libc::S_IXUSR | libc::S_IRWXG | libc::S_IRWXO) } } fn restore_umask(old_umask: libc::mode_t) { unsafe { libc::umask(old_umask); } } fn fix_perm(file: &File) { use std::os::unix::io::AsRawFd; unsafe { libc::fchmod(file.as_raw_fd(), libc::S_IRUSR | libc::S_IWUSR); } } } } #[cfg(test)] mod tests { use super::{Direction, History}; use crate::config::Config; use crate::Result; fn init() -> History { let mut history = History::new(); assert!(history.add("line1")); assert!(history.add("line2")); assert!(history.add("line3")); history } #[test] fn new() { let history = History::new(); assert_eq!(0, history.entries.len()); } #[test] fn add() { let config = Config::builder().history_ignore_space(true).build(); let mut history = History::with_config(config); assert_eq!(config.max_history_size(), history.max_len); assert!(history.add("line1")); assert!(history.add("line2")); assert!(!history.add("line2")); assert!(!history.add("")); assert!(!history.add(" line3")); } #[test] fn set_max_len() { let mut history = init(); history.set_max_len(1); assert_eq!(1, history.entries.len()); assert_eq!(Some(&"line3".to_owned()), history.last()); } #[test] #[cfg_attr(miri, ignore)] // unsupported operation: `getcwd` not available when isolation is enabled fn save() -> Result<()> { check_save("line\nfour \\ abc") } #[test] fn save_windows_path() -> Result<()> { let path = "cd source\\repos\\forks\\nushell\\"; check_save(path) } #[cfg_attr(miri, ignore)] // unsupported operation: `getcwd` not available when isolation is enabled fn check_save(line: &str) -> Result<()> { let mut history = init(); assert!(history.add(line)); let tf = tempfile::NamedTempFile::new()?; history.save(tf.path())?; let mut history2 = History::new(); history2.load(tf.path())?; for (a, b) in history.entries.iter().zip(history2.entries.iter()) { assert_eq!(a, b); } tf.close()?; Ok(()) } #[test] #[cfg_attr(miri, ignore)] // unsupported operation: `getcwd` not available when isolation is enabled fn load_legacy() -> Result<()> { use std::io::Write; let tf = tempfile::NamedTempFile::new()?; { let mut legacy = std::fs::File::create(tf.path())?; // Some data we'd accidentally corrupt if we got the version wrong let data = b"\ test\\n \\abc \\123\n\ 123\\n\\\\n\n\ abcde "; legacy.write_all(data)?; legacy.flush()?; } let mut history = History::new(); history.load(tf.path())?; assert_eq!(history.entries[0], "test\\n \\abc \\123"); assert_eq!(history.entries[1], "123\\n\\\\n"); assert_eq!(history.entries[2], "abcde"); tf.close()?; Ok(()) } #[test] fn search() { let history = init(); assert_eq!(None, history.search("", 0, Direction::Forward)); assert_eq!(None, history.search("none", 0, Direction::Forward)); assert_eq!(None, history.search("line", 3, Direction::Forward)); assert_eq!(Some(0), history.search("line", 0, Direction::Forward)); assert_eq!(Some(1), history.search("line", 1, Direction::Forward)); assert_eq!(Some(2), history.search("line3", 1, Direction::Forward)); } #[test] fn reverse_search() { let history = init(); assert_eq!(None, history.search("", 2, Direction::Reverse)); assert_eq!(None, history.search("none", 2, Direction::Reverse)); assert_eq!(None, history.search("line", 3, Direction::Reverse)); assert_eq!(Some(2), history.search("line", 2, Direction::Reverse)); assert_eq!(Some(1), history.search("line", 1, Direction::Reverse)); assert_eq!(Some(0), history.search("line1", 1, Direction::Reverse)); } } rustyline-6.3.0/src/keymap.rs010064400007650000024000001103351372023615700143710ustar 00000000000000//! Bindings from keys to command for Emacs and Vi modes use std::collections::HashMap; use std::sync::{Arc, RwLock}; use log::debug; use super::Result; use crate::config::Config; use crate::config::EditMode; use crate::keys::KeyPress; use crate::tty::{RawReader, Term, Terminal}; /// The number of times one command should be repeated. pub type RepeatCount = usize; /// Commands #[derive(Debug, Clone, PartialEq)] #[non_exhaustive] pub enum Cmd { /// abort Abort, // Miscellaneous Command /// accept-line AcceptLine, /// beginning-of-history BeginningOfHistory, /// capitalize-word CapitalizeWord, /// clear-screen ClearScreen, /// complete Complete, /// complete-backward CompleteBackward, /// complete-hint CompleteHint, /// downcase-word DowncaseWord, /// vi-eof-maybe EndOfFile, /// end-of-history EndOfHistory, /// forward-search-history ForwardSearchHistory, /// history-search-backward HistorySearchBackward, /// history-search-forward HistorySearchForward, /// Insert 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, /// vi-replace Overwrite(char), /// previous-history PreviousHistory, /// quoted-insert QuotedInsert, /// vi-change-char ReplaceChar(RepeatCount, char), /// vi-change-to, vi-substitute Replace(Movement, Option), /// reverse-search-history ReverseSearchHistory, /// self-insert SelfInsert(RepeatCount, char), /// Suspend 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), /// accepts the line when cursor is at the end of the text (non including /// trailing whitespace), inserts newline character otherwise AcceptOrInsertLine, } impl Cmd { /// Tells if current command should reset kill ring. pub fn should_reset_kill_ring(&self) -> bool { #[allow(clippy::match_same_arms)] match *self { Cmd::Kill(Movement::BackwardChar(_)) | Cmd::Kill(Movement::ForwardChar(_)) => true, Cmd::ClearScreen | Cmd::Kill(_) | Cmd::Replace(..) | Cmd::Noop | Cmd::Suspend | Cmd::Yank(..) | Cmd::YankPop => false, _ => true, } } fn is_repeatable_change(&self) -> bool { match *self { Cmd::Insert(..) | Cmd::Kill(_) | Cmd::ReplaceChar(..) | Cmd::Replace(..) | Cmd::SelfInsert(..) | Cmd::ViYankTo(_) | Cmd::Yank(..) => true, // Cmd::TransposeChars | TODO Validate _ => false, } } fn is_repeatable(&self) -> bool { match *self { Cmd::Move(_) => true, _ => self.is_repeatable_change(), } } // Replay this command with a possible different `RepeatCount`. fn redo(&self, new: Option, wrt: &dyn Refresher) -> Self { match *self { Cmd::Insert(previous, ref text) => { Cmd::Insert(repeat_count(previous, new), text.clone()) } Cmd::Kill(ref mvt) => Cmd::Kill(mvt.redo(new)), Cmd::Move(ref mvt) => Cmd::Move(mvt.redo(new)), Cmd::ReplaceChar(previous, c) => Cmd::ReplaceChar(repeat_count(previous, new), c), Cmd::Replace(ref mvt, ref text) => { if text.is_none() { let last_insert = wrt.last_insert(); if let Movement::ForwardChar(0) = mvt { Cmd::Replace( Movement::ForwardChar(last_insert.as_ref().map_or(0, String::len)), last_insert, ) } else { Cmd::Replace(mvt.redo(new), last_insert) } } else { Cmd::Replace(mvt.redo(new), text.clone()) } } Cmd::SelfInsert(previous, c) => { // consecutive char inserts are repeatable not only the last one... if let Some(text) = wrt.last_insert() { Cmd::Insert(repeat_count(previous, new), text) } else { Cmd::SelfInsert(repeat_count(previous, new), c) } } // Cmd::TransposeChars => Cmd::TransposeChars, Cmd::ViYankTo(ref mvt) => Cmd::ViYankTo(mvt.redo(new)), Cmd::Yank(previous, anchor) => Cmd::Yank(repeat_count(previous, new), anchor), _ => unreachable!(), } } } fn repeat_count(previous: RepeatCount, new: Option) -> RepeatCount { match new { Some(n) => n, None => previous, } } /// Different word definitions #[derive(Debug, Clone, PartialEq, Copy)] pub enum Word { /// non-blanks characters Big, /// alphanumeric characters Emacs, /// alphanumeric (and '_') characters Vi, } /// Where to move with respect to word boundary #[derive(Debug, Clone, PartialEq, Copy)] pub enum At { /// Start of word. Start, /// Before end of word. BeforeEnd, /// After end of word. AfterEnd, } /// Where to paste (relative to cursor position) #[derive(Debug, Clone, PartialEq, Copy)] pub enum Anchor { /// After cursor After, /// Before cursor Before, } /// Vi character search #[derive(Debug, Clone, 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 { fn opposite(self) -> Self { match self { CharSearch::Forward(c) => CharSearch::Backward(c), CharSearch::ForwardBefore(c) => CharSearch::BackwardAfter(c), CharSearch::Backward(c) => CharSearch::Forward(c), CharSearch::BackwardAfter(c) => CharSearch::ForwardBefore(c), } } } /// Where to move #[derive(Debug, Clone, PartialEq)] #[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 /// 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`. 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, } } } #[derive(PartialEq)] enum InputMode { /// Vi Command/Alternate Command, /// Insert/Input mode Insert, /// Overwrite mode Replace, } /// Transform key(s) to commands based on current input mode pub struct InputState { mode: EditMode, custom_bindings: Arc>>, input_mode: InputMode, // vi only ? // numeric arguments: http://web.mit.edu/gnu/doc/html/rlman_1.html#SEC7 num_args: i16, last_cmd: Cmd, // vi only last_char_search: Option, // vi only } /// Provide indirect mutation to user input. pub trait Invoke { /// currently edited line fn input(&self) -> &str; // TODO //fn invoke(&mut self, cmd: Cmd) -> Result; } pub trait Refresher { /// Rewrite the currently edited line accordingly to the buffer content, /// cursor position, and number of columns of the terminal. fn refresh_line(&mut self) -> Result<()>; /// Same as [`refresh_line`] with a specific message instead of hint fn refresh_line_with_msg(&mut self, msg: Option) -> Result<()>; /// Same as `refresh_line` but with a dynamic prompt. fn refresh_prompt_and_line(&mut self, prompt: &str) -> Result<()>; /// Vi only, switch to insert mode. fn doing_insert(&mut self); /// Vi only, switch to command mode. fn done_inserting(&mut self); /// Vi only, last text inserted. fn last_insert(&self) -> Option; /// Returns `true` if the cursor is currently at the end of the line. fn is_cursor_at_end(&self) -> bool; /// Returns `true` if there is a hint displayed. fn has_hint(&self) -> bool; } impl InputState { pub fn new(config: &Config, custom_bindings: Arc>>) -> Self { Self { mode: config.edit_mode(), custom_bindings, input_mode: InputMode::Insert, num_args: 0, last_cmd: Cmd::Noop, last_char_search: None, } } pub fn is_emacs_mode(&self) -> bool { self.mode == EditMode::Emacs } /// Parse user input into one command /// `single_esc_abort` is used in emacs mode on unix platform when a single /// esc key is expected to abort current action. pub fn next_cmd( &mut self, rdr: &mut ::Reader, wrt: &mut dyn Refresher, single_esc_abort: bool, ) -> Result { match self.mode { EditMode::Emacs => { let key = rdr.next_key(single_esc_abort)?; self.emacs(rdr, wrt, key) } EditMode::Vi if self.input_mode != InputMode::Command => { let key = rdr.next_key(false)?; self.vi_insert(rdr, wrt, key) } EditMode::Vi => { let key = rdr.next_key(false)?; self.vi_command(rdr, wrt, key) } } } fn emacs_digit_argument( &mut self, rdr: &mut R, wrt: &mut dyn Refresher, digit: char, ) -> Result { #[allow(clippy::cast_possible_truncation)] match digit { '0'..='9' => { self.num_args = digit.to_digit(10).unwrap() as i16; } '-' => { self.num_args = -1; } _ => unreachable!(), } loop { wrt.refresh_prompt_and_line(&format!("(arg: {}) ", self.num_args))?; let key = rdr.next_key(true)?; #[allow(clippy::cast_possible_truncation)] match key { KeyPress::Char(digit @ '0'..='9') | KeyPress::Meta(digit @ '0'..='9') => { if self.num_args == -1 { self.num_args *= digit.to_digit(10).unwrap() as i16; } else if self.num_args.abs() < 1000 { // shouldn't ever need more than 4 digits self.num_args = self .num_args .saturating_mul(10) .saturating_add(digit.to_digit(10).unwrap() as i16); } } KeyPress::Char('-') | KeyPress::Meta('-') => {} _ => { wrt.refresh_line()?; return Ok(key); } }; } } fn emacs( &mut self, rdr: &mut R, wrt: &mut dyn Refresher, mut key: KeyPress, ) -> Result { if let KeyPress::Meta(digit @ '-') = key { key = self.emacs_digit_argument(rdr, wrt, digit)?; } else if let KeyPress::Meta(digit @ '0'..='9') = key { key = self.emacs_digit_argument(rdr, wrt, digit)?; } let (n, positive) = self.emacs_num_args(); // consume them in all cases { let bindings = self.custom_bindings.read().unwrap(); if let Some(cmd) = bindings.get(&key) { debug!(target: "rustyline", "Custom command: {:?}", cmd); return Ok(if cmd.is_repeatable() { cmd.redo(Some(n), wrt) } else { cmd.clone() }); } } let cmd = match key { KeyPress::Char(c) => { if positive { Cmd::SelfInsert(n, c) } else { Cmd::Unknown } } KeyPress::Ctrl('A') => Cmd::Move(Movement::BeginningOfLine), KeyPress::Ctrl('B') => { if positive { Cmd::Move(Movement::BackwardChar(n)) } else { Cmd::Move(Movement::ForwardChar(n)) } } KeyPress::Ctrl('E') => Cmd::Move(Movement::EndOfLine), KeyPress::Ctrl('F') => { if positive { Cmd::Move(Movement::ForwardChar(n)) } else { Cmd::Move(Movement::BackwardChar(n)) } } KeyPress::Ctrl('G') | KeyPress::Esc | KeyPress::Meta('\x07') => Cmd::Abort, KeyPress::Ctrl('H') | KeyPress::Backspace => { if positive { Cmd::Kill(Movement::BackwardChar(n)) } else { Cmd::Kill(Movement::ForwardChar(n)) } } KeyPress::BackTab => Cmd::CompleteBackward, KeyPress::Tab => { if positive { Cmd::Complete } else { Cmd::CompleteBackward } } // Don't complete hints when the cursor is not at the end of a line KeyPress::Right if wrt.has_hint() && wrt.is_cursor_at_end() => Cmd::CompleteHint, KeyPress::Ctrl('K') => { if positive { Cmd::Kill(Movement::EndOfLine) } else { Cmd::Kill(Movement::BeginningOfLine) } } KeyPress::Ctrl('L') => Cmd::ClearScreen, KeyPress::Ctrl('N') => Cmd::NextHistory, KeyPress::Ctrl('P') => Cmd::PreviousHistory, KeyPress::Ctrl('X') => { let snd_key = rdr.next_key(true)?; match snd_key { KeyPress::Ctrl('G') | KeyPress::Esc => Cmd::Abort, KeyPress::Ctrl('U') => Cmd::Undo(n), _ => Cmd::Unknown, } } KeyPress::Meta('\x08') | KeyPress::Meta('\x7f') => { if positive { Cmd::Kill(Movement::BackwardWord(n, Word::Emacs)) } else { Cmd::Kill(Movement::ForwardWord(n, At::AfterEnd, Word::Emacs)) } } KeyPress::Meta('<') => Cmd::BeginningOfHistory, KeyPress::Meta('>') => Cmd::EndOfHistory, KeyPress::Meta('B') | KeyPress::Meta('b') => { if positive { Cmd::Move(Movement::BackwardWord(n, Word::Emacs)) } else { Cmd::Move(Movement::ForwardWord(n, At::AfterEnd, Word::Emacs)) } } KeyPress::Meta('C') | KeyPress::Meta('c') => Cmd::CapitalizeWord, KeyPress::Meta('D') | KeyPress::Meta('d') => { if positive { Cmd::Kill(Movement::ForwardWord(n, At::AfterEnd, Word::Emacs)) } else { Cmd::Kill(Movement::BackwardWord(n, Word::Emacs)) } } KeyPress::Meta('F') | KeyPress::Meta('f') => { if positive { Cmd::Move(Movement::ForwardWord(n, At::AfterEnd, Word::Emacs)) } else { Cmd::Move(Movement::BackwardWord(n, Word::Emacs)) } } KeyPress::Meta('L') | KeyPress::Meta('l') => Cmd::DowncaseWord, KeyPress::Meta('T') | KeyPress::Meta('t') => Cmd::TransposeWords(n), KeyPress::Meta('U') | KeyPress::Meta('u') => Cmd::UpcaseWord, KeyPress::Meta('Y') | KeyPress::Meta('y') => Cmd::YankPop, _ => self.common(rdr, key, n, positive)?, }; debug!(target: "rustyline", "Emacs command: {:?}", cmd); Ok(cmd) } #[allow(clippy::cast_possible_truncation)] fn vi_arg_digit( &mut self, rdr: &mut R, wrt: &mut dyn Refresher, digit: char, ) -> Result { self.num_args = digit.to_digit(10).unwrap() as i16; loop { wrt.refresh_prompt_and_line(&format!("(arg: {}) ", self.num_args))?; let key = rdr.next_key(false)?; if let KeyPress::Char(digit @ '0'..='9') = key { if self.num_args.abs() < 1000 { // shouldn't ever need more than 4 digits self.num_args = self .num_args .saturating_mul(10) .saturating_add(digit.to_digit(10).unwrap() as i16); } } else { wrt.refresh_line()?; return Ok(key); }; } } fn vi_command( &mut self, rdr: &mut R, wrt: &mut dyn Refresher, mut key: KeyPress, ) -> Result { if let KeyPress::Char(digit @ '1'..='9') = key { key = self.vi_arg_digit(rdr, wrt, digit)?; } let no_num_args = self.num_args == 0; let n = self.vi_num_args(); // consume them in all cases { let bindings = self.custom_bindings.read().unwrap(); if let Some(cmd) = bindings.get(&key) { debug!(target: "rustyline", "Custom command: {:?}", cmd); return Ok(if cmd.is_repeatable() { if no_num_args { cmd.redo(None, wrt) } else { cmd.redo(Some(n), wrt) } } else { cmd.clone() }); } } let cmd = match key { KeyPress::Char('$') | KeyPress::End => Cmd::Move(Movement::EndOfLine), KeyPress::Char('.') => { // vi-redo (repeat last command) if no_num_args { self.last_cmd.redo(None, wrt) } else { self.last_cmd.redo(Some(n), wrt) } } // TODO KeyPress::Char('%') => Cmd::???, Move to the corresponding opening/closing // bracket KeyPress::Char('0') => Cmd::Move(Movement::BeginningOfLine), KeyPress::Char('^') => Cmd::Move(Movement::ViFirstPrint), KeyPress::Char('a') => { // vi-append-mode self.input_mode = InputMode::Insert; wrt.doing_insert(); Cmd::Move(Movement::ForwardChar(n)) } KeyPress::Char('A') => { // vi-append-eol self.input_mode = InputMode::Insert; wrt.doing_insert(); Cmd::Move(Movement::EndOfLine) } KeyPress::Char('b') => Cmd::Move(Movement::BackwardWord(n, Word::Vi)), // vi-prev-word KeyPress::Char('B') => Cmd::Move(Movement::BackwardWord(n, Word::Big)), KeyPress::Char('c') => { self.input_mode = InputMode::Insert; match self.vi_cmd_motion(rdr, wrt, key, n)? { Some(mvt) => Cmd::Replace(mvt, None), None => Cmd::Unknown, } } KeyPress::Char('C') => { self.input_mode = InputMode::Insert; Cmd::Replace(Movement::EndOfLine, None) } KeyPress::Char('d') => match self.vi_cmd_motion(rdr, wrt, key, n)? { Some(mvt) => Cmd::Kill(mvt), None => Cmd::Unknown, }, KeyPress::Char('D') | KeyPress::Ctrl('K') => Cmd::Kill(Movement::EndOfLine), KeyPress::Char('e') => Cmd::Move(Movement::ForwardWord(n, At::BeforeEnd, Word::Vi)), KeyPress::Char('E') => Cmd::Move(Movement::ForwardWord(n, At::BeforeEnd, Word::Big)), KeyPress::Char('i') => { // vi-insertion-mode self.input_mode = InputMode::Insert; wrt.doing_insert(); Cmd::Noop } KeyPress::Char('I') => { // vi-insert-beg self.input_mode = InputMode::Insert; wrt.doing_insert(); Cmd::Move(Movement::BeginningOfLine) } KeyPress::Char(c) if c == 'f' || c == 'F' || c == 't' || c == 'T' => { // vi-char-search let cs = self.vi_char_search(rdr, c)?; match cs { Some(cs) => Cmd::Move(Movement::ViCharSearch(n, cs)), None => Cmd::Unknown, } } KeyPress::Char(';') => match self.last_char_search { Some(cs) => Cmd::Move(Movement::ViCharSearch(n, cs)), None => Cmd::Noop, }, KeyPress::Char(',') => match self.last_char_search { Some(ref cs) => Cmd::Move(Movement::ViCharSearch(n, cs.opposite())), None => Cmd::Noop, }, // TODO KeyPress::Char('G') => Cmd::???, Move to the history line n KeyPress::Char('p') => Cmd::Yank(n, Anchor::After), // vi-put KeyPress::Char('P') => Cmd::Yank(n, Anchor::Before), // vi-put KeyPress::Char('r') => { // vi-replace-char: let ch = rdr.next_key(false)?; match ch { KeyPress::Char(c) => Cmd::ReplaceChar(n, c), KeyPress::Esc => Cmd::Noop, _ => Cmd::Unknown, } } KeyPress::Char('R') => { // vi-replace-mode (overwrite-mode) self.input_mode = InputMode::Replace; Cmd::Replace(Movement::ForwardChar(0), None) } KeyPress::Char('s') => { // vi-substitute-char: self.input_mode = InputMode::Insert; Cmd::Replace(Movement::ForwardChar(n), None) } KeyPress::Char('S') => { // vi-substitute-line: self.input_mode = InputMode::Insert; Cmd::Replace(Movement::WholeLine, None) } KeyPress::Char('u') => Cmd::Undo(n), // KeyPress::Char('U') => Cmd::???, // revert-line KeyPress::Char('w') => Cmd::Move(Movement::ForwardWord(n, At::Start, Word::Vi)), /* vi-next-word */ KeyPress::Char('W') => Cmd::Move(Movement::ForwardWord(n, At::Start, Word::Big)), /* vi-next-word */ // TODO move backward if eol KeyPress::Char('x') => Cmd::Kill(Movement::ForwardChar(n)), // vi-delete KeyPress::Char('X') => Cmd::Kill(Movement::BackwardChar(n)), // vi-rubout KeyPress::Char('y') => match self.vi_cmd_motion(rdr, wrt, key, n)? { Some(mvt) => Cmd::ViYankTo(mvt), None => Cmd::Unknown, }, // KeyPress::Char('Y') => Cmd::???, // vi-yank-to KeyPress::Char('h') | KeyPress::Ctrl('H') | KeyPress::Backspace => { Cmd::Move(Movement::BackwardChar(n)) } KeyPress::Ctrl('G') => Cmd::Abort, KeyPress::Char('l') | KeyPress::Char(' ') => Cmd::Move(Movement::ForwardChar(n)), KeyPress::Ctrl('L') => Cmd::ClearScreen, KeyPress::Char('+') | KeyPress::Char('j') => Cmd::LineDownOrNextHistory(n), // TODO: move to the start of the line. KeyPress::Ctrl('N') => Cmd::NextHistory, KeyPress::Char('-') | KeyPress::Char('k') => Cmd::LineUpOrPreviousHistory(n), // TODO: move to the start of the line. KeyPress::Ctrl('P') => Cmd::PreviousHistory, KeyPress::Ctrl('R') => { self.input_mode = InputMode::Insert; // TODO Validate Cmd::ReverseSearchHistory } KeyPress::Ctrl('S') => { self.input_mode = InputMode::Insert; // TODO Validate Cmd::ForwardSearchHistory } KeyPress::Esc => Cmd::Noop, _ => self.common(rdr, key, n, true)?, }; debug!(target: "rustyline", "Vi command: {:?}", cmd); if cmd.is_repeatable_change() { self.last_cmd = cmd.clone(); } Ok(cmd) } fn vi_insert( &mut self, rdr: &mut R, wrt: &mut dyn Refresher, key: KeyPress, ) -> Result { { let bindings = self.custom_bindings.read().unwrap(); if let Some(cmd) = bindings.get(&key) { debug!(target: "rustyline", "Custom command: {:?}", cmd); return Ok(if cmd.is_repeatable() { cmd.redo(None, wrt) } else { cmd.clone() }); } } let cmd = match key { KeyPress::Char(c) => { if self.input_mode == InputMode::Replace { Cmd::Overwrite(c) } else { Cmd::SelfInsert(1, c) } } KeyPress::Ctrl('H') | KeyPress::Backspace => Cmd::Kill(Movement::BackwardChar(1)), KeyPress::BackTab => Cmd::CompleteBackward, KeyPress::Tab => Cmd::Complete, // Don't complete hints when the cursor is not at the end of a line KeyPress::Right if wrt.has_hint() && wrt.is_cursor_at_end() => Cmd::CompleteHint, KeyPress::Meta(k) => { debug!(target: "rustyline", "Vi fast command mode: {}", k); self.input_mode = InputMode::Command; wrt.done_inserting(); self.vi_command(rdr, wrt, KeyPress::Char(k))? } KeyPress::Esc => { // vi-movement-mode/vi-command-mode self.input_mode = InputMode::Command; wrt.done_inserting(); Cmd::Move(Movement::BackwardChar(1)) } _ => self.common(rdr, key, 1, true)?, }; debug!(target: "rustyline", "Vi insert: {:?}", cmd); if cmd.is_repeatable_change() { if let (Cmd::Replace(..), Cmd::SelfInsert(..)) = (&self.last_cmd, &cmd) { // replacing... } else if let (Cmd::SelfInsert(..), Cmd::SelfInsert(..)) = (&self.last_cmd, &cmd) { // inserting... } else { self.last_cmd = cmd.clone(); } } Ok(cmd) } fn vi_cmd_motion( &mut self, rdr: &mut R, wrt: &mut dyn Refresher, key: KeyPress, n: RepeatCount, ) -> Result> { let mut mvt = rdr.next_key(false)?; if mvt == key { return Ok(Some(Movement::WholeLine)); } let mut n = n; if let KeyPress::Char(digit @ '1'..='9') = mvt { // vi-arg-digit mvt = self.vi_arg_digit(rdr, wrt, digit)?; n = self.vi_num_args().saturating_mul(n); } Ok(match mvt { KeyPress::Char('$') => Some(Movement::EndOfLine), KeyPress::Char('0') => Some(Movement::BeginningOfLine), KeyPress::Char('^') => Some(Movement::ViFirstPrint), KeyPress::Char('b') => Some(Movement::BackwardWord(n, Word::Vi)), KeyPress::Char('B') => Some(Movement::BackwardWord(n, Word::Big)), KeyPress::Char('e') => Some(Movement::ForwardWord(n, At::AfterEnd, Word::Vi)), KeyPress::Char('E') => Some(Movement::ForwardWord(n, At::AfterEnd, Word::Big)), KeyPress::Char(c) if c == 'f' || c == 'F' || c == 't' || c == 'T' => { let cs = self.vi_char_search(rdr, c)?; match cs { Some(cs) => Some(Movement::ViCharSearch(n, cs)), None => None, } } KeyPress::Char(';') => match self.last_char_search { Some(cs) => Some(Movement::ViCharSearch(n, cs)), None => None, }, KeyPress::Char(',') => match self.last_char_search { Some(ref cs) => Some(Movement::ViCharSearch(n, cs.opposite())), None => None, }, KeyPress::Char('h') | KeyPress::Ctrl('H') | KeyPress::Backspace => { Some(Movement::BackwardChar(n)) } KeyPress::Char('l') | KeyPress::Char(' ') => Some(Movement::ForwardChar(n)), KeyPress::Char('j') | KeyPress::Char('+') => Some(Movement::LineDown(n)), KeyPress::Char('k') | KeyPress::Char('-') => Some(Movement::LineUp(n)), KeyPress::Char('w') => { // 'cw' is 'ce' if key == KeyPress::Char('c') { Some(Movement::ForwardWord(n, At::AfterEnd, Word::Vi)) } else { Some(Movement::ForwardWord(n, At::Start, Word::Vi)) } } KeyPress::Char('W') => { // 'cW' is 'cE' if key == KeyPress::Char('c') { Some(Movement::ForwardWord(n, At::AfterEnd, Word::Big)) } else { Some(Movement::ForwardWord(n, At::Start, Word::Big)) } } _ => None, }) } fn vi_char_search( &mut self, rdr: &mut R, cmd: char, ) -> Result> { let ch = rdr.next_key(false)?; Ok(match ch { KeyPress::Char(ch) => { let cs = match cmd { 'f' => CharSearch::Forward(ch), 't' => CharSearch::ForwardBefore(ch), 'F' => CharSearch::Backward(ch), 'T' => CharSearch::BackwardAfter(ch), _ => unreachable!(), }; self.last_char_search = Some(cs); Some(cs) } _ => None, }) } fn common( &mut self, rdr: &mut R, key: KeyPress, n: RepeatCount, positive: bool, ) -> Result { Ok(match key { KeyPress::Home => Cmd::Move(Movement::BeginningOfLine), KeyPress::Left => { if positive { Cmd::Move(Movement::BackwardChar(n)) } else { Cmd::Move(Movement::ForwardChar(n)) } } KeyPress::Ctrl('C') => Cmd::Interrupt, KeyPress::Ctrl('D') => Cmd::EndOfFile, KeyPress::Delete => { if positive { Cmd::Kill(Movement::ForwardChar(n)) } else { Cmd::Kill(Movement::BackwardChar(n)) } } KeyPress::End => Cmd::Move(Movement::EndOfLine), KeyPress::Right => { if positive { Cmd::Move(Movement::ForwardChar(n)) } else { Cmd::Move(Movement::BackwardChar(n)) } } KeyPress::Ctrl('J') | KeyPress::Enter => Cmd::AcceptLine, KeyPress::Down => Cmd::LineDownOrNextHistory(1), KeyPress::Up => Cmd::LineUpOrPreviousHistory(1), KeyPress::Ctrl('R') => Cmd::ReverseSearchHistory, KeyPress::Ctrl('S') => Cmd::ForwardSearchHistory, // most terminals override Ctrl+S to suspend execution KeyPress::Ctrl('T') => Cmd::TransposeChars, KeyPress::Ctrl('U') => { if positive { Cmd::Kill(Movement::BeginningOfLine) } else { Cmd::Kill(Movement::EndOfLine) } }, KeyPress::Ctrl('Q') | // most terminals override Ctrl+Q to resume execution KeyPress::Ctrl('V') => Cmd::QuotedInsert, KeyPress::Ctrl('W') => { if positive { Cmd::Kill(Movement::BackwardWord(n, Word::Big)) } else { Cmd::Kill(Movement::ForwardWord(n, At::AfterEnd, Word::Big)) } } KeyPress::Ctrl('Y') => { if positive { Cmd::Yank(n, Anchor::Before) } else { Cmd::Unknown // TODO Validate } } KeyPress::Ctrl('Z') => Cmd::Suspend, KeyPress::Ctrl('_') => Cmd::Undo(n), KeyPress::UnknownEscSeq => Cmd::Noop, KeyPress::BracketedPasteStart => { let paste = rdr.read_pasted_text()?; Cmd::Insert(1, paste) }, _ => Cmd::Unknown, }) } fn num_args(&mut self) -> i16 { let num_args = match self.num_args { 0 => 1, _ => self.num_args, }; self.num_args = 0; num_args } #[allow(clippy::cast_sign_loss)] fn emacs_num_args(&mut self) -> (RepeatCount, bool) { let num_args = self.num_args(); if num_args < 0 { if let (n, false) = num_args.overflowing_abs() { (n as RepeatCount, false) } else { (RepeatCount::max_value(), false) } } else { (num_args as RepeatCount, true) } } #[allow(clippy::cast_sign_loss)] fn vi_num_args(&mut self) -> RepeatCount { let num_args = self.num_args(); if num_args < 0 { unreachable!() } else { num_args.abs() as RepeatCount } } } rustyline-6.3.0/src/keys.rs010064400007650000024000000056711366145270200140640ustar 00000000000000//! Key constants /// Input key pressed #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum KeyPress { /// Unsupported escape sequence (on unix platform) UnknownEscSeq, /// ⌫ or `KeyPress::Ctrl('H')` Backspace, /// ⇤ (usually Shift-Tab) BackTab, /// Paste (on unix platform) BracketedPasteStart, /// Paste (on unix platform) BracketedPasteEnd, /// Single char Char(char), /// Ctrl-↓ ControlDown, /// Ctrl-← ControlLeft, /// Ctrl-→ ControlRight, /// Ctrl-↑ ControlUp, /// Ctrl-char Ctrl(char), /// ⌦ Delete, /// ↓ arrow key Down, /// ⇲ End, /// ↵ or `KeyPress::Ctrl('M')` Enter, /// Escape or `KeyPress::Ctrl('[')` Esc, /// Function key F(u8), /// ⇱ Home, /// Insert key Insert, /// ← arrow key Left, /// Escape-char or Alt-char Meta(char), /// `KeyPress::Char('\0')` Null, /// ⇟ PageDown, /// ⇞ PageUp, /// → arrow key Right, /// Shift-↓ ShiftDown, /// Shift-← ShiftLeft, /// Shift-→ ShiftRight, /// Shift-↑ ShiftUp, /// ⇥ or `KeyPress::Ctrl('I')` Tab, /// ↑ arrow key Up, } #[cfg(any(windows, unix))] pub fn char_to_key_press(c: char) -> KeyPress { if !c.is_control() { return KeyPress::Char(c); } #[allow(clippy::match_same_arms)] match c { '\x00' => KeyPress::Ctrl(' '), '\x01' => KeyPress::Ctrl('A'), '\x02' => KeyPress::Ctrl('B'), '\x03' => KeyPress::Ctrl('C'), '\x04' => KeyPress::Ctrl('D'), '\x05' => KeyPress::Ctrl('E'), '\x06' => KeyPress::Ctrl('F'), '\x07' => KeyPress::Ctrl('G'), '\x08' => KeyPress::Backspace, // '\b' '\x09' => KeyPress::Tab, // '\t' '\x0a' => KeyPress::Ctrl('J'), // '\n' (10) '\x0b' => KeyPress::Ctrl('K'), '\x0c' => KeyPress::Ctrl('L'), '\x0d' => KeyPress::Enter, // '\r' (13) '\x0e' => KeyPress::Ctrl('N'), '\x0f' => KeyPress::Ctrl('O'), '\x10' => KeyPress::Ctrl('P'), '\x12' => KeyPress::Ctrl('R'), '\x13' => KeyPress::Ctrl('S'), '\x14' => KeyPress::Ctrl('T'), '\x15' => KeyPress::Ctrl('U'), '\x16' => KeyPress::Ctrl('V'), '\x17' => KeyPress::Ctrl('W'), '\x18' => KeyPress::Ctrl('X'), '\x19' => KeyPress::Ctrl('Y'), '\x1a' => KeyPress::Ctrl('Z'), '\x1b' => KeyPress::Esc, // Ctrl-[ '\x1c' => KeyPress::Ctrl('\\'), '\x1d' => KeyPress::Ctrl(']'), '\x1e' => KeyPress::Ctrl('^'), '\x1f' => KeyPress::Ctrl('_'), '\x7f' => KeyPress::Backspace, // Rubout _ => KeyPress::Null, } } #[cfg(test)] mod tests { use super::{char_to_key_press, KeyPress}; #[test] fn char_to_key() { assert_eq!(KeyPress::Esc, char_to_key_press('\x1b')); } } rustyline-6.3.0/src/kill_ring.rs010064400007650000024000000154621346010623700150560ustar 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-6.3.0/src/layout.rs010064400007650000024000000016201365034366500144210ustar 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-6.3.0/src/lib.rs010064400007650000024000001007011372733102500136420ustar 00000000000000//! Readline for Rust //! //! This implementation is based on [Antirez's //! Linenoise](https://github.com/antirez/linenoise) //! //! # Example //! //! Usage //! //! ``` //! let mut rl = rustyline::Editor::<()>::new(); //! let readline = rl.readline(">> "); //! match readline { //! Ok(line) => println!("Line: {:?}", line), //! Err(_) => println!("No input"), //! } //! ``` #![warn(missing_docs)] pub mod completion; pub mod config; mod edit; pub mod error; pub mod highlight; pub mod hint; pub mod history; mod keymap; mod keys; mod kill_ring; mod layout; pub mod line_buffer; mod tty; mod undo; pub mod validate; use std::collections::HashMap; use std::fmt; use std::io::{self, Write}; use std::path::Path; use std::result; use std::sync::{Arc, Mutex, RwLock}; use log::debug; use unicode_width::UnicodeWidthStr; use crate::tty::{RawMode, Renderer, Term, Terminal}; use crate::completion::{longest_common_prefix, Candidate, Completer}; pub use crate::config::{ ColorMode, CompletionType, Config, EditMode, HistoryDuplicates, OutputStreamType, }; use crate::edit::State; use crate::highlight::Highlighter; use crate::hint::Hinter; use crate::history::{Direction, History}; pub use crate::keymap::{Anchor, At, CharSearch, Cmd, Movement, RepeatCount, Word}; use crate::keymap::{InputState, Refresher}; pub use crate::keys::KeyPress; use crate::kill_ring::{KillRing, Mode}; use crate::line_buffer::WordAction; use crate::validate::Validator; /// The error type for I/O and Linux Syscalls (Errno) pub type Result = result::Result; /// Completes the line/word fn complete_line( rdr: &mut ::Reader, s: &mut State<'_, '_, H>, input_state: &mut InputState, config: &Config, ) -> Result> { #[cfg(all(unix, feature = "with-fuzzy"))] use skim::{Skim, 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.borrow_mut().begin(); // Save the current edited line before overwriting it let backup = s.line.as_str().to_owned(); let backup_pos = s.line.pos(); let mut cmd; let mut i = 0; loop { // Show completion or original buffer if i < candidates.len() { let candidate = candidates[i].replacement(); // TODO we can't highlight the line buffer directly /*let candidate = if let Some(highlighter) = s.highlighter { highlighter.highlight_candidate(candidate, CompletionType::Circular) } else { Borrowed(candidate) };*/ completer.update(&mut s.line, start, candidate); s.refresh_line()?; } else { // Restore current edited line s.line.update(&backup, backup_pos); s.refresh_line()?; } cmd = s.next_cmd(input_state, rdr, true)?; match cmd { Cmd::Complete => { i = (i + 1) % (candidates.len() + 1); // Circular if i == candidates.len() { s.out.beep()?; } } Cmd::CompleteBackward => { if i == 0 { i = candidates.len(); // Circular s.out.beep()?; } else { i = (i - 1) % (candidates.len() + 1); // Circular } } Cmd::Abort => { // Re-show original buffer if i < candidates.len() { s.line.update(&backup, backup_pos); s.refresh_line()?; } s.changes.borrow_mut().truncate(mark); return Ok(None); } _ => { s.changes.borrow_mut().end(); break; } } } Ok(Some(cmd)) } else if CompletionType::List == config.completion_type() { if let Some(lcp) = longest_common_prefix(&candidates) { // if we can extend the item, extend it if lcp.len() > s.line.pos() - start { completer.update(&mut s.line, start, lcp); s.refresh_line()?; } } // beep if ambiguous if candidates.len() > 1 { s.out.beep()?; } else { return Ok(None); } // we can't complete any further, wait for second tab let mut cmd = s.next_cmd(input_state, rdr, true)?; // if any character other than tab, pass it to the main loop if cmd != Cmd::Complete { return Ok(Some(cmd)); } // move cursor to EOL to avoid overwriting the command line let save_pos = s.line.pos(); s.edit_move_end()?; s.line.set_pos(save_pos); // we got a second tab, maybe show list of possible completions let show_completions = if candidates.len() > config.completion_prompt_limit() { let msg = format!("\nDisplay all {} possibilities? (y or n)", candidates.len()); s.out.write_and_flush(msg.as_bytes())?; s.layout.end.row += 1; while cmd != Cmd::SelfInsert(1, 'y') && cmd != Cmd::SelfInsert(1, 'Y') && cmd != Cmd::SelfInsert(1, 'n') && cmd != Cmd::SelfInsert(1, 'N') && cmd != Cmd::Kill(Movement::BackwardChar(1)) { cmd = s.next_cmd(input_state, rdr, false)?; } match cmd { Cmd::SelfInsert(1, 'y') | Cmd::SelfInsert(1, 'Y') => true, _ => false, } } else { true }; if show_completions { page_completions(rdr, s, input_state, &candidates) } else { s.refresh_line()?; Ok(None) } } else { // if fuzzy feature is enabled and on unix based systems check for the // corresponding completion_type #[cfg(all(unix, feature = "with-fuzzy"))] { if CompletionType::Fuzzy == config.completion_type() { // skim takes input of candidates separated by new line let input = candidates .iter() .map(|c| c.display()) .collect::>() .join("\n"); // setup skim and run with input options // will display UI for fuzzy search and return selected results // by default skim multi select is off so only expect one selection let options = SkimOptionsBuilder::default() .height(Some("20%")) .prompt(Some("? ")) .reverse(true) .build() .unwrap(); let selected_items = Skim::run_with(&options, Some(Box::new(std::io::Cursor::new(input)))) .map(|out| out.selected_items) .unwrap_or_else(Vec::new); // match the first (and only) returned option with the candidate and update the // line otherwise only refresh line to clear the skim UI changes if let Some(item) = selected_items.first() { if let Some(candidate) = candidates.get(item.get_index()) { completer.update(&mut s.line, start, candidate.replacement()); } } 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 s.line.yank(hint, 1).is_none() { s.out.beep()?; } s.refresh_line_with_msg(None)?; Ok(()) } fn page_completions( rdr: &mut ::Reader, s: &mut State<'_, '_, H>, input_state: &mut InputState, candidates: &[C], ) -> Result> { use std::cmp; let min_col_pad = 2; let cols = s.out.get_columns(); let max_width = cmp::min( cols, candidates .iter() .map(|s| s.display().width()) .max() .unwrap() + min_col_pad, ); let num_cols = cols / max_width; let mut pause_row = s.out.get_rows() - 1; let num_rows = (candidates.len() + num_cols - 1) / num_cols; let mut ab = String::new(); for row in 0..num_rows { if row == pause_row { s.out.write_and_flush(b"\n--More--")?; let mut cmd = Cmd::Noop; while cmd != Cmd::SelfInsert(1, 'y') && cmd != Cmd::SelfInsert(1, 'Y') && cmd != Cmd::SelfInsert(1, 'n') && cmd != Cmd::SelfInsert(1, 'N') && cmd != Cmd::SelfInsert(1, 'q') && cmd != Cmd::SelfInsert(1, 'Q') && cmd != Cmd::SelfInsert(1, ' ') && cmd != Cmd::Kill(Movement::BackwardChar(1)) && cmd != Cmd::AcceptLine && cmd != Cmd::AcceptOrInsertLine { cmd = s.next_cmd(input_state, rdr, false)?; } match cmd { Cmd::SelfInsert(1, 'y') | Cmd::SelfInsert(1, 'Y') | Cmd::SelfInsert(1, ' ') => { pause_row += s.out.get_rows() - 1; } Cmd::AcceptLine | Cmd::AcceptOrInsertLine => { pause_row += 1; } _ => break, } s.out.write_and_flush(b"\n")?; } else { s.out.write_and_flush(b"\n")?; } ab.clear(); for col in 0..num_cols { let i = (col * num_rows) + row; if i < candidates.len() { let candidate = &candidates[i].display(); let width = candidate.width(); if let Some(highlighter) = s.highlighter() { ab.push_str(&highlighter.highlight_candidate(candidate, CompletionType::List)); } else { ab.push_str(candidate); } if ((col + 1) * num_rows) + row < candidates.len() { for _ in width..max_width { ab.push(' '); } } } } s.out.write_and_flush(ab.as_bytes())?; } s.out.write_and_flush(b"\n")?; s.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: &History, ) -> Result> { if history.is_empty() { return Ok(None); } let mark = s.changes.borrow_mut().begin(); // Save the current edited line (and cursor position) before overwriting it let backup = s.line.as_str().to_owned(); let backup_pos = s.line.pos(); let mut search_buf = String::new(); let mut history_idx = history.len() - 1; let mut direction = Direction::Reverse; let mut success = true; let mut cmd; // Display the reverse-i-search prompt and process chars loop { let prompt = if success { format!("(reverse-i-search)`{}': ", search_buf) } else { format!("(failed reverse-i-search)`{}': ", search_buf) }; s.refresh_prompt_and_line(&prompt)?; cmd = s.next_cmd(input_state, rdr, true)?; if let Cmd::SelfInsert(_, c) = cmd { search_buf.push(c); } else { match cmd { Cmd::Kill(Movement::BackwardChar(_)) => { search_buf.pop(); continue; } Cmd::ReverseSearchHistory => { direction = Direction::Reverse; if history_idx > 0 { history_idx -= 1; } else { success = false; continue; } } Cmd::ForwardSearchHistory => { direction = Direction::Forward; if history_idx < history.len() - 1 { history_idx += 1; } else { success = false; continue; } } Cmd::Abort => { // Restore current edited line (before search) s.line.update(&backup, backup_pos); s.refresh_line()?; s.changes.borrow_mut().truncate(mark); return Ok(None); } Cmd::Move(_) => { s.refresh_line()?; // restore prompt break; } _ => break, } } success = match history.search(&search_buf, history_idx, direction) { Some(idx) => { history_idx = idx; let entry = history.get(idx).unwrap(); let pos = entry.find(&search_buf).unwrap(); s.line.update(entry, pos); true } _ => false, }; } s.changes.borrow_mut().end(); Ok(Some(cmd)) } /// Handles reading and editing the readline buffer. /// It will also handle special inputs in an appropriate fashion /// (e.g., C-c will exit readline) fn readline_edit( prompt: &str, initial: Option<(&str, &str)>, editor: &mut Editor, original_mode: &tty::Mode, ) -> Result { let helper = editor.helper.as_ref(); let mut stdout = editor.term.create_writer(); editor.reset_kill_ring(); // TODO recreate a new kill ring vs Arc> let ctx = Context::new(&editor.history); let mut s = State::new(&mut stdout, prompt, helper, ctx); let mut input_state = InputState::new(&editor.config, Arc::clone(&editor.custom_bindings)); s.line.set_delete_listener(editor.kill_ring.clone()); s.line.set_change_listener(s.changes.clone()); if let Some((left, right)) = initial { s.line .update((left.to_owned() + right).as_ref(), left.len()); } let mut rdr = editor.term.create_reader(&editor.config)?; if editor.term.is_output_tty() { if let Err(e) = s.move_cursor_at_leftmost(&mut rdr) { if s.out.sigwinch() { s.out.update_size(); } else { return Err(e); } } } s.refresh_line()?; loop { let rc = s.next_cmd(&mut input_state, &mut rdr, false); let mut cmd = rc?; if cmd.should_reset_kill_ring() { editor.reset_kill_ring(); } // autocomplete if cmd == Cmd::Complete && s.helper.is_some() { let next = complete_line(&mut rdr, &mut s, &mut input_state, &editor.config)?; if let Some(next) = next { cmd = next; } else { continue; } } if Cmd::CompleteHint == cmd { complete_hint_line(&mut s)?; continue; } if let Cmd::SelfInsert(n, c) = cmd { s.edit_insert(c, n)?; continue; } else if let Cmd::Insert(n, text) = cmd { s.edit_yank(&input_state, &text, Anchor::Before, n)?; continue; } if cmd == Cmd::ReverseSearchHistory { // Search history backward let next = reverse_incremental_search(&mut rdr, &mut s, &mut input_state, &editor.history)?; if let Some(next) = next { cmd = next; } else { continue; } } match cmd { Cmd::Move(Movement::BeginningOfLine) => { // Move to the beginning of line. s.edit_move_home()? } Cmd::Move(Movement::ViFirstPrint) => { s.edit_move_home()?; s.edit_move_to_next_word(At::Start, Word::Big, 1)? } Cmd::Move(Movement::BackwardChar(n)) => { // Move back a character. s.edit_move_backward(n)? } Cmd::ReplaceChar(n, c) => s.edit_replace_char(c, n)?, Cmd::Replace(mvt, text) => { s.edit_kill(&mvt)?; if let Some(text) = text { s.edit_insert_text(&text)? } } Cmd::Overwrite(c) => { s.edit_overwrite_char(c)?; } Cmd::EndOfFile => { if !input_state.is_emacs_mode() && !s.line.is_empty() { s.edit_move_end()?; break; } else if s.line.is_empty() { return Err(error::ReadlineError::Eof); } else { s.edit_delete(1)? } } Cmd::Move(Movement::EndOfLine) => { // Move to the end of line. s.edit_move_end()? } Cmd::Move(Movement::ForwardChar(n)) => { // Move forward a character. s.edit_move_forward(n)? } Cmd::ClearScreen => { // Clear the screen leaving the current line at the top of the screen. s.clear_screen()?; s.refresh_line()? } Cmd::NextHistory => { // Fetch the next command from the history list. s.edit_history_next(false)? } Cmd::PreviousHistory => { // Fetch the previous command from the history list. s.edit_history_next(true)? } Cmd::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(Direction::Reverse)?, Cmd::HistorySearchForward => s.edit_history_search(Direction::Forward)?, Cmd::TransposeChars => { // Exchange the char before cursor with the character at cursor. s.edit_transpose_chars()? } #[cfg(unix)] Cmd::QuotedInsert => { // Quoted insert use tty::RawReader; let c = rdr.next_char()?; s.edit_insert(c, 1)? } Cmd::Yank(n, anchor) => { // retrieve (yank) last item killed let mut kill_ring = editor.kill_ring.lock().unwrap(); if let Some(text) = kill_ring.yank() { s.edit_yank(&input_state, text, anchor, n)? } } Cmd::ViYankTo(ref mvt) => { if let Some(text) = s.line.copy(mvt) { let mut kill_ring = editor.kill_ring.lock().unwrap(); kill_ring.kill(&text, Mode::Append) } } Cmd::AcceptLine | Cmd::AcceptOrInsertLine => { #[cfg(test)] { editor.term.cursor = s.layout.cursor.col; } if s.has_hint() || !s.is_default_prompt() { // Force a refresh without hints to leave the previous // line as the user typed it after a newline. s.refresh_line_with_msg(None)?; } // Only accept value if cursor is at the end of the buffer if s.validate()? && (cmd == Cmd::AcceptLine || s.line.is_end_of_input()) { break; } else { s.edit_insert('\n', 1)?; } continue; } Cmd::BeginningOfHistory => { // move to first entry in history s.edit_history(true)? } Cmd::EndOfHistory => { // move to last entry in history s.edit_history(false)? } Cmd::Move(Movement::BackwardWord(n, word_def)) => { // move backwards one word s.edit_move_to_prev_word(word_def, n)? } Cmd::CapitalizeWord => { // capitalize word after point s.edit_word(WordAction::CAPITALIZE)? } Cmd::Kill(ref mvt) => { s.edit_kill(mvt)?; } Cmd::Move(Movement::ForwardWord(n, at, word_def)) => { // move forwards one word s.edit_move_to_next_word(at, word_def, n)? } Cmd::Move(Movement::LineUp(n)) => { s.edit_move_line_up(n)?; } Cmd::Move(Movement::LineDown(n)) => { s.edit_move_line_down(n)?; } Cmd::Move(Movement::BeginningOfBuffer) => { // Move to the start of the buffer. s.edit_move_buffer_start()? } Cmd::Move(Movement::EndOfBuffer) => { // Move to the end of the buffer. s.edit_move_buffer_end()? } Cmd::DowncaseWord => { // lowercase word after point s.edit_word(WordAction::LOWERCASE)? } Cmd::TransposeWords(n) => { // transpose words s.edit_transpose_words(n)? } Cmd::UpcaseWord => { // uppercase word after point s.edit_word(WordAction::UPPERCASE)? } Cmd::YankPop => { // yank-pop let mut kill_ring = editor.kill_ring.lock().unwrap(); if let Some((yank_size, text)) = kill_ring.yank_pop() { s.edit_yank_pop(yank_size, text)? } } Cmd::Move(Movement::ViCharSearch(n, cs)) => s.edit_move_to(cs, n)?, Cmd::Undo(n) => { if s.changes.borrow_mut().undo(&mut s.line, n) { s.refresh_line()?; } } Cmd::Interrupt => { return Err(error::ReadlineError::Interrupted); } #[cfg(unix)] Cmd::Suspend => { original_mode.disable_raw_mode()?; tty::suspend()?; editor.term.enable_raw_mode()?; // TODO original_mode may have changed s.refresh_line()?; continue; } _ => { // Ignore the character typed. } } } if cfg!(windows) { let _ = original_mode; // silent warning } Ok(s.line.into_string()) } struct Guard<'m>(&'m tty::Mode); #[allow(unused_must_use)] impl Drop for Guard<'_> { fn drop(&mut self) { let Guard(mode) = *self; mode.disable_raw_mode(); } } /// Readline method that will enable RAW mode, call the `readline_edit()` /// method and disable raw mode fn readline_raw( prompt: &str, initial: Option<(&str, &str)>, editor: &mut Editor, ) -> Result { let original_mode = editor.term.enable_raw_mode()?; let guard = Guard(&original_mode); let user_input = readline_edit(prompt, initial, editor, &original_mode); if editor.config.auto_add_history() { if let Ok(ref line) = user_input { editor.add_history_entry(line.as_str()); } } drop(guard); // disable_raw_mode(original_mode)?; match editor.config.output_stream() { OutputStreamType::Stdout => writeln!(io::stdout())?, OutputStreamType::Stderr => writeln!(io::stderr())?, }; user_input } fn readline_direct() -> Result { let mut line = String::new(); if io::stdin().read_line(&mut line)? > 0 { Ok(line) } else { Err(error::ReadlineError::Eof) } } /// Syntax specific helper. /// /// TODO Tokenizer/parser used for both completion, suggestion, highlighting. /// (parse current line once) pub trait Helper where Self: Completer + Hinter + Highlighter + Validator, { } impl Helper for () {} impl<'h, H: ?Sized + Helper> Helper for &'h H {} /// Completion/suggestion context pub struct Context<'h> { history: &'h History, history_index: usize, } impl<'h> Context<'h> { /// Constructor. Visible for testing. pub fn new(history: &'h History) -> Self { Context { history, history_index: history.len(), } } /// Return an immutable reference to the history object. pub fn history(&self) -> &History { &self.history } /// The history index we are currently editing pub fn history_index(&self) -> usize { self.history_index } } /// Line editor pub struct Editor { term: Terminal, history: History, helper: Option, kill_ring: Arc>, config: Config, custom_bindings: Arc>>, } #[allow(clippy::new_without_default)] impl Editor { /// Create an editor with the default configuration pub fn new() -> Self { Self::with_config(Config::default()) } /// Create an editor with a specific configuration. pub fn with_config(config: Config) -> Self { let term = Terminal::new( config.color_mode(), config.output_stream(), config.tab_stop(), config.bell_style(), ); Self { term, history: History::with_config(config), helper: None, kill_ring: Arc::new(Mutex::new(KillRing::new(60))), config, custom_bindings: Arc::new(RwLock::new(HashMap::new())), } } /// This method will read a line from STDIN and will display a `prompt`. /// /// It uses terminal-style interaction if `stdin` is connected to a /// terminal. /// Otherwise (e.g., if `stdin` is a pipe or the terminal is not supported), /// it uses file-style interaction. pub fn readline(&mut self, prompt: &str) -> Result { self.readline_with(prompt, None) } /// This function behaves in the exact same manner as `readline`, except /// that it pre-populates the input area. /// /// The text that resides in the input area is given as a 2-tuple. /// The string on the left of the tuple is what will appear to the left of /// the cursor and the string on the right is what will appear to the /// right of the cursor. pub fn readline_with_initial(&mut self, prompt: &str, initial: (&str, &str)) -> Result { self.readline_with(prompt, Some(initial)) } fn readline_with(&mut self, prompt: &str, initial: Option<(&str, &str)>) -> Result { if self.term.is_unsupported() { debug!(target: "rustyline", "unsupported terminal"); // Write prompt and flush it to stdout let mut stdout = io::stdout(); stdout.write_all(prompt.as_bytes())?; stdout.flush()?; readline_direct() } else if self.term.is_stdin_tty() { readline_raw(prompt, initial, self) } else { debug!(target: "rustyline", "stdin is not a tty"); // Not a tty: read from file / pipe. readline_direct() } } /// Load the history from the specified file. pub fn load_history + ?Sized>(&mut self, path: &P) -> Result<()> { self.history.load(path) } /// Save the history in the specified file. pub fn save_history + ?Sized>(&self, path: &P) -> Result<()> { self.history.save(path) } /// Add a new entry in the history. pub fn add_history_entry + Into>(&mut self, line: S) -> bool { self.history.add(line) } /// Clear history. pub fn clear_history(&mut self) { self.history.clear() } /// Return a mutable reference to the history object. pub fn history_mut(&mut self) -> &mut History { &mut self.history } /// Return an immutable reference to the history object. pub fn history(&self) -> &History { &self.history } /// Register a callback function to be called for tab-completion /// or to show hints to the user at the right of the prompt. pub fn set_helper(&mut self, helper: Option) { self.helper = helper; } /// Return a mutable reference to the helper. pub fn helper_mut(&mut self) -> Option<&mut H> { self.helper.as_mut() } /// Return an immutable reference to the helper. pub fn helper(&self) -> Option<&H> { self.helper.as_ref() } /// Bind a sequence to a command. pub fn bind_sequence(&mut self, key_seq: KeyPress, cmd: Cmd) -> Option { if let Ok(mut bindings) = self.custom_bindings.write() { bindings.insert(key_seq, cmd) } else { None } } /// Remove a binding for the given sequence. pub fn unbind_sequence(&mut self, key_seq: KeyPress) -> Option { if let Ok(mut bindings) = self.custom_bindings.write() { bindings.remove(&key_seq) } else { None } } /// Returns an iterator over edited lines /// ``` /// let mut rl = rustyline::Editor::<()>::new(); /// for readline in rl.iter("> ") { /// match readline { /// Ok(line) => { /// println!("Line: {}", line); /// } /// Err(err) => { /// println!("Error: {:?}", err); /// break; /// } /// } /// } /// ``` pub fn iter<'a>(&'a mut self, prompt: &'a str) -> impl Iterator> + 'a { Iter { editor: self, prompt, } } fn reset_kill_ring(&self) { let mut kill_ring = self.kill_ring.lock().unwrap(); kill_ring.reset(); } /// If output stream is a tty, this function returns its width and height as /// a number of characters. pub fn dimensions(&mut self) -> Option<(usize, usize)> { if self.term.is_output_tty() { let out = self.term.create_writer(); Some((out.get_columns(), out.get_rows())) } else { None } } } impl config::Configurer for Editor { fn config_mut(&mut self) -> &mut Config { &mut self.config } fn set_max_history_size(&mut self, max_size: usize) { self.config_mut().set_max_history_size(max_size); self.history.set_max_len(max_size); } fn set_history_ignore_dups(&mut self, yes: bool) { self.config_mut().set_history_ignore_dups(yes); self.history.ignore_dups = yes; } fn set_history_ignore_space(&mut self, yes: bool) { self.config_mut().set_history_ignore_space(yes); self.history.ignore_space = yes; } fn set_color_mode(&mut self, color_mode: ColorMode) { self.config_mut().set_color_mode(color_mode); self.term.color_mode = color_mode; } } impl fmt::Debug for Editor { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Editor") .field("term", &self.term) .field("config", &self.config) .finish() } } struct Iter<'a, H: Helper> { editor: &'a mut Editor, prompt: &'a str, } impl<'a, H: Helper> Iterator for Iter<'a, H> { type Item = Result; fn next(&mut self) -> Option> { let readline = self.editor.readline(self.prompt); match readline { Ok(l) => Some(Ok(l)), Err(error::ReadlineError::Eof) => None, e @ Err(_) => Some(e), } } } #[cfg(test)] #[macro_use] extern crate assert_matches; #[cfg(test)] mod test; #[cfg(doctest)] doc_comment::doctest!("../README.md"); rustyline-6.3.0/src/line_buffer.rs010064400007650000024000001611501366145270200153640ustar 00000000000000//! Line buffer with current cursor position use crate::keymap::{At, CharSearch, Movement, RepeatCount, Word}; use std::cell::RefCell; use std::fmt; use std::iter; use std::ops::{Deref, Index, Range}; use std::rc::Rc; use std::string::Drain; use std::sync::{Arc, Mutex}; use unicode_segmentation::UnicodeSegmentation; /// Default maximum buffer size for the line read pub(crate) const MAX_LINE: usize = 4096; /// Word's case change #[derive(Clone, Copy)] pub enum WordAction { /// Capitalize word CAPITALIZE, /// lowercase word LOWERCASE, /// uppercase word UPPERCASE, } /// Delete (kill) direction #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum Direction { Forward, Backward, } impl Default for Direction { fn default() -> Self { Direction::Forward } } /// Listener to be notified when some text is deleted. pub(crate) trait DeleteListener { fn start_killing(&mut self); fn delete(&mut self, idx: usize, string: &str, dir: Direction); fn stop_killing(&mut self); } /// Listener to be notified when the line is modified. pub(crate) trait ChangeListener: DeleteListener { fn insert_char(&mut self, idx: usize, c: char); fn insert_str(&mut self, idx: usize, string: &str); fn replace(&mut self, idx: usize, old: &str, new: &str); } /// Represent the current input (text and cursor position). /// /// The methods do text manipulations or/and cursor movements. pub struct LineBuffer { buf: String, // Edited line buffer (rl_line_buffer) pos: usize, // Current cursor position (byte position) (rl_point) can_growth: bool, // Whether to allow dynamic growth dl: Option>>, cl: Option>>, } impl fmt::Debug for LineBuffer { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("LineBuffer") .field("buf", &self.buf) .field("pos", &self.pos) .finish() } } impl LineBuffer { /// Create a new line buffer with the given maximum `capacity`. pub fn with_capacity(capacity: usize) -> Self { Self { buf: String::with_capacity(capacity), pos: 0, can_growth: false, dl: None, cl: None, } } /// Set whether to allow dynamic allocation pub(crate) fn can_growth(mut self, can_growth: bool) -> Self { self.can_growth = can_growth; self } fn must_truncate(&self, new_len: usize) -> bool { !self.can_growth && new_len > self.buf.capacity() } #[cfg(test)] pub(crate) fn init( line: &str, pos: usize, cl: Option>>, ) -> Self { let mut lb = Self::with_capacity(MAX_LINE); assert!(lb.insert_str(0, line)); lb.set_pos(pos); lb.cl = cl; lb } pub(crate) fn set_delete_listener(&mut self, dl: Arc>) { self.dl = Some(dl); } pub(crate) fn set_change_listener(&mut self, dl: Rc>) { self.cl = Some(dl); } /// Extracts a string slice containing the entire buffer. pub fn as_str(&self) -> &str { &self.buf } /// Converts a buffer into a `String` without copying or allocating. pub fn into_string(self) -> String { self.buf } /// Current cursor position (byte position) pub fn pos(&self) -> usize { self.pos } /// Set cursor position (byte position) pub fn set_pos(&mut self, pos: usize) { assert!(pos <= self.buf.len()); self.pos = pos; } /// Returns the length of this buffer, in bytes. pub fn len(&self) -> usize { self.buf.len() } /// Returns `true` if this buffer has a length of zero. pub fn is_empty(&self) -> bool { self.buf.is_empty() } /// Set line content (`buf`) and cursor position (`pos`). pub fn update(&mut self, buf: &str, pos: usize) { assert!(pos <= buf.len()); let end = self.len(); self.drain(0..end, Direction::default()); let max = self.buf.capacity(); if self.must_truncate(buf.len()) { self.insert_str(0, &buf[..max]); if pos > max { self.pos = max; } else { self.pos = pos; } } else { self.insert_str(0, buf); self.pos = pos; } } 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. pub fn next_pos(&self, n: RepeatCount) -> Option { if self.pos == self.buf.len() { return None; } self.buf[self.pos..] .grapheme_indices(true) .take(n) .last() .map(|(i, s)| i + self.pos + s.len()) } /// Returns the position of the character just before the current cursor /// position. fn prev_pos(&self, n: RepeatCount) -> Option { if self.pos == 0 { return None; } self.buf[..self.pos] .grapheme_indices(true) .rev() .take(n) .last() .map(|(i, _)| i) } /// Insert the character `ch` at current cursor position /// and advance cursor position accordingly. /// Return `None` when maximum buffer size has been reached, /// `true` when the character has been appended to the end of the line. pub fn insert(&mut self, ch: char, n: RepeatCount) -> Option { let shift = ch.len_utf8() * n; if self.must_truncate(self.buf.len() + shift) { return None; } let push = self.pos == self.buf.len(); if n == 1 { self.buf.insert(self.pos, ch); for cl in &self.cl { if let Ok(mut cl) = cl.try_borrow_mut() { cl.insert_char(self.pos, ch); } // Ok: while undoing, cl is borrowed. And we want to ignore // changes while undoing. } } else { let text = iter::repeat(ch).take(n).collect::(); let pos = self.pos; self.insert_str(pos, &text); } self.pos += shift; Some(push) } /// Yank/paste `text` at current position. /// Return `None` when maximum buffer size has been reached or is empty, /// `true` when the character has been appended to the end of the line. pub fn yank(&mut self, text: &str, n: RepeatCount) -> Option { let shift = text.len() * n; if text.is_empty() || self.must_truncate(self.buf.len() + shift) { return None; } let push = self.pos == self.buf.len(); let pos = self.pos; if n == 1 { self.insert_str(pos, text); } else { let text = iter::repeat(text).take(n).collect::(); self.insert_str(pos, &text); } self.pos += shift; Some(push) } /// Delete previously yanked text and yank/paste `text` at current position. pub fn yank_pop(&mut self, yank_size: usize, text: &str) -> Option { let end = self.pos; let start = end - yank_size; self.drain(start..end, Direction::default()); self.pos -= yank_size; self.yank(text, 1) } /// Move cursor on the left. pub fn move_backward(&mut self, n: RepeatCount) -> bool { match self.prev_pos(n) { Some(pos) => { self.pos = pos; true } None => false, } } /// Move cursor on the right. pub fn move_forward(&mut self, n: RepeatCount) -> bool { match self.next_pos(n) { Some(pos) => { self.pos = pos; true } None => false, } } /// Move cursor to the start of the 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) 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) -> Option { match self.next_pos(n) { Some(pos) => { let start = self.pos; let chars = self .drain(start..pos, Direction::Forward) .collect::(); Some(chars) } None => None, } } /// Delete the character at the left of the cursor. /// Basically that is what happens with the "Backspace" keyboard key. pub fn backspace(&mut self, n: RepeatCount) -> bool { match self.prev_pos(n) { Some(pos) => { let end = self.pos; self.drain(pos..end, Direction::Backward); self.pos = pos; true } None => false, } } /// Kill the text from point to the end of the line. pub fn kill_line(&mut self) -> bool { if !self.buf.is_empty() && self.pos < self.buf.len() { let start = self.pos; let end = self.end_of_line(); if start == end { self.delete(1); } else { self.drain(start..end, Direction::Forward); } true } else { false } } /// Kill the text from point to the end of the buffer. pub fn kill_buffer(&mut self) -> bool { if !self.buf.is_empty() && self.pos < self.buf.len() { let start = self.pos; let end = self.buf.len(); self.drain(start..end, Direction::Forward); true } else { false } } /// Kill backward from point to the beginning of the line. pub fn discard_line(&mut self) -> bool { if self.pos > 0 && !self.buf.is_empty() { let start = self.start_of_line(); let end = self.pos; if end == start { self.backspace(1) } else { self.drain(start..end, Direction::Backward); self.pos = start; true } } else { false } } /// Kill backward from point to the beginning of the buffer. pub fn discard_buffer(&mut self) -> bool { if self.pos > 0 && !self.buf.is_empty() { let end = self.pos; self.drain(0..end, Direction::Backward); self.pos = 0; true } else { false } } /// Exchange the char before cursor with the character at cursor. pub fn transpose_chars(&mut self) -> bool { if self.pos == 0 || self.buf.graphemes(true).count() < 2 { return false; } if self.pos == self.buf.len() { self.move_backward(1); } let chars = self.delete(1).unwrap(); self.move_backward(1); self.yank(&chars, 1); self.move_forward(1); true } /// Go left until start of word fn prev_word_pos(&self, pos: usize, word_def: Word, n: RepeatCount) -> Option { if pos == 0 { return None; } let mut sow = 0; let mut gis = self.buf[..pos].grapheme_indices(true).rev(); 'outer: for _ in 0..n { sow = 0; let mut gj = gis.next(); 'inner: loop { if let Some((j, y)) = gj { let gi = gis.next(); if let Some((_, x)) = gi { if is_start_of_word(word_def, x, y) { sow = j; break 'inner; } gj = gi; } else { break 'outer; } } else { break 'outer; } } } Some(sow) } /// Moves the cursor to the beginning of previous word. pub fn move_to_prev_word(&mut self, word_def: Word, n: RepeatCount) -> bool { if let Some(pos) = self.prev_word_pos(self.pos, word_def, n) { self.pos = pos; true } else { false } } /// Delete the previous word, maintaining the cursor at the start of the /// current word. pub fn delete_prev_word(&mut self, word_def: Word, n: RepeatCount) -> bool { if let Some(pos) = self.prev_word_pos(self.pos, word_def, n) { let end = self.pos; self.drain(pos..end, Direction::Backward); self.pos = pos; true } else { false } } fn next_word_pos(&self, pos: usize, at: At, word_def: Word, n: RepeatCount) -> Option { if pos == self.buf.len() { return None; } let mut wp = 0; let mut gis = self.buf[pos..].grapheme_indices(true); let mut gi = if at == At::BeforeEnd { // TODO Validate gis.next() } else { None }; 'outer: for _ in 0..n { wp = 0; gi = gis.next(); 'inner: loop { if let Some((i, x)) = gi { let gj = gis.next(); if let Some((j, y)) = gj { if at == At::Start && is_start_of_word(word_def, x, y) { wp = j; break 'inner; } else if at != At::Start && is_end_of_word(word_def, x, y) { if word_def == Word::Emacs || at == At::AfterEnd { wp = j; } else { wp = i; } break 'inner; } gi = gj; } else { break 'outer; } } else { break 'outer; } } } if wp == 0 { if word_def == Word::Emacs || at == At::AfterEnd { Some(self.buf.len()) } else { match gi { Some((i, _)) if i != 0 => Some(i + pos), _ => None, } } } else { Some(wp + pos) } } /// Moves the cursor to the end of next word. pub fn move_to_next_word(&mut self, at: At, word_def: Word, n: RepeatCount) -> bool { if let Some(pos) = self.next_word_pos(self.pos, at, word_def, n) { self.pos = pos; true } else { false } } /// Moves the cursor to the same column in the line above pub fn move_to_line_up(&mut self, n: RepeatCount) -> bool { match self.buf[..self.pos].rfind('\n') { Some(off) => { let column = self.buf[off + 1..self.pos].graphemes(true).count(); let mut dest_start = self.buf[..off].rfind('\n').map(|n| n + 1).unwrap_or(0); let mut dest_end = off; for _ in 1..n { if dest_start == 0 { break; } dest_end = dest_start - 1; dest_start = self.buf[..dest_end].rfind('\n').map(|n| n + 1).unwrap_or(0); } let gidx = self.buf[dest_start..dest_end] .grapheme_indices(true) .nth(column); self.pos = gidx.map(|(idx, _)| dest_start + idx).unwrap_or(off); // if there's no enough columns true } None => false, } } /// N lines up starting from the current one /// /// Fails if the cursor is on the first line fn n_lines_up(&self, n: RepeatCount) -> Option<(usize, usize)> { let mut start = if let Some(off) = self.buf[..self.pos].rfind('\n') { off + 1 } else { return None; }; let end = self.buf[self.pos..] .find('\n') .map(|x| self.pos + x + 1) .unwrap_or_else(|| self.buf.len()); for _ in 0..n { if let Some(off) = self.buf[..start - 1].rfind('\n') { start = off + 1 } else { start = 0; break; } } Some((start, end)) } /// N lines down starting from the current one /// /// Fails if the cursor is on the last line fn n_lines_down(&self, n: RepeatCount) -> Option<(usize, usize)> { let mut end = if let Some(off) = self.buf[self.pos..].find('\n') { self.pos + off + 1 } else { return None; }; let start = self.buf[..self.pos].rfind('\n').unwrap_or(0); for _ in 0..n { if let Some(off) = self.buf[end..].find('\n') { end = end + off + 1 } else { end = self.buf.len(); break; }; } Some((start, end)) } /// Moves the cursor to the same column in the line above pub fn move_to_line_down(&mut self, n: RepeatCount) -> bool { match self.buf[self.pos..].find('\n') { Some(off) => { let line_start = self.buf[..self.pos].rfind('\n').map(|n| n + 1).unwrap_or(0); let column = self.buf[line_start..self.pos].graphemes(true).count(); let mut dest_start = self.pos + off + 1; let mut dest_end = self.buf[dest_start..] .find('\n') .map(|v| dest_start + v) .unwrap_or_else(|| self.buf.len()); for _ in 1..n { if dest_end == self.buf.len() { break; } dest_start = dest_end + 1; dest_end = self.buf[dest_start..] .find('\n') .map(|v| dest_start + v) .unwrap_or_else(|| self.buf.len()); } self.pos = self.buf[dest_start..dest_end] .grapheme_indices(true) .nth(column) .map(|(idx, _)| dest_start + idx) .unwrap_or(dest_end); // if there's no enough columns debug_assert!(self.pos <= self.buf.len()); true } None => false, } } fn search_char_pos(&self, cs: CharSearch, n: RepeatCount) -> Option { let mut shift = 0; let search_result = match cs { CharSearch::Backward(c) | CharSearch::BackwardAfter(c) => self.buf[..self.pos] .char_indices() .rev() .filter(|&(_, ch)| ch == c) .take(n) .last() .map(|(i, _)| i), CharSearch::Forward(c) | CharSearch::ForwardBefore(c) => { if let Some(cc) = self.grapheme_at_cursor() { shift = self.pos + cc.len(); if shift < self.buf.len() { self.buf[shift..] .char_indices() .filter(|&(_, ch)| ch == c) .take(n) .last() .map(|(i, _)| i) } else { None } } else { None } } }; if let Some(pos) = search_result { Some(match cs { CharSearch::Backward(_) => pos, CharSearch::BackwardAfter(c) => pos + c.len_utf8(), CharSearch::Forward(_) => shift + pos, CharSearch::ForwardBefore(_) => { shift + pos - self.buf[..shift + pos] .chars() .next_back() .unwrap() .len_utf8() } }) } else { None } } /// Move cursor to the matching character position. /// Return `true` when the search succeeds. pub fn move_to(&mut self, cs: CharSearch, n: RepeatCount) -> bool { if let Some(pos) = self.search_char_pos(cs, n) { self.pos = pos; true } else { false } } /// Kill from the cursor to the end of the current word, /// or, if between words, to the end of the next word. pub fn delete_word(&mut self, at: At, word_def: Word, n: RepeatCount) -> bool { if let Some(pos) = self.next_word_pos(self.pos, at, word_def, n) { let start = self.pos; self.drain(start..pos, Direction::Forward); true } else { false } } /// Delete range specified by `cs` search. pub fn delete_to(&mut self, cs: CharSearch, n: RepeatCount) -> bool { let search_result = match cs { CharSearch::ForwardBefore(c) => self.search_char_pos(CharSearch::Forward(c), n), _ => self.search_char_pos(cs, n), }; if let Some(pos) = search_result { match cs { CharSearch::Backward(_) | CharSearch::BackwardAfter(_) => { let end = self.pos; self.pos = pos; self.drain(pos..end, Direction::Backward); } CharSearch::ForwardBefore(_) => { let start = self.pos; self.drain(start..pos, Direction::Forward); } CharSearch::Forward(c) => { let start = self.pos; self.drain(start..pos + c.len_utf8(), Direction::Forward); } }; true } else { false } } fn skip_whitespace(&self) -> Option { if self.pos == self.buf.len() { return None; } self.buf[self.pos..] .grapheme_indices(true) .filter_map(|(i, ch)| { if ch.chars().all(char::is_alphanumeric) { Some(i) } else { None } }) .next() .map(|i| i + self.pos) } /// Alter the next word. pub fn edit_word(&mut self, a: WordAction) -> bool { if let Some(start) = self.skip_whitespace() { if let Some(end) = self.next_word_pos(start, At::AfterEnd, Word::Emacs, 1) { if start == end { return false; } let word = self .drain(start..end, Direction::default()) .collect::(); let result = match a { WordAction::CAPITALIZE => { let ch = (&word).graphemes(true).next().unwrap(); let cap = ch.to_uppercase(); cap + &word[ch.len()..].to_lowercase() } WordAction::LOWERCASE => word.to_lowercase(), WordAction::UPPERCASE => word.to_uppercase(), }; self.insert_str(start, &result); self.pos = start + result.len(); return true; } } false } /// Transpose two words pub fn transpose_words(&mut self, n: RepeatCount) -> bool { let word_def = Word::Emacs; self.move_to_next_word(At::AfterEnd, word_def, n); let w2_end = self.pos; self.move_to_prev_word(word_def, 1); let w2_beg = self.pos; self.move_to_prev_word(word_def, n); let w1_beg = self.pos; self.move_to_next_word(At::AfterEnd, word_def, 1); let w1_end = self.pos; if w1_beg == w2_beg || w2_beg < w1_end { return false; } let w1 = self.buf[w1_beg..w1_end].to_owned(); let w2 = self .drain(w2_beg..w2_end, Direction::default()) .collect::(); self.insert_str(w2_beg, &w1); self.drain(w1_beg..w1_end, Direction::default()); self.insert_str(w1_beg, &w2); self.pos = w2_end; true } /// Replaces the content between [`start`..`end`] with `text` /// and positions the cursor to the end of text. pub fn replace(&mut self, range: Range, text: &str) { let start = range.start; for cl in &self.cl { if let Ok(mut cl) = cl.try_borrow_mut() { cl.replace(start, self.buf.index(range.clone()), text); } // Ok: while undoing, cl is borrowed. And we want to ignore // changes while undoing. } self.buf.drain(range); if start == self.buf.len() { self.buf.push_str(text); } else { self.buf.insert_str(start, text); } self.pos = start + text.len(); } /// Insert the `s`tring at the specified position. /// Return `true` if the text has been inserted at the end of the line. pub fn insert_str(&mut self, idx: usize, s: &str) -> bool { for cl in &self.cl { if let Ok(mut cl) = cl.try_borrow_mut() { cl.insert_str(idx, s); } // Ok: while undoing, cl is borrowed. And we want to ignore // changes while undoing. } if idx == self.buf.len() { self.buf.push_str(s); true } else { self.buf.insert_str(idx, s); false } } /// Remove the specified `range` in the line. pub fn delete_range(&mut self, range: Range) { self.set_pos(range.start); self.drain(range, Direction::default()); } fn drain(&mut self, range: Range, dir: Direction) -> Drain<'_> { for dl in &self.dl { let lock = dl.try_lock(); if let Ok(mut dl) = lock { dl.delete(range.start, &self.buf[range.start..range.end], dir); } } for cl in &self.cl { if let Ok(mut cl) = cl.try_borrow_mut() { cl.delete(range.start, &self.buf[range.start..range.end], dir); } // Ok: while undoing, cl is borrowed. And we want to ignore // changes while undoing. } self.buf.drain(range) } /// Return the content between current cursor position and `mvt` position. /// Return `None` when the buffer is empty or when the movement fails. pub fn copy(&self, mvt: &Movement) -> Option { if self.is_empty() { return None; } match *mvt { Movement::WholeLine => { 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 if let Some(pos) = self.next_word_pos(0, At::Start, Word::Big, 1) { Some(self.buf[pos..self.pos].to_owned()) } else { None } } Movement::EndOfLine => { 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) => { if let Some(pos) = self.prev_word_pos(self.pos, word_def, n) { Some(self.buf[pos..self.pos].to_owned()) } else { None } } Movement::ForwardWord(n, at, word_def) => { if let Some(pos) = self.next_word_pos(self.pos, at, word_def, n) { Some(self.buf[self.pos..pos].to_owned()) } else { None } } Movement::ViCharSearch(n, cs) => { let search_result = match cs { CharSearch::ForwardBefore(c) => self.search_char_pos(CharSearch::Forward(c), n), _ => self.search_char_pos(cs, n), }; if let Some(pos) = search_result { Some(match cs { CharSearch::Backward(_) | CharSearch::BackwardAfter(_) => { self.buf[pos..self.pos].to_owned() } CharSearch::ForwardBefore(_) => self.buf[self.pos..pos].to_owned(), CharSearch::Forward(c) => self.buf[self.pos..pos + c.len_utf8()].to_owned(), }) } else { None } } Movement::BackwardChar(n) => { if let Some(pos) = self.prev_pos(n) { Some(self.buf[pos..self.pos].to_owned()) } else { None } } Movement::ForwardChar(n) => { if let Some(pos) = self.next_pos(n) { Some(self.buf[self.pos..pos].to_owned()) } else { None } } 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) -> bool { let notify = match *mvt { Movement::ForwardChar(_) | Movement::BackwardChar(_) => false, _ => true, }; if notify { if let Some(dl) = self.dl.as_ref() { let mut dl = dl.lock().unwrap(); dl.start_killing() } } let killed = match *mvt { Movement::ForwardChar(n) => { // Delete (forward) `n` characters at point. self.delete(n).is_some() } Movement::BackwardChar(n) => { // Delete `n` characters backward. self.backspace(n) } Movement::EndOfLine => { // Kill the text from point to the end of the line. self.kill_line() } Movement::WholeLine => { self.move_home(); self.kill_line() } Movement::BeginningOfLine => { // Kill backward from point to the beginning of the line. self.discard_line() } Movement::BackwardWord(n, word_def) => { // kill `n` words backward (until start of word) self.delete_prev_word(word_def, n) } Movement::ForwardWord(n, at, word_def) => { // kill `n` words forward (until start/end of word) self.delete_word(at, word_def, n) } Movement::ViCharSearch(n, cs) => self.delete_to(cs, n), Movement::LineUp(n) => { if let Some((start, end)) = self.n_lines_up(n) { self.delete_range(start..end); true } else { false } } Movement::LineDown(n) => { if let Some((start, end)) = self.n_lines_down(n) { self.delete_range(start..end); true } else { false } } Movement::ViFirstPrint => { false // TODO } Movement::EndOfBuffer => { // Kill the text from point to the end of the buffer. self.kill_buffer() } Movement::BeginningOfBuffer => { // Kill backward from point to the beginning of the buffer. self.discard_buffer() } Movement::WholeBuffer => { self.move_buffer_start(); self.kill_buffer() } }; if notify { if let Some(dl) = self.dl.as_ref() { let mut dl = dl.lock().unwrap(); dl.stop_killing() } } killed } } impl Deref for LineBuffer { type Target = str; fn deref(&self) -> &str { self.as_str() } } fn is_start_of_word(word_def: Word, previous: &str, grapheme: &str) -> bool { (!is_word_char(word_def, previous) && is_word_char(word_def, grapheme)) || (word_def == Word::Vi && !is_other_char(previous) && is_other_char(grapheme)) } fn is_end_of_word(word_def: Word, grapheme: &str, next: &str) -> bool { (!is_word_char(word_def, next) && is_word_char(word_def, grapheme)) || (word_def == Word::Vi && !is_other_char(next) && is_other_char(grapheme)) } fn is_word_char(word_def: Word, grapheme: &str) -> bool { match word_def { Word::Emacs => grapheme.chars().all(char::is_alphanumeric), Word::Vi => is_vi_word_char(grapheme), Word::Big => !grapheme.chars().any(char::is_whitespace), } } fn is_vi_word_char(grapheme: &str) -> bool { grapheme.chars().all(char::is_alphanumeric) || grapheme == "_" } fn is_other_char(grapheme: &str) -> bool { !(grapheme.chars().any(char::is_whitespace) || is_vi_word_char(grapheme)) } #[cfg(test)] mod test { use super::{ChangeListener, DeleteListener, Direction, LineBuffer, WordAction, MAX_LINE}; use crate::keymap::{At, CharSearch, Word}; use std::cell::RefCell; use std::rc::Rc; struct Listener { deleted_str: Option, } impl Listener { fn new() -> Rc> { let l = Listener { deleted_str: None }; Rc::new(RefCell::new(l)) } fn assert_deleted_str_eq(&self, expected: &str) { let actual = self.deleted_str.as_ref().expect("no deleted string"); assert_eq!(expected, actual) } } impl DeleteListener for Listener { fn start_killing(&mut self) {} fn delete(&mut self, _: usize, string: &str, _: Direction) { self.deleted_str = Some(string.to_owned()); } fn stop_killing(&mut self) {} } impl ChangeListener for Listener { fn insert_char(&mut self, _: usize, _: char) {} fn insert_str(&mut self, _: usize, _: &str) {} fn replace(&mut self, _: usize, _: &str, _: &str) {} } #[test] fn next_pos() { let s = LineBuffer::init("ö̲g̈", 0, None); assert_eq!(7, s.len()); let pos = s.next_pos(1); assert_eq!(Some(4), pos); let s = LineBuffer::init("ö̲g̈", 4, None); let pos = s.next_pos(1); assert_eq!(Some(7), pos); } #[test] fn prev_pos() { let s = LineBuffer::init("ö̲g̈", 4, None); assert_eq!(7, s.len()); let pos = s.prev_pos(1); assert_eq!(Some(0), pos); let s = LineBuffer::init("ö̲g̈", 7, None); let pos = s.prev_pos(1); assert_eq!(Some(4), pos); } #[test] fn insert() { let mut s = LineBuffer::with_capacity(MAX_LINE); let push = s.insert('α', 1).unwrap(); assert_eq!("α", s.buf); assert_eq!(2, s.pos); assert_eq!(true, push); let push = s.insert('ß', 1).unwrap(); assert_eq!("αß", s.buf); assert_eq!(4, s.pos); assert_eq!(true, push); s.pos = 0; let push = s.insert('γ', 1).unwrap(); assert_eq!("γαß", s.buf); assert_eq!(2, s.pos); assert_eq!(false, push); } #[test] fn yank_after() { let mut s = LineBuffer::init("αß", 2, None); s.move_forward(1); let ok = s.yank("γδε", 1); assert_eq!(Some(true), ok); assert_eq!("αßγδε", s.buf); assert_eq!(10, s.pos); } #[test] fn yank_before() { let mut s = LineBuffer::init("αε", 2, None); let ok = s.yank("ßγδ", 1); assert_eq!(Some(false), ok); assert_eq!("αßγδε", s.buf); assert_eq!(8, s.pos); } #[test] fn moves() { let mut s = LineBuffer::init("αß", 4, None); let ok = s.move_backward(1); assert_eq!("αß", s.buf); assert_eq!(2, s.pos); assert_eq!(true, ok); let ok = s.move_forward(1); assert_eq!("αß", s.buf); assert_eq!(4, s.pos); assert_eq!(true, ok); let ok = s.move_home(); assert_eq!("αß", s.buf); assert_eq!(0, s.pos); assert_eq!(true, ok); let ok = s.move_end(); assert_eq!("αß", s.buf); assert_eq!(4, s.pos); assert_eq!(true, ok); } #[test] fn move_home_end_multiline() { let text = "αa\nsdf ßc\nasdf"; let mut s = LineBuffer::init(text, 7, None); let ok = s.move_home(); assert_eq!(text, s.buf); assert_eq!(4, s.pos); assert_eq!(true, ok); let ok = s.move_home(); assert_eq!(text, s.buf); assert_eq!(4, s.pos); assert_eq!(false, ok); let ok = s.move_end(); assert_eq!(text, s.buf); assert_eq!(11, s.pos); assert_eq!(true, ok); let ok = s.move_end(); assert_eq!(text, s.buf); assert_eq!(11, s.pos); assert_eq!(false, ok); } #[test] fn move_buffer_multiline() { let text = "αa\nsdf ßc\nasdf"; let mut s = LineBuffer::init(text, 7, None); let ok = s.move_buffer_start(); assert_eq!(text, s.buf); assert_eq!(0, s.pos); assert_eq!(true, ok); let ok = s.move_buffer_start(); assert_eq!(text, s.buf); assert_eq!(0, s.pos); assert_eq!(false, ok); let ok = s.move_buffer_end(); assert_eq!(text, s.buf); assert_eq!(text.len(), s.pos); assert_eq!(true, ok); let ok = s.move_buffer_end(); assert_eq!(text, s.buf); assert_eq!(text.len(), s.pos); assert_eq!(false, ok); } #[test] fn move_grapheme() { let mut s = LineBuffer::init("ag̈", 4, None); assert_eq!(4, s.len()); let ok = s.move_backward(1); assert_eq!(true, ok); assert_eq!(1, s.pos); let ok = s.move_forward(1); assert_eq!(true, ok); assert_eq!(4, s.pos); } #[test] fn delete() { let cl = Listener::new(); let mut s = LineBuffer::init("αß", 2, Some(cl.clone())); let chars = s.delete(1); assert_eq!("α", s.buf); assert_eq!(2, s.pos); assert_eq!(Some("ß".to_owned()), chars); let ok = s.backspace(1); assert_eq!("", s.buf); assert_eq!(0, s.pos); assert_eq!(true, ok); cl.borrow().assert_deleted_str_eq("α"); } #[test] fn kill() { let cl = Listener::new(); let mut s = LineBuffer::init("αßγδε", 6, Some(cl.clone())); let ok = s.kill_line(); assert_eq!("αßγ", s.buf); assert_eq!(6, s.pos); assert_eq!(true, ok); cl.borrow().assert_deleted_str_eq("δε"); s.pos = 4; let ok = s.discard_line(); assert_eq!("γ", s.buf); assert_eq!(0, s.pos); assert_eq!(true, ok); cl.borrow().assert_deleted_str_eq("αß"); } #[test] fn kill_multiline() { let cl = Listener::new(); let mut s = LineBuffer::init("αß\nγδ 12\nε f4", 7, Some(cl.clone())); let ok = s.kill_line(); assert_eq!("αß\nγ\nε f4", s.buf); assert_eq!(7, s.pos); assert_eq!(true, ok); cl.borrow().assert_deleted_str_eq("δ 12"); let ok = s.kill_line(); assert_eq!("αß\nγε f4", s.buf); assert_eq!(7, s.pos); assert_eq!(true, ok); cl.borrow().assert_deleted_str_eq("\n"); let ok = s.kill_line(); assert_eq!("αß\nγ", s.buf); assert_eq!(7, s.pos); assert_eq!(true, ok); cl.borrow().assert_deleted_str_eq("ε f4"); let ok = s.kill_line(); assert_eq!(7, s.pos); assert_eq!(false, ok); } #[test] fn discard_multiline() { let cl = Listener::new(); let mut s = LineBuffer::init("αß\nc γδε", 9, Some(cl.clone())); let ok = s.discard_line(); assert_eq!("αß\nδε", s.buf); assert_eq!(5, s.pos); assert_eq!(true, ok); cl.borrow().assert_deleted_str_eq("c γ"); let ok = s.discard_line(); assert_eq!("αßδε", s.buf); assert_eq!(4, s.pos); assert_eq!(true, ok); cl.borrow().assert_deleted_str_eq("\n"); let ok = s.discard_line(); assert_eq!("δε", s.buf); assert_eq!(0, s.pos); assert_eq!(true, ok); cl.borrow().assert_deleted_str_eq("αß"); let ok = s.discard_line(); assert_eq!(0, s.pos); assert_eq!(false, ok); } #[test] fn transpose() { let mut s = LineBuffer::init("aßc", 1, None); let ok = s.transpose_chars(); assert_eq!("ßac", s.buf); assert_eq!(3, s.pos); assert_eq!(true, ok); s.buf = String::from("aßc"); s.pos = 3; let ok = s.transpose_chars(); assert_eq!("acß", s.buf); assert_eq!(4, s.pos); assert_eq!(true, ok); s.buf = String::from("aßc"); s.pos = 4; let ok = s.transpose_chars(); assert_eq!("acß", s.buf); assert_eq!(4, s.pos); assert_eq!(true, ok); } #[test] fn move_to_prev_word() { let mut s = LineBuffer::init("a ß c", 6, None); // before 'c' let ok = s.move_to_prev_word(Word::Emacs, 1); assert_eq!("a ß c", s.buf); assert_eq!(2, s.pos); // before 'ß' assert!(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, None); let ok = s.move_to_prev_word(Word::Vi, 1); assert!(ok); assert_eq!(17, s.pos); let ok = s.move_to_prev_word(Word::Vi, 1); assert!(ok); assert_eq!(15, s.pos); let ok = s.move_to_prev_word(Word::Vi, 1); assert!(ok); assert_eq!(12, s.pos); let ok = s.move_to_prev_word(Word::Vi, 1); assert!(ok); assert_eq!(11, s.pos); let ok = s.move_to_prev_word(Word::Vi, 1); assert!(ok); assert_eq!(7, s.pos); let ok = s.move_to_prev_word(Word::Vi, 1); assert!(ok); assert_eq!(6, s.pos); let ok = s.move_to_prev_word(Word::Vi, 1); assert!(ok); assert_eq!(0, s.pos); let ok = s.move_to_prev_word(Word::Vi, 1); assert!(!ok); } #[test] fn move_to_prev_big_word() { let mut s = LineBuffer::init("alpha ,beta/rho; mu", 19, None); let ok = s.move_to_prev_word(Word::Big, 1); assert!(ok); assert_eq!(17, s.pos); let ok = s.move_to_prev_word(Word::Big, 1); assert!(ok); assert_eq!(6, s.pos); let ok = s.move_to_prev_word(Word::Big, 1); assert!(ok); assert_eq!(0, s.pos); let ok = s.move_to_prev_word(Word::Big, 1); assert!(!ok); } #[test] fn move_to_forward() { let mut s = LineBuffer::init("αßγδε", 2, None); let ok = s.move_to(CharSearch::ForwardBefore('ε'), 1); assert_eq!(true, ok); assert_eq!(6, s.pos); let mut s = LineBuffer::init("αßγδε", 2, None); let ok = s.move_to(CharSearch::Forward('ε'), 1); assert_eq!(true, ok); assert_eq!(8, s.pos); let mut s = LineBuffer::init("αßγδε", 2, None); let ok = s.move_to(CharSearch::Forward('ε'), 10); assert_eq!(true, ok); assert_eq!(8, s.pos); } #[test] fn move_to_backward() { let mut s = LineBuffer::init("αßγδε", 8, None); let ok = s.move_to(CharSearch::BackwardAfter('ß'), 1); assert_eq!(true, ok); assert_eq!(4, s.pos); let mut s = LineBuffer::init("αßγδε", 8, None); let ok = s.move_to(CharSearch::Backward('ß'), 1); assert_eq!(true, ok); assert_eq!(2, s.pos); } #[test] fn delete_prev_word() { let cl = Listener::new(); let mut s = LineBuffer::init("a ß c", 6, Some(cl.clone())); let ok = s.delete_prev_word(Word::Big, 1); assert_eq!("a c", s.buf); assert_eq!(2, s.pos); assert_eq!(true, ok); cl.borrow().assert_deleted_str_eq("ß "); } #[test] fn move_to_next_word() { let mut s = LineBuffer::init("a ß c", 1, None); // after 'a' let ok = s.move_to_next_word(At::AfterEnd, Word::Emacs, 1); assert_eq!("a ß c", s.buf); assert_eq!(true, ok); assert_eq!(4, s.pos); // after 'ß' let ok = s.move_to_next_word(At::AfterEnd, Word::Emacs, 1); assert_eq!(true, ok); assert_eq!(7, s.pos); // after 'c' s.move_home(); let ok = s.move_to_next_word(At::AfterEnd, Word::Emacs, 1); assert_eq!(true, ok); assert_eq!(1, s.pos); // after 'a' let ok = s.move_to_next_word(At::AfterEnd, Word::Emacs, 2); assert_eq!(true, ok); assert_eq!(7, s.pos); // after 'c' } #[test] fn move_to_end_of_word() { let mut s = LineBuffer::init("a ßeta c", 1, None); let ok = s.move_to_next_word(At::BeforeEnd, Word::Vi, 1); assert_eq!("a ßeta c", s.buf); assert_eq!(6, s.pos); assert_eq!(true, ok); } #[test] fn move_to_end_of_vi_word() { let mut s = LineBuffer::init("alpha ,beta/rho; mu", 0, None); let ok = s.move_to_next_word(At::BeforeEnd, Word::Vi, 1); assert!(ok); assert_eq!(4, s.pos); let ok = s.move_to_next_word(At::BeforeEnd, Word::Vi, 1); assert!(ok); assert_eq!(6, s.pos); let ok = s.move_to_next_word(At::BeforeEnd, Word::Vi, 1); assert!(ok); assert_eq!(10, s.pos); let ok = s.move_to_next_word(At::BeforeEnd, Word::Vi, 1); assert!(ok); assert_eq!(11, s.pos); let ok = s.move_to_next_word(At::BeforeEnd, Word::Vi, 1); assert!(ok); assert_eq!(14, s.pos); let ok = s.move_to_next_word(At::BeforeEnd, Word::Vi, 1); assert!(ok); assert_eq!(15, s.pos); let ok = s.move_to_next_word(At::BeforeEnd, Word::Vi, 1); assert!(ok); assert_eq!(18, s.pos); let ok = s.move_to_next_word(At::BeforeEnd, Word::Vi, 1); assert!(!ok); } #[test] fn move_to_end_of_big_word() { let mut s = LineBuffer::init("alpha ,beta/rho; mu", 0, None); let ok = s.move_to_next_word(At::BeforeEnd, Word::Big, 1); assert!(ok); assert_eq!(4, s.pos); let ok = s.move_to_next_word(At::BeforeEnd, Word::Big, 1); assert!(ok); assert_eq!(15, s.pos); let ok = s.move_to_next_word(At::BeforeEnd, Word::Big, 1); assert!(ok); assert_eq!(18, s.pos); let ok = s.move_to_next_word(At::BeforeEnd, Word::Big, 1); assert!(!ok); } #[test] fn move_to_start_of_word() { let mut s = LineBuffer::init("a ß c", 2, None); let ok = s.move_to_next_word(At::Start, Word::Emacs, 1); assert_eq!("a ß c", s.buf); assert_eq!(6, s.pos); assert_eq!(true, ok); } #[test] fn move_to_start_of_vi_word() { let mut s = LineBuffer::init("alpha ,beta/rho; mu", 0, None); let ok = s.move_to_next_word(At::Start, Word::Vi, 1); assert!(ok); assert_eq!(6, s.pos); let ok = s.move_to_next_word(At::Start, Word::Vi, 1); assert!(ok); assert_eq!(7, s.pos); let ok = s.move_to_next_word(At::Start, Word::Vi, 1); assert!(ok); assert_eq!(11, s.pos); let ok = s.move_to_next_word(At::Start, Word::Vi, 1); assert!(ok); assert_eq!(12, s.pos); let ok = s.move_to_next_word(At::Start, Word::Vi, 1); assert!(ok); assert_eq!(15, s.pos); let ok = s.move_to_next_word(At::Start, Word::Vi, 1); assert!(ok); assert_eq!(17, s.pos); let ok = s.move_to_next_word(At::Start, Word::Vi, 1); assert!(ok); assert_eq!(18, s.pos); let ok = s.move_to_next_word(At::Start, Word::Vi, 1); assert!(!ok); } #[test] fn move_to_start_of_big_word() { let mut s = LineBuffer::init("alpha ,beta/rho; mu", 0, None); let ok = s.move_to_next_word(At::Start, Word::Big, 1); assert!(ok); assert_eq!(6, s.pos); let ok = s.move_to_next_word(At::Start, Word::Big, 1); assert!(ok); assert_eq!(17, s.pos); let ok = s.move_to_next_word(At::Start, Word::Big, 1); assert!(ok); assert_eq!(18, s.pos); let ok = s.move_to_next_word(At::Start, Word::Big, 1); assert!(!ok); } #[test] fn delete_word() { let cl = Listener::new(); let mut s = LineBuffer::init("a ß c", 1, Some(cl.clone())); let ok = s.delete_word(At::AfterEnd, Word::Emacs, 1); assert_eq!("a c", s.buf); assert_eq!(1, s.pos); assert_eq!(true, ok); cl.borrow().assert_deleted_str_eq(" ß"); let mut s = LineBuffer::init("test", 0, Some(cl.clone())); let ok = s.delete_word(At::AfterEnd, Word::Vi, 1); assert_eq!("", s.buf); assert_eq!(0, s.pos); assert_eq!(true, ok); cl.borrow().assert_deleted_str_eq("test"); } #[test] fn delete_til_start_of_word() { let cl = Listener::new(); let mut s = LineBuffer::init("a ß c", 2, Some(cl.clone())); let ok = s.delete_word(At::Start, Word::Emacs, 1); assert_eq!("a c", s.buf); assert_eq!(2, s.pos); assert_eq!(true, ok); cl.borrow().assert_deleted_str_eq("ß "); } #[test] fn delete_to_forward() { let cl = Listener::new(); let mut s = LineBuffer::init("αßγδε", 2, Some(cl.clone())); let ok = s.delete_to(CharSearch::ForwardBefore('ε'), 1); assert_eq!(true, ok); cl.borrow().assert_deleted_str_eq("ßγδ"); assert_eq!("αε", s.buf); assert_eq!(2, s.pos); let mut s = LineBuffer::init("αßγδε", 2, Some(cl.clone())); let ok = s.delete_to(CharSearch::Forward('ε'), 1); assert_eq!(true, ok); cl.borrow().assert_deleted_str_eq("ßγδε"); assert_eq!("α", s.buf); assert_eq!(2, s.pos); } #[test] fn delete_to_backward() { let cl = Listener::new(); let mut s = LineBuffer::init("αßγδε", 8, Some(cl.clone())); let ok = s.delete_to(CharSearch::BackwardAfter('α'), 1); assert_eq!(true, ok); cl.borrow().assert_deleted_str_eq("ßγδ"); assert_eq!("αε", s.buf); assert_eq!(2, s.pos); let mut s = LineBuffer::init("αßγδε", 8, Some(cl.clone())); let ok = s.delete_to(CharSearch::Backward('ß'), 1); assert_eq!(true, ok); cl.borrow().assert_deleted_str_eq("ßγδ"); assert_eq!("αε", s.buf); assert_eq!(2, s.pos); } #[test] fn edit_word() { let mut s = LineBuffer::init("a ßeta c", 1, None); assert!(s.edit_word(WordAction::UPPERCASE)); assert_eq!("a SSETA c", s.buf); assert_eq!(7, s.pos); let mut s = LineBuffer::init("a ßetA c", 1, None); assert!(s.edit_word(WordAction::LOWERCASE)); assert_eq!("a ßeta c", s.buf); assert_eq!(7, s.pos); let mut s = LineBuffer::init("a ßETA c", 1, None); assert!(s.edit_word(WordAction::CAPITALIZE)); assert_eq!("a SSeta c", s.buf); assert_eq!(7, s.pos); let mut s = LineBuffer::init("test", 1, None); assert!(s.edit_word(WordAction::CAPITALIZE)); assert_eq!("tEst", s.buf); assert_eq!(4, s.pos); } #[test] fn transpose_words() { let mut s = LineBuffer::init("ßeta / δelta__", 15, None); assert!(s.transpose_words(1)); assert_eq!("δelta__ / ßeta", s.buf); assert_eq!(16, s.pos); let mut s = LineBuffer::init("ßeta / δelta", 14, None); assert!(s.transpose_words(1)); assert_eq!("δelta / ßeta", s.buf); assert_eq!(14, s.pos); let mut s = LineBuffer::init(" / δelta", 8, None); assert!(!s.transpose_words(1)); let mut s = LineBuffer::init("ßeta / __", 9, None); assert!(!s.transpose_words(1)); } #[test] fn move_by_line() { let text = "aa123\nsdf bc\nasdf"; let mut s = LineBuffer::init(text, 14, None); // move up let ok = s.move_to_line_up(1); assert_eq!(7, s.pos); assert!(ok); let ok = s.move_to_line_up(1); assert_eq!(1, s.pos); assert!(ok); let ok = s.move_to_line_up(1); assert_eq!(1, s.pos); assert!(!ok); // move down let ok = s.move_to_line_down(1); assert_eq!(7, s.pos); assert!(ok); let ok = s.move_to_line_down(1); assert_eq!(14, s.pos); assert!(ok); let ok = s.move_to_line_down(1); assert_eq!(14, s.pos); assert!(!ok); // move by multiple steps let ok = s.move_to_line_up(2); assert_eq!(1, s.pos); assert!(ok); let ok = s.move_to_line_down(2); assert_eq!(14, s.pos); assert!(ok); } } rustyline-6.3.0/src/test/common.rs010064400007650000024000000253421346010623700153510ustar 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::KeyPress; #[test] fn home_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_cursor( *mode, ("", ""), &[KeyPress::Home, KeyPress::Enter], ("", ""), ); assert_cursor( *mode, ("Hi", ""), &[KeyPress::Home, KeyPress::Enter], ("", "Hi"), ); if *mode == EditMode::Vi { // vi command mode assert_cursor( *mode, ("Hi", ""), &[KeyPress::Esc, KeyPress::Home, KeyPress::Enter], ("", "Hi"), ); } } } #[test] fn end_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_cursor(*mode, ("", ""), &[KeyPress::End, KeyPress::Enter], ("", "")); assert_cursor( *mode, ("H", "i"), &[KeyPress::End, KeyPress::Enter], ("Hi", ""), ); assert_cursor( *mode, ("", "Hi"), &[KeyPress::End, KeyPress::Enter], ("Hi", ""), ); if *mode == EditMode::Vi { // vi command mode assert_cursor( *mode, ("", "Hi"), &[KeyPress::Esc, KeyPress::End, KeyPress::Enter], ("Hi", ""), ); } } } #[test] fn left_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_cursor( *mode, ("Hi", ""), &[KeyPress::Left, KeyPress::Enter], ("H", "i"), ); assert_cursor( *mode, ("H", "i"), &[KeyPress::Left, KeyPress::Enter], ("", "Hi"), ); assert_cursor( *mode, ("", "Hi"), &[KeyPress::Left, KeyPress::Enter], ("", "Hi"), ); if *mode == EditMode::Vi { // vi command mode assert_cursor( *mode, ("Bye", ""), &[KeyPress::Esc, KeyPress::Left, KeyPress::Enter], ("B", "ye"), ); } } } #[test] fn right_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_cursor( *mode, ("", ""), &[KeyPress::Right, KeyPress::Enter], ("", ""), ); assert_cursor( *mode, ("", "Hi"), &[KeyPress::Right, KeyPress::Enter], ("H", "i"), ); assert_cursor( *mode, ("B", "ye"), &[KeyPress::Right, KeyPress::Enter], ("By", "e"), ); assert_cursor( *mode, ("H", "i"), &[KeyPress::Right, KeyPress::Enter], ("Hi", ""), ); if *mode == EditMode::Vi { // vi command mode assert_cursor( *mode, ("", "Hi"), &[KeyPress::Esc, KeyPress::Right, KeyPress::Enter], ("H", "i"), ); } } } #[test] fn enter_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_line(*mode, &[KeyPress::Enter], ""); assert_line(*mode, &[KeyPress::Char('a'), KeyPress::Enter], "a"); assert_line_with_initial(*mode, ("Hi", ""), &[KeyPress::Enter], "Hi"); assert_line_with_initial(*mode, ("", "Hi"), &[KeyPress::Enter], "Hi"); assert_line_with_initial(*mode, ("H", "i"), &[KeyPress::Enter], "Hi"); if *mode == EditMode::Vi { // vi command mode assert_line(*mode, &[KeyPress::Esc, KeyPress::Enter], ""); assert_line( *mode, &[KeyPress::Char('a'), KeyPress::Esc, KeyPress::Enter], "a", ); assert_line_with_initial(*mode, ("Hi", ""), &[KeyPress::Esc, KeyPress::Enter], "Hi"); assert_line_with_initial(*mode, ("", "Hi"), &[KeyPress::Esc, KeyPress::Enter], "Hi"); assert_line_with_initial(*mode, ("H", "i"), &[KeyPress::Esc, KeyPress::Enter], "Hi"); } } } #[test] fn newline_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_line(*mode, &[KeyPress::Ctrl('J')], ""); assert_line(*mode, &[KeyPress::Char('a'), KeyPress::Ctrl('J')], "a"); if *mode == EditMode::Vi { // vi command mode assert_line(*mode, &[KeyPress::Esc, KeyPress::Ctrl('J')], ""); assert_line( *mode, &[KeyPress::Char('a'), KeyPress::Esc, KeyPress::Ctrl('J')], "a", ); } } } #[test] fn eof_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { let mut editor = init_editor(*mode, &[KeyPress::Ctrl('D')]); let err = editor.readline(">>"); assert_matches!(err, Err(ReadlineError::Eof)); } assert_line( EditMode::Emacs, &[KeyPress::Char('a'), KeyPress::Ctrl('D'), KeyPress::Enter], "a", ); assert_line( EditMode::Vi, &[KeyPress::Char('a'), KeyPress::Ctrl('D')], "a", ); assert_line( EditMode::Vi, &[KeyPress::Char('a'), KeyPress::Esc, KeyPress::Ctrl('D')], "a", ); assert_line_with_initial( EditMode::Emacs, ("", "Hi"), &[KeyPress::Ctrl('D'), KeyPress::Enter], "i", ); assert_line_with_initial(EditMode::Vi, ("", "Hi"), &[KeyPress::Ctrl('D')], "Hi"); assert_line_with_initial( EditMode::Vi, ("", "Hi"), &[KeyPress::Esc, KeyPress::Ctrl('D')], "Hi", ); } #[test] fn interrupt_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { let mut editor = init_editor(*mode, &[KeyPress::Ctrl('C')]); let err = editor.readline(">>"); assert_matches!(err, Err(ReadlineError::Interrupted)); let mut editor = init_editor(*mode, &[KeyPress::Ctrl('C')]); let err = editor.readline_with_initial(">>", ("Hi", "")); assert_matches!(err, Err(ReadlineError::Interrupted)); if *mode == EditMode::Vi { // vi command mode let mut editor = init_editor(*mode, &[KeyPress::Esc, KeyPress::Ctrl('C')]); let err = editor.readline_with_initial(">>", ("Hi", "")); assert_matches!(err, Err(ReadlineError::Interrupted)); } } } #[test] fn delete_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_cursor( *mode, ("a", ""), &[KeyPress::Delete, KeyPress::Enter], ("a", ""), ); assert_cursor( *mode, ("", "a"), &[KeyPress::Delete, KeyPress::Enter], ("", ""), ); if *mode == EditMode::Vi { // vi command mode assert_cursor( *mode, ("", "a"), &[KeyPress::Esc, KeyPress::Delete, KeyPress::Enter], ("", ""), ); } } } #[test] fn ctrl_t() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_cursor( *mode, ("a", "b"), &[KeyPress::Ctrl('T'), KeyPress::Enter], ("ba", ""), ); assert_cursor( *mode, ("ab", "cd"), &[KeyPress::Ctrl('T'), KeyPress::Enter], ("acb", "d"), ); if *mode == EditMode::Vi { // vi command mode assert_cursor( *mode, ("ab", ""), &[KeyPress::Esc, KeyPress::Ctrl('T'), KeyPress::Enter], ("ba", ""), ); } } } #[test] fn ctrl_u() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_cursor( *mode, ("start of line ", "end"), &[KeyPress::Ctrl('U'), KeyPress::Enter], ("", "end"), ); assert_cursor( *mode, ("", "end"), &[KeyPress::Ctrl('U'), KeyPress::Enter], ("", "end"), ); if *mode == EditMode::Vi { // vi command mode assert_cursor( *mode, ("start of line ", "end"), &[KeyPress::Esc, KeyPress::Ctrl('U'), KeyPress::Enter], ("", " end"), ); } } } #[cfg(unix)] #[test] fn ctrl_v() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_cursor( *mode, ("", ""), &[KeyPress::Ctrl('V'), KeyPress::Char('\t'), KeyPress::Enter], ("\t", ""), ); if *mode == EditMode::Vi { // vi command mode assert_cursor( *mode, ("", ""), &[ KeyPress::Esc, KeyPress::Ctrl('V'), KeyPress::Char('\t'), KeyPress::Enter, ], ("\t", ""), ); } } } #[test] fn ctrl_w() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_cursor( *mode, ("Hello, ", "world"), &[KeyPress::Ctrl('W'), KeyPress::Enter], ("", "world"), ); assert_cursor( *mode, ("Hello, world.", ""), &[KeyPress::Ctrl('W'), KeyPress::Enter], ("Hello, ", ""), ); if *mode == EditMode::Vi { // vi command mode assert_cursor( *mode, ("Hello, world.", ""), &[KeyPress::Esc, KeyPress::Ctrl('W'), KeyPress::Enter], ("Hello, ", "."), ); } } } #[test] fn ctrl_y() { for mode in &[EditMode::Emacs /* FIXME, EditMode::Vi */] { assert_cursor( *mode, ("Hello, ", "world"), &[KeyPress::Ctrl('W'), KeyPress::Ctrl('Y'), KeyPress::Enter], ("Hello, ", "world"), ); } } #[test] fn ctrl__() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_cursor( *mode, ("Hello, ", "world"), &[KeyPress::Ctrl('W'), KeyPress::Ctrl('_'), KeyPress::Enter], ("Hello, ", "world"), ); if *mode == EditMode::Vi { // vi command mode assert_cursor( *mode, ("Hello, ", "world"), &[ KeyPress::Esc, KeyPress::Ctrl('W'), KeyPress::Ctrl('_'), KeyPress::Enter, ], ("Hello,", " world"), ); } } } rustyline-6.3.0/src/test/emacs.rs010064400007650000024000000225731364141226000151510ustar 00000000000000//! Emacs specific key bindings use super::{assert_cursor, assert_history}; use crate::config::EditMode; use crate::keys::KeyPress; #[test] fn ctrl_a() { assert_cursor( EditMode::Emacs, ("Hi", ""), &[KeyPress::Ctrl('A'), KeyPress::Enter], ("", "Hi"), ); assert_cursor( EditMode::Emacs, ("test test\n123", "foo"), &[KeyPress::Ctrl('A'), KeyPress::Enter], ("test test\n", "123foo"), ); } #[test] fn ctrl_e() { assert_cursor( EditMode::Emacs, ("", "Hi"), &[KeyPress::Ctrl('E'), KeyPress::Enter], ("Hi", ""), ); assert_cursor( EditMode::Emacs, ("foo", "test test\n123"), &[KeyPress::Ctrl('E'), KeyPress::Enter], ("footest test", "\n123"), ); } #[test] fn ctrl_b() { assert_cursor( EditMode::Emacs, ("Hi", ""), &[KeyPress::Ctrl('B'), KeyPress::Enter], ("H", "i"), ); assert_cursor( EditMode::Emacs, ("Hi", ""), &[KeyPress::Meta('2'), KeyPress::Ctrl('B'), KeyPress::Enter], ("", "Hi"), ); assert_cursor( EditMode::Emacs, ("", "Hi"), &[ KeyPress::Meta('-'), KeyPress::Meta('2'), KeyPress::Ctrl('B'), KeyPress::Enter, ], ("Hi", ""), ); } #[test] fn ctrl_f() { assert_cursor( EditMode::Emacs, ("", "Hi"), &[KeyPress::Ctrl('F'), KeyPress::Enter], ("H", "i"), ); assert_cursor( EditMode::Emacs, ("", "Hi"), &[KeyPress::Meta('2'), KeyPress::Ctrl('F'), KeyPress::Enter], ("Hi", ""), ); assert_cursor( EditMode::Emacs, ("Hi", ""), &[ KeyPress::Meta('-'), KeyPress::Meta('2'), KeyPress::Ctrl('F'), KeyPress::Enter, ], ("", "Hi"), ); } #[test] fn ctrl_h() { assert_cursor( EditMode::Emacs, ("Hi", ""), &[KeyPress::Ctrl('H'), KeyPress::Enter], ("H", ""), ); assert_cursor( EditMode::Emacs, ("Hi", ""), &[KeyPress::Meta('2'), KeyPress::Ctrl('H'), KeyPress::Enter], ("", ""), ); assert_cursor( EditMode::Emacs, ("", "Hi"), &[ KeyPress::Meta('-'), KeyPress::Meta('2'), KeyPress::Ctrl('H'), KeyPress::Enter, ], ("", ""), ); } #[test] fn backspace() { assert_cursor( EditMode::Emacs, ("", ""), &[KeyPress::Backspace, KeyPress::Enter], ("", ""), ); assert_cursor( EditMode::Emacs, ("Hi", ""), &[KeyPress::Backspace, KeyPress::Enter], ("H", ""), ); assert_cursor( EditMode::Emacs, ("", "Hi"), &[KeyPress::Backspace, KeyPress::Enter], ("", "Hi"), ); } #[test] fn ctrl_k() { assert_cursor( EditMode::Emacs, ("Hi", ""), &[KeyPress::Ctrl('K'), KeyPress::Enter], ("Hi", ""), ); assert_cursor( EditMode::Emacs, ("", "Hi"), &[KeyPress::Ctrl('K'), KeyPress::Enter], ("", ""), ); assert_cursor( EditMode::Emacs, ("B", "ye"), &[KeyPress::Ctrl('K'), KeyPress::Enter], ("B", ""), ); assert_cursor( EditMode::Emacs, ("Hi", "foo\nbar"), &[KeyPress::Ctrl('K'), KeyPress::Enter], ("Hi", "\nbar"), ); assert_cursor( EditMode::Emacs, ("Hi", "\nbar"), &[KeyPress::Ctrl('K'), KeyPress::Enter], ("Hi", "bar"), ); assert_cursor( EditMode::Emacs, ("Hi", "bar"), &[KeyPress::Ctrl('K'), KeyPress::Enter], ("Hi", ""), ); } #[test] fn ctrl_u() { assert_cursor( EditMode::Emacs, ("", "Hi"), &[KeyPress::Ctrl('U'), KeyPress::Enter], ("", "Hi"), ); assert_cursor( EditMode::Emacs, ("Hi", ""), &[KeyPress::Ctrl('U'), KeyPress::Enter], ("", ""), ); assert_cursor( EditMode::Emacs, ("B", "ye"), &[KeyPress::Ctrl('U'), KeyPress::Enter], ("", "ye"), ); assert_cursor( EditMode::Emacs, ("foo\nbar", "Hi"), &[KeyPress::Ctrl('U'), KeyPress::Enter], ("foo\n", "Hi"), ); assert_cursor( EditMode::Emacs, ("foo\n", "Hi"), &[KeyPress::Ctrl('U'), KeyPress::Enter], ("foo", "Hi"), ); assert_cursor( EditMode::Emacs, ("foo", "Hi"), &[KeyPress::Ctrl('U'), KeyPress::Enter], ("", "Hi"), ); } #[test] fn ctrl_n() { assert_history( EditMode::Emacs, &["line1", "line2"], &[ KeyPress::Ctrl('P'), KeyPress::Ctrl('P'), KeyPress::Ctrl('N'), KeyPress::Enter, ], "", ("line2", ""), ); } #[test] fn ctrl_p() { assert_history( EditMode::Emacs, &["line1"], &[KeyPress::Ctrl('P'), KeyPress::Enter], "", ("line1", ""), ); } #[test] fn ctrl_t() { /* FIXME assert_cursor( ("ab", "cd"), &[KeyPress::Meta('2'), KeyPress::Ctrl('T'), KeyPress::Enter], ("acdb", ""), );*/ } #[test] fn ctrl_x_ctrl_u() { assert_cursor( EditMode::Emacs, ("Hello, ", "world"), &[ KeyPress::Ctrl('W'), KeyPress::Ctrl('X'), KeyPress::Ctrl('U'), KeyPress::Enter, ], ("Hello, ", "world"), ); } #[test] fn meta_b() { assert_cursor( EditMode::Emacs, ("Hello, world!", ""), &[KeyPress::Meta('B'), KeyPress::Enter], ("Hello, ", "world!"), ); assert_cursor( EditMode::Emacs, ("Hello, world!", ""), &[KeyPress::Meta('2'), KeyPress::Meta('B'), KeyPress::Enter], ("", "Hello, world!"), ); assert_cursor( EditMode::Emacs, ("", "Hello, world!"), &[KeyPress::Meta('-'), KeyPress::Meta('B'), KeyPress::Enter], ("Hello", ", world!"), ); } #[test] fn meta_f() { assert_cursor( EditMode::Emacs, ("", "Hello, world!"), &[KeyPress::Meta('F'), KeyPress::Enter], ("Hello", ", world!"), ); assert_cursor( EditMode::Emacs, ("", "Hello, world!"), &[KeyPress::Meta('2'), KeyPress::Meta('F'), KeyPress::Enter], ("Hello, world", "!"), ); assert_cursor( EditMode::Emacs, ("Hello, world!", ""), &[KeyPress::Meta('-'), KeyPress::Meta('F'), KeyPress::Enter], ("Hello, ", "world!"), ); } #[test] fn meta_c() { assert_cursor( EditMode::Emacs, ("hi", ""), &[KeyPress::Meta('C'), KeyPress::Enter], ("hi", ""), ); assert_cursor( EditMode::Emacs, ("", "hi"), &[KeyPress::Meta('C'), KeyPress::Enter], ("Hi", ""), ); /* FIXME assert_cursor( ("", "hi test"), &[KeyPress::Meta('2'), KeyPress::Meta('C'), KeyPress::Enter], ("Hi Test", ""), );*/ } #[test] fn meta_l() { assert_cursor( EditMode::Emacs, ("Hi", ""), &[KeyPress::Meta('L'), KeyPress::Enter], ("Hi", ""), ); assert_cursor( EditMode::Emacs, ("", "HI"), &[KeyPress::Meta('L'), KeyPress::Enter], ("hi", ""), ); /* FIXME assert_cursor( ("", "HI TEST"), &[KeyPress::Meta('2'), KeyPress::Meta('L'), KeyPress::Enter], ("hi test", ""), );*/ } #[test] fn meta_u() { assert_cursor( EditMode::Emacs, ("hi", ""), &[KeyPress::Meta('U'), KeyPress::Enter], ("hi", ""), ); assert_cursor( EditMode::Emacs, ("", "hi"), &[KeyPress::Meta('U'), KeyPress::Enter], ("HI", ""), ); /* FIXME assert_cursor( ("", "hi test"), &[KeyPress::Meta('2'), KeyPress::Meta('U'), KeyPress::Enter], ("HI TEST", ""), );*/ } #[test] fn meta_d() { assert_cursor( EditMode::Emacs, ("Hello", ", world!"), &[KeyPress::Meta('D'), KeyPress::Enter], ("Hello", "!"), ); assert_cursor( EditMode::Emacs, ("Hello", ", world!"), &[KeyPress::Meta('2'), KeyPress::Meta('D'), KeyPress::Enter], ("Hello", ""), ); } #[test] fn meta_t() { assert_cursor( EditMode::Emacs, ("Hello", ", world!"), &[KeyPress::Meta('T'), KeyPress::Enter], ("world, Hello", "!"), ); /* FIXME assert_cursor( ("One Two", " Three Four"), &[KeyPress::Meta('T'), KeyPress::Enter], ("One Four Three Two", ""), );*/ } #[test] fn meta_y() { assert_cursor( EditMode::Emacs, ("Hello, world", "!"), &[ KeyPress::Ctrl('W'), KeyPress::Left, KeyPress::Ctrl('W'), KeyPress::Ctrl('Y'), KeyPress::Meta('Y'), KeyPress::Enter, ], ("world", " !"), ); } #[test] fn meta_backspace() { assert_cursor( EditMode::Emacs, ("Hello, wor", "ld!"), &[KeyPress::Meta('\x08'), KeyPress::Enter], ("Hello, ", "ld!"), ); } #[test] fn meta_digit() { assert_cursor( EditMode::Emacs, ("", ""), &[KeyPress::Meta('3'), KeyPress::Char('h'), KeyPress::Enter], ("hhh", ""), ); } rustyline-6.3.0/src/test/history.rs010064400007650000024000000133661357476110200155710ustar 00000000000000//! History related commands tests use super::assert_history; use crate::config::EditMode; use crate::keys::KeyPress; #[test] fn down_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_history( *mode, &["line1"], &[KeyPress::Down, KeyPress::Enter], "", ("", ""), ); assert_history( *mode, &["line1", "line2"], &[KeyPress::Up, KeyPress::Up, KeyPress::Down, KeyPress::Enter], "", ("line2", ""), ); assert_history( *mode, &["line1"], &[ KeyPress::Char('a'), KeyPress::Up, KeyPress::Down, // restore original line KeyPress::Enter, ], "", ("a", ""), ); assert_history( *mode, &["line1"], &[ KeyPress::Char('a'), KeyPress::Down, // noop KeyPress::Enter, ], "", ("a", ""), ); } } #[test] fn up_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_history(*mode, &[], &[KeyPress::Up, KeyPress::Enter], "", ("", "")); assert_history( *mode, &["line1"], &[KeyPress::Up, KeyPress::Enter], "", ("line1", ""), ); assert_history( *mode, &["line1", "line2"], &[KeyPress::Up, KeyPress::Up, KeyPress::Enter], "", ("line1", ""), ); } } #[test] fn ctrl_r() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_history( *mode, &[], &[KeyPress::Ctrl('R'), KeyPress::Char('o'), KeyPress::Enter], "", ("o", ""), ); assert_history( *mode, &["rustc", "cargo"], &[ KeyPress::Ctrl('R'), KeyPress::Char('o'), KeyPress::Right, // just to assert cursor pos KeyPress::Enter, ], "", ("cargo", ""), ); assert_history( *mode, &["rustc", "cargo"], &[ KeyPress::Ctrl('R'), KeyPress::Char('u'), KeyPress::Right, // just to assert cursor pos KeyPress::Enter, ], "", ("ru", "stc"), ); assert_history( *mode, &["rustc", "cargo"], &[ KeyPress::Ctrl('R'), KeyPress::Char('r'), KeyPress::Char('u'), KeyPress::Right, // just to assert cursor pos KeyPress::Enter, ], "", ("r", "ustc"), ); assert_history( *mode, &["rustc", "cargo"], &[ KeyPress::Ctrl('R'), KeyPress::Char('r'), KeyPress::Ctrl('R'), KeyPress::Right, // just to assert cursor pos KeyPress::Enter, ], "", ("r", "ustc"), ); assert_history( *mode, &["rustc", "cargo"], &[ KeyPress::Ctrl('R'), KeyPress::Char('r'), KeyPress::Char('z'), // no match KeyPress::Right, // just to assert cursor pos KeyPress::Enter, ], "", ("car", "go"), ); assert_history( EditMode::Emacs, &["rustc", "cargo"], &[ KeyPress::Char('a'), KeyPress::Ctrl('R'), KeyPress::Char('r'), KeyPress::Ctrl('G'), // abort (FIXME: doesn't work with vi mode) KeyPress::Enter, ], "", ("a", ""), ); } } #[test] fn ctrl_r_with_long_prompt() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_history( *mode, &["rustc", "cargo"], &[KeyPress::Ctrl('R'), KeyPress::Char('o'), KeyPress::Enter], ">>>>>>>>>>>>>>>>>>>>>>>>>>> ", ("cargo", ""), ); } } #[test] fn ctrl_s() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_history( *mode, &["rustc", "cargo"], &[ KeyPress::Ctrl('R'), KeyPress::Char('r'), KeyPress::Ctrl('R'), KeyPress::Ctrl('S'), KeyPress::Right, // just to assert cursor pos KeyPress::Enter, ], "", ("car", "go"), ); } } #[test] fn meta_lt() { assert_history( EditMode::Emacs, &[""], &[KeyPress::Meta('<'), KeyPress::Enter], "", ("", ""), ); assert_history( EditMode::Emacs, &["rustc", "cargo"], &[KeyPress::Meta('<'), KeyPress::Enter], "", ("rustc", ""), ); } #[test] fn meta_gt() { assert_history( EditMode::Emacs, &[""], &[KeyPress::Meta('>'), KeyPress::Enter], "", ("", ""), ); assert_history( EditMode::Emacs, &["rustc", "cargo"], &[KeyPress::Meta('<'), KeyPress::Meta('>'), KeyPress::Enter], "", ("", ""), ); assert_history( EditMode::Emacs, &["rustc", "cargo"], &[ KeyPress::Char('a'), KeyPress::Meta('<'), KeyPress::Meta('>'), // restore original line KeyPress::Enter, ], "", ("a", ""), ); } rustyline-6.3.0/src/test/mod.rs010064400007650000024000000077311372733102500146430ustar 00000000000000use std::collections::HashMap; use std::sync::{Arc, RwLock}; use std::vec::IntoIter; use crate::completion::Completer; use crate::config::{Config, EditMode}; use crate::edit::init_state; use crate::highlight::Highlighter; use crate::hint::Hinter; use crate::keymap::{Cmd, InputState}; use crate::keys::KeyPress; use crate::tty::Sink; use crate::validate::Validator; use crate::{Context, Editor, Helper, Result}; mod common; mod emacs; mod history; mod vi_cmd; mod vi_insert; fn init_editor(mode: EditMode, keys: &[KeyPress]) -> Editor<()> { let config = Config::builder().edit_mode(mode).build(); let mut editor = Editor::<()>::with_config(config); editor.term.keys.extend(keys.iter().cloned()); editor } struct SimpleCompleter; impl Completer for SimpleCompleter { type Candidate = String; fn complete( &self, line: &str, _pos: usize, _ctx: &Context<'_>, ) -> Result<(usize, Vec)> { Ok((0, vec![line.to_owned() + "t"])) } } impl Helper for SimpleCompleter {} impl Hinter for SimpleCompleter {} impl Highlighter for SimpleCompleter {} impl Validator for SimpleCompleter {} #[test] fn complete_line() { let mut out = Sink::new(); let history = crate::history::History::new(); let helper = Some(SimpleCompleter); let mut s = init_state(&mut out, "rus", 3, helper.as_ref(), &history); let config = Config::default(); let mut input_state = InputState::new(&config, Arc::new(RwLock::new(HashMap::new()))); let keys = vec![KeyPress::Enter]; let mut rdr: IntoIter = keys.into_iter(); let cmd = super::complete_line(&mut rdr, &mut s, &mut input_state, &Config::default()).unwrap(); assert_eq!(Some(Cmd::AcceptLine), cmd); assert_eq!("rust", s.line.as_str()); assert_eq!(4, s.line.pos()); } // `keys`: keys to press // `expected_line`: line after enter key fn assert_line(mode: EditMode, keys: &[KeyPress], expected_line: &str) { let mut editor = init_editor(mode, keys); let actual_line = editor.readline(">>").unwrap(); assert_eq!(expected_line, actual_line); } // `initial`: line status before `keys` pressed: strings before and after cursor // `keys`: keys to press // `expected_line`: line after enter key fn assert_line_with_initial( mode: EditMode, initial: (&str, &str), keys: &[KeyPress], expected_line: &str, ) { let mut editor = init_editor(mode, keys); let actual_line = editor.readline_with_initial(">>", initial).unwrap(); assert_eq!(expected_line, actual_line); } // `initial`: line status before `keys` pressed: strings before and after cursor // `keys`: keys to press // `expected`: line status before enter key: strings before and after cursor fn assert_cursor(mode: EditMode, initial: (&str, &str), keys: &[KeyPress], expected: (&str, &str)) { let mut editor = init_editor(mode, keys); let actual_line = editor.readline_with_initial("", initial).unwrap(); assert_eq!(expected.0.to_owned() + expected.1, actual_line); assert_eq!(expected.0.len(), editor.term.cursor); } // `entries`: history entries before `keys` pressed // `keys`: keys to press // `expected`: line status before enter key: strings before and after cursor fn assert_history( mode: EditMode, entries: &[&str], keys: &[KeyPress], prompt: &str, expected: (&str, &str), ) { let mut editor = init_editor(mode, keys); for entry in entries { editor.history.add(*entry); } let actual_line = editor.readline(prompt).unwrap(); assert_eq!(expected.0.to_owned() + expected.1, actual_line); if prompt.is_empty() { assert_eq!(expected.0.len(), editor.term.cursor); } } #[test] fn unknown_esc_key() { for mode in &[EditMode::Emacs, EditMode::Vi] { assert_line(*mode, &[KeyPress::UnknownEscSeq, KeyPress::Enter], ""); } } #[test] fn test_send() { fn assert_send() {} assert_send::>(); } #[test] fn test_sync() { fn assert_sync() {} assert_sync::>(); } rustyline-6.3.0/src/test/vi_cmd.rs010064400007650000024000000334171364141226000153210ustar 00000000000000//! Vi command mode specific key bindings use super::{assert_cursor, assert_history}; use crate::config::EditMode; use crate::keys::KeyPress; #[test] fn dollar() { assert_cursor( EditMode::Vi, ("", "Hi"), &[KeyPress::Esc, KeyPress::Char('$'), KeyPress::Enter], ("Hi", ""), // FIXME ); } /*#[test] fn dot() { // TODO }*/ #[test] fn semi_colon() { assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[ KeyPress::Esc, KeyPress::Char('f'), KeyPress::Char('o'), KeyPress::Char(';'), KeyPress::Enter, ], ("Hello, w", "orld!"), ); } #[test] fn comma() { assert_cursor( EditMode::Vi, ("Hello, w", "orld!"), &[ KeyPress::Esc, KeyPress::Char('f'), KeyPress::Char('l'), KeyPress::Char(','), KeyPress::Enter, ], ("Hel", "lo, world!"), ); } #[test] fn zero() { assert_cursor( EditMode::Vi, ("Hi", ""), &[KeyPress::Esc, KeyPress::Char('0'), KeyPress::Enter], ("", "Hi"), ); } #[test] fn caret() { assert_cursor( EditMode::Vi, (" Hi", ""), &[KeyPress::Esc, KeyPress::Char('^'), KeyPress::Enter], (" ", "Hi"), ); } #[test] fn a() { assert_cursor( EditMode::Vi, ("B", "e"), &[ KeyPress::Esc, KeyPress::Char('a'), KeyPress::Char('y'), KeyPress::Enter, ], ("By", "e"), ); } #[test] fn uppercase_a() { assert_cursor( EditMode::Vi, ("", "By"), &[ KeyPress::Esc, KeyPress::Char('A'), KeyPress::Char('e'), KeyPress::Enter, ], ("Bye", ""), ); } #[test] fn b() { assert_cursor( EditMode::Vi, ("Hello, world!", ""), &[KeyPress::Esc, KeyPress::Char('b'), KeyPress::Enter], ("Hello, ", "world!"), ); assert_cursor( EditMode::Vi, ("Hello, world!", ""), &[ KeyPress::Esc, KeyPress::Char('2'), KeyPress::Char('b'), KeyPress::Enter, ], ("Hello", ", world!"), ); } #[test] fn uppercase_b() { assert_cursor( EditMode::Vi, ("Hello, world!", ""), &[KeyPress::Esc, KeyPress::Char('B'), KeyPress::Enter], ("Hello, ", "world!"), ); assert_cursor( EditMode::Vi, ("Hello, world!", ""), &[ KeyPress::Esc, KeyPress::Char('2'), KeyPress::Char('B'), KeyPress::Enter, ], ("", "Hello, world!"), ); } #[test] fn uppercase_c() { assert_cursor( EditMode::Vi, ("Hello, w", "orld!"), &[ KeyPress::Esc, KeyPress::Char('C'), KeyPress::Char('i'), KeyPress::Enter, ], ("Hello, i", ""), ); } #[test] fn ctrl_k() { for key in &[KeyPress::Char('D'), KeyPress::Ctrl('K')] { assert_cursor( EditMode::Vi, ("Hi", ""), &[KeyPress::Esc, *key, KeyPress::Enter], ("H", ""), ); assert_cursor( EditMode::Vi, ("", "Hi"), &[KeyPress::Esc, *key, KeyPress::Enter], ("", ""), ); assert_cursor( EditMode::Vi, ("By", "e"), &[KeyPress::Esc, *key, KeyPress::Enter], ("B", ""), ); } } #[test] fn e() { assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[KeyPress::Esc, KeyPress::Char('e'), KeyPress::Enter], ("Hell", "o, world!"), ); assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[ KeyPress::Esc, KeyPress::Char('2'), KeyPress::Char('e'), KeyPress::Enter, ], ("Hello, worl", "d!"), ); } #[test] fn uppercase_e() { assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[KeyPress::Esc, KeyPress::Char('E'), KeyPress::Enter], ("Hello", ", world!"), ); assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[ KeyPress::Esc, KeyPress::Char('2'), KeyPress::Char('E'), KeyPress::Enter, ], ("Hello, world", "!"), ); } #[test] fn f() { assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[ KeyPress::Esc, KeyPress::Char('f'), KeyPress::Char('r'), KeyPress::Enter, ], ("Hello, wo", "rld!"), ); assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[ KeyPress::Esc, KeyPress::Char('3'), KeyPress::Char('f'), KeyPress::Char('l'), KeyPress::Enter, ], ("Hello, wor", "ld!"), ); } #[test] fn uppercase_f() { assert_cursor( EditMode::Vi, ("Hello, world!", ""), &[ KeyPress::Esc, KeyPress::Char('F'), KeyPress::Char('r'), KeyPress::Enter, ], ("Hello, wo", "rld!"), ); assert_cursor( EditMode::Vi, ("Hello, world!", ""), &[ KeyPress::Esc, KeyPress::Char('3'), KeyPress::Char('F'), KeyPress::Char('l'), KeyPress::Enter, ], ("He", "llo, world!"), ); } #[test] fn i() { assert_cursor( EditMode::Vi, ("Be", ""), &[ KeyPress::Esc, KeyPress::Char('i'), KeyPress::Char('y'), KeyPress::Enter, ], ("By", "e"), ); } #[test] fn uppercase_i() { assert_cursor( EditMode::Vi, ("Be", ""), &[ KeyPress::Esc, KeyPress::Char('I'), KeyPress::Char('y'), KeyPress::Enter, ], ("y", "Be"), ); } #[test] fn u() { assert_cursor( EditMode::Vi, ("Hello, ", "world"), &[ KeyPress::Esc, KeyPress::Ctrl('W'), KeyPress::Char('u'), KeyPress::Enter, ], ("Hello,", " world"), ); } #[test] fn w() { assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[KeyPress::Esc, KeyPress::Char('w'), KeyPress::Enter], ("Hello", ", world!"), ); assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[ KeyPress::Esc, KeyPress::Char('2'), KeyPress::Char('w'), KeyPress::Enter, ], ("Hello, ", "world!"), ); } #[test] fn uppercase_w() { assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[KeyPress::Esc, KeyPress::Char('W'), KeyPress::Enter], ("Hello, ", "world!"), ); assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[ KeyPress::Esc, KeyPress::Char('2'), KeyPress::Char('W'), KeyPress::Enter, ], ("Hello, world", "!"), ); } #[test] fn x() { assert_cursor( EditMode::Vi, ("", "a"), &[KeyPress::Esc, KeyPress::Char('x'), KeyPress::Enter], ("", ""), ); } #[test] fn uppercase_x() { assert_cursor( EditMode::Vi, ("Hi", ""), &[KeyPress::Esc, KeyPress::Char('X'), KeyPress::Enter], ("", "i"), ); } #[test] fn h() { for key in &[ KeyPress::Char('h'), KeyPress::Ctrl('H'), KeyPress::Backspace, ] { assert_cursor( EditMode::Vi, ("Bye", ""), &[KeyPress::Esc, *key, KeyPress::Enter], ("B", "ye"), ); assert_cursor( EditMode::Vi, ("Bye", ""), &[KeyPress::Esc, KeyPress::Char('2'), *key, KeyPress::Enter], ("", "Bye"), ); } } #[test] fn l() { for key in &[KeyPress::Char('l'), KeyPress::Char(' ')] { assert_cursor( EditMode::Vi, ("", "Hi"), &[KeyPress::Esc, *key, KeyPress::Enter], ("H", "i"), ); assert_cursor( EditMode::Vi, ("", "Hi"), &[KeyPress::Esc, KeyPress::Char('2'), *key, KeyPress::Enter], ("Hi", ""), ); } } #[test] fn j() { for key in &[KeyPress::Char('j'), KeyPress::Char('+')] { assert_cursor( EditMode::Vi, ("Hel", "lo,\nworld!"), // NOTE: escape moves backwards on char &[KeyPress::Esc, *key, KeyPress::Enter], ("Hello,\nwo", "rld!"), ); assert_cursor( EditMode::Vi, ("", "One\nTwo\nThree"), &[KeyPress::Esc, KeyPress::Char('2'), *key, KeyPress::Enter], ("One\nTwo\n", "Three"), ); assert_cursor( EditMode::Vi, ("Hel", "lo,\nworld!"), // NOTE: escape moves backwards on char &[KeyPress::Esc, KeyPress::Char('7'), *key, KeyPress::Enter], ("Hello,\nwo", "rld!"), ); } } #[test] fn k() { for key in &[KeyPress::Char('k'), KeyPress::Char('-')] { assert_cursor( EditMode::Vi, ("Hello,\nworl", "d!"), // NOTE: escape moves backwards on char &[KeyPress::Esc, *key, KeyPress::Enter], ("Hel", "lo,\nworld!"), ); assert_cursor( EditMode::Vi, ("One\nTwo\nT", "hree"), // NOTE: escape moves backwards on char &[KeyPress::Esc, KeyPress::Char('2'), *key, KeyPress::Enter], ("", "One\nTwo\nThree"), ); assert_cursor( EditMode::Vi, ("Hello,\nworl", "d!"), // NOTE: escape moves backwards on char &[KeyPress::Esc, KeyPress::Char('5'), *key, KeyPress::Enter], ("Hel", "lo,\nworld!"), ); assert_cursor( EditMode::Vi, ("first line\nshort\nlong line", ""), &[KeyPress::Esc, *key, KeyPress::Enter], ("first line\nshort", "\nlong line"), ); } } #[test] fn ctrl_n() { for key in &[KeyPress::Ctrl('N')] { assert_history( EditMode::Vi, &["line1", "line2"], &[ KeyPress::Esc, KeyPress::Ctrl('P'), KeyPress::Ctrl('P'), *key, KeyPress::Enter, ], "", ("line2", ""), ); } } #[test] fn ctrl_p() { for key in &[KeyPress::Ctrl('P')] { assert_history( EditMode::Vi, &["line1"], &[KeyPress::Esc, *key, KeyPress::Enter], "", ("line1", ""), ); } } #[test] fn p() { assert_cursor( EditMode::Vi, ("Hello, ", "world"), &[ KeyPress::Esc, KeyPress::Ctrl('W'), KeyPress::Char('p'), KeyPress::Enter, ], (" Hello", ",world"), ); } #[test] fn uppercase_p() { assert_cursor( EditMode::Vi, ("Hello, ", "world"), &[ KeyPress::Esc, KeyPress::Ctrl('W'), KeyPress::Char('P'), KeyPress::Enter, ], ("Hello", ", world"), ); } #[test] fn r() { assert_cursor( EditMode::Vi, ("Hi", ", world!"), &[ KeyPress::Esc, KeyPress::Char('r'), KeyPress::Char('o'), KeyPress::Enter, ], ("H", "o, world!"), ); assert_cursor( EditMode::Vi, ("He", "llo, world!"), &[ KeyPress::Esc, KeyPress::Char('4'), KeyPress::Char('r'), KeyPress::Char('i'), KeyPress::Enter, ], ("Hiii", "i, world!"), ); } #[test] fn s() { assert_cursor( EditMode::Vi, ("Hi", ", world!"), &[ KeyPress::Esc, KeyPress::Char('s'), KeyPress::Char('o'), KeyPress::Enter, ], ("Ho", ", world!"), ); assert_cursor( EditMode::Vi, ("He", "llo, world!"), &[ KeyPress::Esc, KeyPress::Char('4'), KeyPress::Char('s'), KeyPress::Char('i'), KeyPress::Enter, ], ("Hi", ", world!"), ); } #[test] fn uppercase_s() { assert_cursor( EditMode::Vi, ("Hello, ", "world"), &[KeyPress::Esc, KeyPress::Char('S'), KeyPress::Enter], ("", ""), ); } #[test] fn t() { assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[ KeyPress::Esc, KeyPress::Char('t'), KeyPress::Char('r'), KeyPress::Enter, ], ("Hello, w", "orld!"), ); assert_cursor( EditMode::Vi, ("", "Hello, world!"), &[ KeyPress::Esc, KeyPress::Char('3'), KeyPress::Char('t'), KeyPress::Char('l'), KeyPress::Enter, ], ("Hello, wo", "rld!"), ); } #[test] fn uppercase_t() { assert_cursor( EditMode::Vi, ("Hello, world!", ""), &[ KeyPress::Esc, KeyPress::Char('T'), KeyPress::Char('r'), KeyPress::Enter, ], ("Hello, wor", "ld!"), ); assert_cursor( EditMode::Vi, ("Hello, world!", ""), &[ KeyPress::Esc, KeyPress::Char('3'), KeyPress::Char('T'), KeyPress::Char('l'), KeyPress::Enter, ], ("Hel", "lo, world!"), ); } rustyline-6.3.0/src/test/vi_insert.rs010064400007650000024000000020501346010623700160520ustar 00000000000000//! Vi insert mode specific key bindings use super::assert_cursor; use crate::config::EditMode; use crate::keys::KeyPress; #[test] fn insert_mode_by_default() { assert_cursor( EditMode::Vi, ("", ""), &[KeyPress::Char('a'), KeyPress::Enter], ("a", ""), ); } #[test] fn ctrl_h() { assert_cursor( EditMode::Vi, ("Hi", ""), &[KeyPress::Ctrl('H'), KeyPress::Enter], ("H", ""), ); } #[test] fn backspace() { assert_cursor( EditMode::Vi, ("", ""), &[KeyPress::Backspace, KeyPress::Enter], ("", ""), ); assert_cursor( EditMode::Vi, ("Hi", ""), &[KeyPress::Backspace, KeyPress::Enter], ("H", ""), ); assert_cursor( EditMode::Vi, ("", "Hi"), &[KeyPress::Backspace, KeyPress::Enter], ("", "Hi"), ); } #[test] fn esc() { assert_cursor( EditMode::Vi, ("", ""), &[KeyPress::Char('a'), KeyPress::Esc, KeyPress::Enter], ("", "a"), ); } rustyline-6.3.0/src/tty/mod.rs010064400007650000024000000157401365034366500145130ustar 00000000000000//! This module implements and describes common TTY methods & traits use unicode_width::UnicodeWidthStr; use crate::config::{BellStyle, ColorMode, Config, OutputStreamType}; use crate::highlight::Highlighter; use crate::keys::KeyPress; use crate::layout::{Layout, Position}; use crate::line_buffer::LineBuffer; use crate::Result; /// Terminal state pub trait RawMode: Sized { /// Disable RAW mode for the terminal. fn disable_raw_mode(&self) -> Result<()>; } /// Translate bytes read from stdin to keys. pub trait RawReader { /// Blocking read of key pressed. fn next_key(&mut self, single_esc_abort: bool) -> Result; /// For CTRL-V support #[cfg(unix)] fn next_char(&mut self) -> Result; /// Bracketed paste fn read_pasted_text(&mut self) -> Result; } /// Display prompt, line and cursor in terminal output pub trait Renderer { type Reader: RawReader; fn move_cursor(&mut self, old: Position, new: Position) -> Result<()>; /// Display `prompt`, line and cursor in terminal output #[allow(clippy::too_many_arguments)] fn refresh_line( &mut self, prompt: &str, line: &LineBuffer, hint: Option<&str>, old_layout: &Layout, new_layout: &Layout, highlighter: Option<&dyn Highlighter>, ) -> Result<()>; /// 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(&self, buf: &[u8]) -> Result<()>; /// Beep, used for completion when there is nothing to complete or when all /// the choices were already shown. fn beep(&mut self) -> Result<()>; /// Clear the screen. Used to handle ctrl+l fn clear_screen(&mut self) -> Result<()>; /// Check if a SIGWINCH signal has been received fn sigwinch(&self) -> bool; /// Update the number of columns/rows in the current terminal. fn update_size(&mut self); /// Get the number of columns in the current terminal. fn get_columns(&self) -> usize; /// Get the number of rows in the current terminal. fn get_rows(&self) -> usize; /// Check if output supports colors. fn colors_enabled(&self) -> bool; /// Make sure prompt is at the leftmost edge of the screen fn move_cursor_at_leftmost(&mut self, rdr: &mut Self::Reader) -> Result<()>; } impl<'a, R: Renderer + ?Sized> Renderer for &'a mut R { type Reader = R::Reader; fn move_cursor(&mut self, old: Position, new: Position) -> Result<()> { (**self).move_cursor(old, new) } fn refresh_line( &mut self, prompt: &str, line: &LineBuffer, hint: Option<&str>, old_layout: &Layout, new_layout: &Layout, highlighter: Option<&dyn Highlighter>, ) -> Result<()> { (**self).refresh_line(prompt, line, hint, old_layout, new_layout, highlighter) } fn calculate_position(&self, s: &str, orig: Position) -> Position { (**self).calculate_position(s, orig) } fn write_and_flush(&self, buf: &[u8]) -> Result<()> { (**self).write_and_flush(buf) } fn beep(&mut self) -> Result<()> { (**self).beep() } fn clear_screen(&mut self) -> Result<()> { (**self).clear_screen() } fn sigwinch(&self) -> bool { (**self).sigwinch() } fn update_size(&mut self) { (**self).update_size() } fn get_columns(&self) -> usize { (**self).get_columns() } fn get_rows(&self) -> usize { (**self).get_rows() } fn colors_enabled(&self) -> bool { (**self).colors_enabled() } fn move_cursor_at_leftmost(&mut self, rdr: &mut R::Reader) -> Result<()> { (**self).move_cursor_at_leftmost(rdr) } } // ignore ANSI escape sequence fn width(s: &str, esc_seq: &mut u8) -> usize { if *esc_seq == 1 { if s == "[" { // CSI *esc_seq = 2; } else { // two-character sequence *esc_seq = 0; } 0 } else if *esc_seq == 2 { if s == ";" || (s.as_bytes()[0] >= b'0' && s.as_bytes()[0] <= b'9') { /*} else if s == "m" { // last *esc_seq = 0;*/ } else { // not supported *esc_seq = 0; } 0 } else if s == "\x1b" { *esc_seq = 1; 0 } else if s == "\n" { 0 } else { s.width() } } /// Terminal contract pub trait Term { type Reader: RawReader; // rl_instream type Writer: Renderer; // rl_outstream type Mode: RawMode; fn new( color_mode: ColorMode, stream: OutputStreamType, tab_stop: usize, bell_style: BellStyle, ) -> Self; /// Check if current terminal can provide a rich line-editing user /// interface. fn is_unsupported(&self) -> bool; /// check if stdin is connected to a terminal. fn is_stdin_tty(&self) -> bool; /// check if output stream is connected to a terminal. fn is_output_tty(&self) -> bool; /// Enable RAW mode for the terminal. fn enable_raw_mode(&mut self) -> Result; /// Create a RAW reader fn create_reader(&self, config: &Config) -> Result; /// Create a writer fn create_writer(&self) -> Self::Writer; } // If on Windows platform import Windows TTY module // and re-export into mod.rs scope #[cfg(all(windows, not(target_arch = "wasm32")))] mod windows; #[cfg(all(windows, not(target_arch = "wasm32")))] pub use self::windows::*; // If on Unix platform import Unix TTY module // and re-export into mod.rs scope #[cfg(all(unix, not(target_arch = "wasm32")))] mod unix; #[cfg(all(unix, not(target_arch = "wasm32")))] pub use self::unix::*; #[cfg(any(test, target_arch = "wasm32"))] mod test; #[cfg(any(test, target_arch = "wasm32"))] pub use self::test::*; rustyline-6.3.0/src/tty/test.rs010064400007650000024000000077341366622224700147160ustar 00000000000000//! Tests specific definitions use std::iter::IntoIterator; use std::slice::Iter; use std::vec::IntoIter; use super::{RawMode, RawReader, Renderer, Term}; use crate::config::{BellStyle, ColorMode, Config, OutputStreamType}; use crate::error::ReadlineError; use crate::highlight::Highlighter; use crate::keys::KeyPress; use crate::layout::{Layout, Position}; use crate::line_buffer::LineBuffer; use crate::Result; pub type Mode = (); impl RawMode for Mode { fn disable_raw_mode(&self) -> Result<()> { Ok(()) } } impl<'a> RawReader for Iter<'a, KeyPress> { fn next_key(&mut self, _: bool) -> Result { match self.next() { Some(key) => Ok(*key), None => Err(ReadlineError::Eof), } } #[cfg(unix)] fn next_char(&mut self) -> Result { unimplemented!(); } fn read_pasted_text(&mut self) -> Result { unimplemented!() } } impl RawReader for IntoIter { fn next_key(&mut self, _: bool) -> Result { match self.next() { Some(key) => Ok(key), None => Err(ReadlineError::Eof), } } #[cfg(unix)] fn next_char(&mut self) -> Result { match self.next() { Some(KeyPress::Char(c)) => Ok(c), None => Err(ReadlineError::Eof), _ => unimplemented!(), } } fn read_pasted_text(&mut self) -> Result { unimplemented!() } } pub struct Sink {} impl Sink { pub fn new() -> Sink { Sink {} } } impl Renderer for Sink { type Reader = IntoIter; fn move_cursor(&mut self, _: Position, _: Position) -> Result<()> { Ok(()) } fn refresh_line( &mut self, _prompt: &str, _line: &LineBuffer, _hint: Option<&str>, _old_layout: &Layout, _new_layout: &Layout, _highlighter: Option<&dyn Highlighter>, ) -> Result<()> { Ok(()) } fn calculate_position(&self, s: &str, orig: Position) -> Position { let mut pos = orig; pos.col += s.len(); pos } fn write_and_flush(&self, _: &[u8]) -> Result<()> { Ok(()) } fn beep(&mut self) -> Result<()> { Ok(()) } fn clear_screen(&mut self) -> Result<()> { Ok(()) } fn sigwinch(&self) -> bool { false } fn update_size(&mut self) {} fn get_columns(&self) -> usize { 80 } fn get_rows(&self) -> usize { 24 } fn colors_enabled(&self) -> bool { false } fn move_cursor_at_leftmost(&mut self, _: &mut IntoIter) -> Result<()> { Ok(()) } } pub type Terminal = DummyTerminal; #[derive(Clone, Debug)] pub struct DummyTerminal { pub keys: Vec, pub cursor: usize, // cursor position before last command pub color_mode: ColorMode, pub bell_style: BellStyle, } impl Term for DummyTerminal { type Mode = Mode; type Reader = IntoIter; type Writer = Sink; fn new( color_mode: ColorMode, _stream: OutputStreamType, _tab_stop: usize, bell_style: BellStyle, ) -> DummyTerminal { DummyTerminal { keys: Vec::new(), cursor: 0, color_mode, bell_style, } } // Init checks: #[cfg(not(target_arch = "wasm32"))] fn is_unsupported(&self) -> bool { false } #[cfg(target_arch = "wasm32")] fn is_unsupported(&self) -> bool { true } fn is_stdin_tty(&self) -> bool { true } fn is_output_tty(&self) -> bool { false } // Interactive loop: fn enable_raw_mode(&mut self) -> Result { Ok(()) } fn create_reader(&self, _: &Config) -> Result> { Ok(self.keys.clone().into_iter()) } fn create_writer(&self) -> Sink { Sink::new() } } #[cfg(unix)] pub fn suspend() -> Result<()> { Ok(()) } rustyline-6.3.0/src/tty/unix.rs010064400007650000024000000763221372023615700147150ustar 00000000000000//! Unix specific definitions use std::cmp::Ordering; use std::io::{self, Read, Write}; use std::os::unix::io::{AsRawFd, RawFd}; use std::sync; use std::sync::atomic; use log::{debug, warn}; use nix::poll::{self, PollFlags}; use nix::sys::signal; use nix::sys::termios; use nix::sys::termios::SetArg; use unicode_segmentation::UnicodeSegmentation; use utf8parse::{Parser, Receiver}; use super::{width, RawMode, RawReader, Renderer, Term}; use crate::config::{BellStyle, ColorMode, Config, OutputStreamType}; use crate::error; use crate::highlight::Highlighter; use crate::keys::{self, KeyPress}; use crate::layout::{Layout, Position}; use crate::line_buffer::LineBuffer; use crate::Result; const STDIN_FILENO: RawFd = libc::STDIN_FILENO; /// Unsupported Terminals that don't support RAW mode const UNSUPPORTED_TERM: [&str; 3] = ["dumb", "cons25", "emacs"]; const BRACKETED_PASTE_ON: &[u8] = b"\x1b[?2004h"; const BRACKETED_PASTE_OFF: &[u8] = b"\x1b[?2004l"; impl AsRawFd for OutputStreamType { fn as_raw_fd(&self) -> RawFd { match self { OutputStreamType::Stdout => libc::STDOUT_FILENO, OutputStreamType::Stderr => libc::STDERR_FILENO, } } } nix::ioctl_read_bad!(win_size, libc::TIOCGWINSZ, libc::winsize); #[allow(clippy::useless_conversion)] fn get_win_size(fileno: &T) -> (usize, usize) { use std::mem::zeroed; if cfg!(test) { return (80, 24); } unsafe { let mut size: libc::winsize = zeroed(); match win_size(fileno.as_raw_fd(), &mut size) { Ok(0) => { // 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 // inifinite rows let cols = if size.ws_col == 0 { 80 } else { size.ws_col as usize }; let rows = if size.ws_row == 0 { usize::max_value() } 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 { unsafe { libc::isatty(fd) != 0 } } pub struct PosixMode { termios: termios::Termios, out: Option, } #[cfg(not(test))] pub type Mode = PosixMode; impl RawMode for PosixMode { /// Disable RAW mode for the terminal. fn disable_raw_mode(&self) -> Result<()> { termios::tcsetattr(STDIN_FILENO, SetArg::TCSADRAIN, &self.termios)?; // disable bracketed paste if let Some(out) = self.out { write_and_flush(out, BRACKETED_PASTE_OFF)?; } Ok(()) } } // Rust std::io::Stdin is buffered with no way to know if bytes are available. // So we use low-level stuff instead... struct StdinRaw {} impl Read for StdinRaw { fn read(&mut self, buf: &mut [u8]) -> io::Result { loop { let res = unsafe { libc::read( STDIN_FILENO, buf.as_mut_ptr() as *mut libc::c_void, buf.len() as libc::size_t, ) }; if res == -1 { let error = io::Error::last_os_error(); if error.kind() != io::ErrorKind::Interrupted || SIGWINCH.load(atomic::Ordering::Relaxed) { return Err(error); } } else { #[allow(clippy::cast_sign_loss)] return Ok(res as usize); } } } } /// Console input reader pub struct PosixRawReader { stdin: StdinRaw, timeout_ms: i32, buf: [u8; 1], parser: Parser, receiver: Utf8, } struct Utf8 { c: Option, valid: bool, } impl PosixRawReader { fn new(config: &Config) -> Result { Ok(Self { stdin: StdinRaw {}, timeout_ms: config.keyseq_timeout(), buf: [0; 1], parser: Parser::new(), receiver: Utf8 { c: None, valid: true, }, }) } /// Handle ESC sequences fn escape_sequence(&mut self) -> Result { // Read the next byte representing the escape sequence. let seq1 = self.next_char()?; if seq1 == '[' { // ESC [ sequences. (CSI) self.escape_csi() } else if seq1 == 'O' { // xterm // ESC O sequences. (SS3) self.escape_o() } else if seq1 == '\x1b' { // ESC ESC Ok(KeyPress::Esc) } else { // TODO ESC-R (r): Undo all changes made to this line. Ok(KeyPress::Meta(seq1)) } } /// Handle ESC [ escape sequences fn escape_csi(&mut self) -> Result { let seq2 = self.next_char()?; if seq2.is_digit(10) { match seq2 { '0' | '9' => { debug!(target: "rustyline", "unsupported esc sequence: ESC [ {:?}", seq2); Ok(KeyPress::UnknownEscSeq) } _ => { // Extended escape, read additional byte. self.extended_escape(seq2) } } } else if seq2 == '[' { let seq3 = self.next_char()?; // Linux console Ok(match seq3 { 'A' => KeyPress::F(1), 'B' => KeyPress::F(2), 'C' => KeyPress::F(3), 'D' => KeyPress::F(4), 'E' => KeyPress::F(5), _ => { debug!(target: "rustyline", "unsupported esc sequence: ESC [ [ {:?}", seq3); KeyPress::UnknownEscSeq } }) } else { // ANSI Ok(match seq2 { 'A' => KeyPress::Up, // kcuu1 'B' => KeyPress::Down, // kcud1 'C' => KeyPress::Right, // kcuf1 'D' => KeyPress::Left, // kcub1 'F' => KeyPress::End, 'H' => KeyPress::Home, // khome 'Z' => KeyPress::BackTab, _ => { debug!(target: "rustyline", "unsupported esc sequence: ESC [ {:?}", seq2); KeyPress::UnknownEscSeq } }) } } /// Handle ESC [ escape sequences #[allow(clippy::cognitive_complexity)] fn extended_escape(&mut self, seq2: char) -> Result { let seq3 = self.next_char()?; if seq3 == '~' { Ok(match seq2 { '1' | '7' => KeyPress::Home, // tmux, xrvt '2' => KeyPress::Insert, '3' => KeyPress::Delete, // kdch1 '4' | '8' => KeyPress::End, // tmux, xrvt '5' => KeyPress::PageUp, // kpp '6' => KeyPress::PageDown, // knp _ => { debug!(target: "rustyline", "unsupported esc sequence: ESC [ {} ~", seq2); KeyPress::UnknownEscSeq } }) } else if seq3.is_digit(10) { let seq4 = self.next_char()?; if seq4 == '~' { Ok(match (seq2, seq3) { ('1', '1') => KeyPress::F(1), // rxvt-unicode ('1', '2') => KeyPress::F(2), // rxvt-unicode ('1', '3') => KeyPress::F(3), // rxvt-unicode ('1', '4') => KeyPress::F(4), // rxvt-unicode ('1', '5') => KeyPress::F(5), // kf5 ('1', '7') => KeyPress::F(6), // kf6 ('1', '8') => KeyPress::F(7), // kf7 ('1', '9') => KeyPress::F(8), // kf8 ('2', '0') => KeyPress::F(9), // kf9 ('2', '1') => KeyPress::F(10), // kf10 ('2', '3') => KeyPress::F(11), // kf11 ('2', '4') => KeyPress::F(12), // kf12 _ => { debug!(target: "rustyline", "unsupported esc sequence: ESC [ {}{} ~", seq2, seq3); KeyPress::UnknownEscSeq } }) } else if seq4 == ';' { let seq5 = self.next_char()?; if seq5.is_digit(10) { let seq6 = self.next_char()?; if seq6.is_digit(10) { self.next_char()?; // 'R' expected } else if seq6 == 'R' { } else { debug!(target: "rustyline", "unsupported esc sequence: ESC [ {}{} ; {} {}", seq2, seq3, seq5, seq6); } } else { debug!(target: "rustyline", "unsupported esc sequence: ESC [ {}{} ; {:?}", seq2, seq3, seq5); } Ok(KeyPress::UnknownEscSeq) } else if seq4.is_digit(10) { let seq5 = self.next_char()?; if seq5 == '~' { Ok(match (seq2, seq3, seq4) { ('2', '0', '0') => KeyPress::BracketedPasteStart, ('2', '0', '1') => KeyPress::BracketedPasteEnd, _ => { debug!(target: "rustyline", "unsupported esc sequence: ESC [ {}{}{}~", seq2, seq3, seq4); KeyPress::UnknownEscSeq } }) } else { debug!(target: "rustyline", "unsupported esc sequence: ESC [ {}{}{} {}", seq2, seq3, seq4, seq5); Ok(KeyPress::UnknownEscSeq) } } else { debug!(target: "rustyline", "unsupported esc sequence: ESC [ {}{} {:?}", seq2, seq3, seq4); Ok(KeyPress::UnknownEscSeq) } } else if seq3 == ';' { let seq4 = self.next_char()?; if seq4.is_digit(10) { let seq5 = self.next_char()?; if seq5.is_digit(10) { self.next_char()?; // 'R' expected Ok(KeyPress::UnknownEscSeq) } else if seq2 == '1' { Ok(match (seq4, seq5) { ('5', 'A') => KeyPress::ControlUp, ('5', 'B') => KeyPress::ControlDown, ('5', 'C') => KeyPress::ControlRight, ('5', 'D') => KeyPress::ControlLeft, ('2', 'A') => KeyPress::ShiftUp, ('2', 'B') => KeyPress::ShiftDown, ('2', 'C') => KeyPress::ShiftRight, ('2', 'D') => KeyPress::ShiftLeft, _ => { debug!(target: "rustyline", "unsupported esc sequence: ESC [ 1 ; {} {:?}", seq4, seq5); KeyPress::UnknownEscSeq } }) } else { debug!(target: "rustyline", "unsupported esc sequence: ESC [ {} ; {} {:?}", seq2, seq4, seq5); Ok(KeyPress::UnknownEscSeq) } } else { debug!(target: "rustyline", "unsupported esc sequence: ESC [ {} ; {:?}", seq2, seq4); Ok(KeyPress::UnknownEscSeq) } } else { Ok(match (seq2, seq3) { ('5', 'A') => KeyPress::ControlUp, ('5', 'B') => KeyPress::ControlDown, ('5', 'C') => KeyPress::ControlRight, ('5', 'D') => KeyPress::ControlLeft, _ => { debug!(target: "rustyline", "unsupported esc sequence: ESC [ {} {:?}", seq2, seq3); KeyPress::UnknownEscSeq } }) } } /// Handle ESC O escape sequences fn escape_o(&mut self) -> Result { let seq2 = self.next_char()?; Ok(match seq2 { 'A' => KeyPress::Up, // kcuu1 'B' => KeyPress::Down, // kcud1 'C' => KeyPress::Right, // kcuf1 'D' => KeyPress::Left, // kcub1 'F' => KeyPress::End, // kend 'H' => KeyPress::Home, // khome 'P' => KeyPress::F(1), // kf1 'Q' => KeyPress::F(2), // kf2 'R' => KeyPress::F(3), // kf3 'S' => KeyPress::F(4), // kf4 'a' => KeyPress::ControlUp, 'b' => KeyPress::ControlDown, 'c' => KeyPress::ControlRight, // rxvt 'd' => KeyPress::ControlLeft, // rxvt _ => { debug!(target: "rustyline", "unsupported esc sequence: ESC O {:?}", seq2); KeyPress::UnknownEscSeq } }) } fn poll(&mut self, timeout_ms: i32) -> ::nix::Result { let mut fds = [poll::PollFd::new(STDIN_FILENO, PollFlags::POLLIN)]; poll::poll(&mut fds, timeout_ms) } } impl RawReader for PosixRawReader { fn next_key(&mut self, single_esc_abort: bool) -> Result { let c = self.next_char()?; let mut key = keys::char_to_key_press(c); if key == KeyPress::Esc { let timeout_ms = if single_esc_abort && self.timeout_ms == -1 { 0 } else { self.timeout_ms }; match self.poll(timeout_ms) { Ok(n) if n == 0 => { // single escape } Ok(_) => { // escape sequence key = self.escape_sequence()? } // Err(ref e) if e.kind() == ErrorKind::Interrupted => continue, Err(e) => return Err(e.into()), } } debug!(target: "rustyline", "key: {:?}", key); Ok(key) } fn next_char(&mut self) -> Result { loop { let n = self.stdin.read(&mut self.buf)?; if n == 0 { return Err(error::ReadlineError::Eof); } let b = self.buf[0]; self.parser.advance(&mut self.receiver, b); if !self.receiver.valid { return Err(error::ReadlineError::Utf8Error); } else if let Some(c) = self.receiver.c.take() { return Ok(c); } } } fn read_pasted_text(&mut self) -> Result { let mut buffer = String::new(); loop { match self.next_char()? { '\x1b' => { let key = self.escape_sequence()?; if key == KeyPress::BracketedPasteEnd { break; } else { continue; // TODO validate } } c => buffer.push(c), }; } let buffer = buffer.replace("\r\n", "\n"); let buffer = buffer.replace("\r", "\n"); Ok(buffer) } } impl Receiver for Utf8 { /// Called whenever a code point is parsed successfully fn codepoint(&mut self, c: char) { self.c = Some(c); self.valid = true; } /// Called when an invalid_sequence is detected fn invalid_sequence(&mut self) { self.c = None; self.valid = false; } } /// Console output writer pub struct PosixRenderer { out: OutputStreamType, cols: usize, // Number of columns in terminal buffer: String, tab_stop: usize, colors_enabled: bool, bell_style: BellStyle, } impl PosixRenderer { fn new( out: OutputStreamType, tab_stop: usize, colors_enabled: bool, bell_style: BellStyle, ) -> Self { let (cols, _) = get_win_size(&out); Self { out, cols, buffer: String::with_capacity(1024), tab_stop, colors_enabled, bell_style, } } fn clear_old_rows(&mut self, layout: &Layout) { use std::fmt::Write; let current_row = layout.cursor.row; let old_rows = layout.end.row; // old_rows < cursor_row if the prompt spans multiple lines and if // this is the default State. let cursor_row_movement = old_rows.saturating_sub(current_row); // move the cursor down as required if cursor_row_movement > 0 { write!(self.buffer, "\x1b[{}B", cursor_row_movement).unwrap(); } // clear old rows for _ in 0..old_rows { self.buffer.push_str("\r\x1b[0K\x1b[A"); } // clear the line self.buffer.push_str("\r\x1b[0K"); } } impl Renderer for PosixRenderer { type Reader = PosixRawReader; fn move_cursor(&mut self, old: Position, new: Position) -> Result<()> { use std::fmt::Write; self.buffer.clear(); let row_ordering = new.row.cmp(&old.row); if row_ordering == Ordering::Greater { // move down let row_shift = new.row - old.row; if row_shift == 1 { self.buffer.push_str("\x1b[B"); } else { write!(self.buffer, "\x1b[{}B", row_shift).unwrap(); } } else if row_ordering == Ordering::Less { // move up let row_shift = old.row - new.row; if row_shift == 1 { self.buffer.push_str("\x1b[A"); } else { write!(self.buffer, "\x1b[{}A", row_shift).unwrap(); } } let col_ordering = new.col.cmp(&old.col); if col_ordering == Ordering::Greater { // move right let col_shift = new.col - old.col; if col_shift == 1 { self.buffer.push_str("\x1b[C"); } else { write!(self.buffer, "\x1b[{}C", col_shift).unwrap(); } } else if col_ordering == Ordering::Less { // move left let col_shift = old.col - new.col; if col_shift == 1 { self.buffer.push_str("\x1b[D"); } else { write!(self.buffer, "\x1b[{}D", col_shift).unwrap(); } } self.write_and_flush(self.buffer.as_bytes()) } fn refresh_line( &mut self, prompt: &str, line: &LineBuffer, hint: Option<&str>, old_layout: &Layout, new_layout: &Layout, highlighter: Option<&dyn Highlighter>, ) -> Result<()> { use std::fmt::Write; self.buffer.clear(); let default_prompt = new_layout.default_prompt; let cursor = new_layout.cursor; let end_pos = new_layout.end; 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 && !self.buffer.ends_with('\n') { self.buffer.push_str("\n"); } // position the cursor let new_cursor_row_movement = end_pos.row - cursor.row; // move the cursor up as required if new_cursor_row_movement > 0 { write!(self.buffer, "\x1b[{}A", new_cursor_row_movement).unwrap(); } // position the cursor within the line if cursor.col > 0 { write!(self.buffer, "\r\x1b[{}C", cursor.col).unwrap(); } else { self.buffer.push('\r'); } self.write_and_flush(self.buffer.as_bytes())?; Ok(()) } fn write_and_flush(&self, buf: &[u8]) -> Result<()> { write_and_flush(self.out, buf) } /// Control characters are treated as having zero width. /// Characters with 2 column width are correctly handled (not split). fn calculate_position(&self, s: &str, orig: Position) -> Position { let mut pos = orig; let mut esc_seq = 0; for c in s.graphemes(true) { if c == "\n" { pos.row += 1; pos.col = 0; continue; } let cw = if c == "\t" { self.tab_stop - (pos.col % self.tab_stop) } else { width(c, &mut esc_seq) }; pos.col += cw; if pos.col > self.cols { pos.row += 1; pos.col = cw; } } if pos.col == self.cols { pos.col = 0; pos.row += 1; } pos } fn beep(&mut self) -> Result<()> { match self.bell_style { BellStyle::Audible => { io::stderr().write_all(b"\x07")?; io::stderr().flush()?; Ok(()) } _ => Ok(()), } } /// Clear the screen. Used to handle ctrl+l fn clear_screen(&mut self) -> Result<()> { self.write_and_flush(b"\x1b[H\x1b[2J") } /// Check if a SIGWINCH signal has been received fn sigwinch(&self) -> bool { SIGWINCH.compare_and_swap(true, false, atomic::Ordering::SeqCst) } /// Try to update the number of columns in the current terminal, fn update_size(&mut self) { let (cols, _) = get_win_size(&self.out); self.cols = cols; } fn get_columns(&self) -> usize { self.cols } /// Try to get the number of rows in the current terminal, /// or assume 24 if it fails. fn get_rows(&self) -> usize { let (_, rows) = get_win_size(&self.out); rows } fn colors_enabled(&self) -> bool { self.colors_enabled } fn move_cursor_at_leftmost(&mut self, rdr: &mut PosixRawReader) -> Result<()> { if rdr.poll(0)? != 0 { debug!(target: "rustyline", "cannot request cursor location"); return Ok(()); } /* Report cursor location */ self.write_and_flush(b"\x1b[6n")?; /* Read the response: ESC [ rows ; cols R */ if rdr.poll(100)? == 0 || rdr.next_char()? != '\x1b' || rdr.next_char()? != '[' || read_digits_until(rdr, ';')?.is_none() { warn!(target: "rustyline", "cannot read initial cursor location"); return Ok(()); } let col = read_digits_until(rdr, 'R')?; debug!(target: "rustyline", "initial cursor location: {:?}", col); if col.is_some() && col != Some(1) { self.write_and_flush(b"\n")?; } Ok(()) } } fn read_digits_until(rdr: &mut PosixRawReader, sep: char) -> Result> { let mut num: u32 = 0; loop { match rdr.next_char()? { digit @ '0'..='9' => { num = num .saturating_mul(10) .saturating_add(digit.to_digit(10).unwrap()); continue; } c if c == sep => break, _ => return Ok(None), } } Ok(Some(num)) } static SIGWINCH_ONCE: sync::Once = sync::Once::new(); static SIGWINCH: atomic::AtomicBool = atomic::AtomicBool::new(false); fn install_sigwinch_handler() { SIGWINCH_ONCE.call_once(|| unsafe { let sigwinch = signal::SigAction::new( signal::SigHandler::Handler(sigwinch_handler), signal::SaFlags::empty(), signal::SigSet::empty(), ); let _ = signal::sigaction(signal::SIGWINCH, &sigwinch); }); } extern "C" fn sigwinch_handler(_: libc::c_int) { SIGWINCH.store(true, atomic::Ordering::SeqCst); debug!(target: "rustyline", "SIGWINCH"); } #[cfg(not(test))] pub type Terminal = PosixTerminal; #[derive(Clone, Debug)] pub struct PosixTerminal { unsupported: bool, stdin_isatty: bool, stdstream_isatty: bool, pub(crate) color_mode: ColorMode, stream_type: OutputStreamType, tab_stop: usize, bell_style: BellStyle, } impl PosixTerminal { fn colors_enabled(&self) -> bool { match self.color_mode { ColorMode::Enabled => self.stdstream_isatty, ColorMode::Forced => true, ColorMode::Disabled => false, } } } impl Term for PosixTerminal { type Mode = PosixMode; type Reader = PosixRawReader; type Writer = PosixRenderer; fn new( color_mode: ColorMode, stream_type: OutputStreamType, tab_stop: usize, bell_style: BellStyle, ) -> Self { let term = Self { unsupported: is_unsupported_term(), stdin_isatty: is_a_tty(STDIN_FILENO), stdstream_isatty: is_a_tty(stream_type.as_raw_fd()), color_mode, stream_type, tab_stop, bell_style, }; if !term.unsupported && term.stdin_isatty && term.stdstream_isatty { install_sigwinch_handler(); } term } // Init checks: /// Check if current terminal can provide a rich line-editing user /// interface. fn is_unsupported(&self) -> bool { self.unsupported } /// check if stdin is connected to a terminal. fn is_stdin_tty(&self) -> bool { self.stdin_isatty } fn is_output_tty(&self) -> bool { self.stdstream_isatty } // Interactive loop: fn enable_raw_mode(&mut self) -> Result { use nix::errno::Errno::ENOTTY; use nix::sys::termios::{ControlFlags, InputFlags, LocalFlags, SpecialCharacterIndices}; if !self.stdin_isatty { return Err(nix::Error::from_errno(ENOTTY).into()); } let original_mode = termios::tcgetattr(STDIN_FILENO)?; let mut raw = original_mode.clone(); // disable BREAK interrupt, CR to NL conversion on input, // input parity check, strip high bit (bit 8), output flow control raw.input_flags &= !(InputFlags::BRKINT | InputFlags::ICRNL | InputFlags::INPCK | InputFlags::ISTRIP | InputFlags::IXON); // we don't want raw output, it turns newlines into straight line feeds // disable all output processing // raw.c_oflag = raw.c_oflag & !(OutputFlags::OPOST); // character-size mark (8 bits) raw.control_flags |= ControlFlags::CS8; // disable echoing, canonical mode, extended input processing and signals raw.local_flags &= !(LocalFlags::ECHO | LocalFlags::ICANON | LocalFlags::IEXTEN | LocalFlags::ISIG); raw.control_chars[SpecialCharacterIndices::VMIN as usize] = 1; // One character-at-a-time input raw.control_chars[SpecialCharacterIndices::VTIME as usize] = 0; // with blocking read termios::tcsetattr(STDIN_FILENO, SetArg::TCSADRAIN, &raw)?; // enable bracketed paste let out = if let Err(e) = write_and_flush(self.stream_type, BRACKETED_PASTE_ON) { debug!(target: "rustyline", "Cannot enable bracketed paste: {}", e); None } else { Some(self.stream_type) }; Ok(PosixMode { termios: original_mode, out, }) } /// Create a RAW reader fn create_reader(&self, config: &Config) -> Result { PosixRawReader::new(config) } fn create_writer(&self) -> PosixRenderer { PosixRenderer::new( self.stream_type, self.tab_stop, self.colors_enabled(), self.bell_style, ) } } #[cfg(not(test))] pub fn suspend() -> Result<()> { use nix::unistd::Pid; // suspend the whole process group signal::kill(Pid::from_raw(0), signal::SIGTSTP)?; Ok(()) } fn write_and_flush(out: OutputStreamType, buf: &[u8]) -> Result<()> { match out { OutputStreamType::Stdout => { io::stdout().write_all(buf)?; io::stdout().flush()?; } OutputStreamType::Stderr => { io::stderr().write_all(buf)?; io::stderr().flush()?; } } Ok(()) } #[cfg(test)] mod test { use super::{Position, PosixRenderer, PosixTerminal, Renderer}; use crate::config::{BellStyle, OutputStreamType}; use crate::line_buffer::LineBuffer; #[test] #[ignore] fn prompt_with_ansi_escape_codes() { let out = PosixRenderer::new(OutputStreamType::Stdout, 4, true, BellStyle::default()); let pos = out.calculate_position("\x1b[1;32m>>\x1b[0m ", Position::default()); assert_eq!(3, pos.col); assert_eq!(0, pos.row); } #[test] fn test_unsupported_term() { ::std::env::set_var("TERM", "xterm"); assert_eq!(false, super::is_unsupported_term()); ::std::env::set_var("TERM", "dumb"); assert_eq!(true, super::is_unsupported_term()); } #[test] fn test_send() { fn assert_send() {} assert_send::(); } #[test] fn test_sync() { fn assert_sync() {} assert_sync::(); } #[test] fn test_line_wrap() { let mut out = PosixRenderer::new(OutputStreamType::Stdout, 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, None); 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)); 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}[0K> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\u{1b}[1C", out.buffer ); } } rustyline-6.3.0/src/tty/windows.rs010064400007650000024000000602061372733102500154130ustar 00000000000000//! Windows specific definitions #![allow(clippy::try_err)] // suggested fix does not work (cannot infer...) use std::io::{self, ErrorKind, Write}; use std::mem; use std::sync::atomic; 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::winnt::{CHAR, HANDLE}; use winapi::um::{consoleapi, handleapi, processenv, winbase, wincon, winuser}; use super::{width, RawMode, RawReader, Renderer, Term}; use crate::config::{BellStyle, ColorMode, Config, OutputStreamType}; use crate::error; use crate::highlight::Highlighter; use crate::keys::{self, KeyPress}; use crate::layout::{Layout, Position}; use crate::line_buffer::LineBuffer; use crate::Result; const STDIN_FILENO: DWORD = winbase::STD_INPUT_HANDLE; const STDOUT_FILENO: DWORD = winbase::STD_OUTPUT_HANDLE; const STDERR_FILENO: DWORD = winbase::STD_ERROR_HANDLE; fn get_std_handle(fd: DWORD) -> Result { let handle = unsafe { processenv::GetStdHandle(fd) }; if handle == handleapi::INVALID_HANDLE_VALUE { Err(io::Error::last_os_error())?; } else if handle.is_null() { Err(io::Error::new( io::ErrorKind::Other, "no stdio handle available for this process", ))?; } Ok(handle) } fn check(rc: BOOL) -> Result<()> { if rc == FALSE { Err(io::Error::last_os_error())? } else { Ok(()) } } fn get_win_size(handle: HANDLE) -> (usize, usize) { let mut info = unsafe { mem::zeroed() }; match unsafe { wincon::GetConsoleScreenBufferInfo(handle, &mut info) } { 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) } #[cfg(not(test))] pub type Mode = ConsoleMode; #[derive(Clone, Copy, Debug)] pub struct ConsoleMode { original_stdin_mode: DWORD, stdin_handle: HANDLE, original_stdstream_mode: Option, stdstream_handle: HANDLE, } impl RawMode for ConsoleMode { /// Disable RAW mode for the terminal. fn disable_raw_mode(&self) -> Result<()> { check(unsafe { consoleapi::SetConsoleMode(self.stdin_handle, self.original_stdin_mode) })?; if let Some(original_stdstream_mode) = self.original_stdstream_mode { check(unsafe { consoleapi::SetConsoleMode(self.stdstream_handle, original_stdstream_mode) })?; } Ok(()) } } /// Console input reader pub struct ConsoleRawReader { handle: HANDLE, } impl ConsoleRawReader { pub fn create() -> Result { let handle = get_std_handle(STDIN_FILENO)?; Ok(ConsoleRawReader { handle }) } } impl RawReader for ConsoleRawReader { fn next_key(&mut self, _: bool) -> Result { use std::char::decode_utf16; use winapi::um::wincon::{ LEFT_ALT_PRESSED, LEFT_CTRL_PRESSED, RIGHT_ALT_PRESSED, RIGHT_CTRL_PRESSED, SHIFT_PRESSED, }; let mut rec: wincon::INPUT_RECORD = unsafe { mem::zeroed() }; let mut count = 0; let mut surrogate = 0; loop { // TODO GetNumberOfConsoleInputEvents check(unsafe { consoleapi::ReadConsoleInputW(self.handle, &mut rec, 1 as DWORD, &mut count) })?; if rec.EventType == wincon::WINDOW_BUFFER_SIZE_EVENT { SIGWINCH.store(true, atomic::Ordering::SeqCst); debug!(target: "rustyline", "SIGWINCH"); return Err(error::ReadlineError::WindowResize); // sigwinch + // err => err // ignored } else if rec.EventType != wincon::KEY_EVENT { continue; } let key_event = unsafe { rec.Event.KeyEvent() }; // writeln!(io::stderr(), "key_event: {:?}", key_event).unwrap(); if key_event.bKeyDown == 0 && key_event.wVirtualKeyCode != winuser::VK_MENU as WORD { continue; } // key_event.wRepeatCount seems to be always set to 1 (maybe because we only // read one character at a time) let alt_gr = key_event.dwControlKeyState & (LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED) == (LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED); let alt = key_event.dwControlKeyState & (LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED) != 0; let ctrl = key_event.dwControlKeyState & (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED) != 0; let meta = alt && !alt_gr; let shift = key_event.dwControlKeyState & SHIFT_PRESSED != 0; let utf16 = unsafe { *key_event.uChar.UnicodeChar() }; if utf16 == 0 { match i32::from(key_event.wVirtualKeyCode) { winuser::VK_LEFT => { return Ok(if ctrl { KeyPress::ControlLeft } else if shift { KeyPress::ShiftLeft } else { KeyPress::Left }); } winuser::VK_RIGHT => { return Ok(if ctrl { KeyPress::ControlRight } else if shift { KeyPress::ShiftRight } else { KeyPress::Right }); } winuser::VK_UP => { return Ok(if ctrl { KeyPress::ControlUp } else if shift { KeyPress::ShiftUp } else { KeyPress::Up }); } winuser::VK_DOWN => { return Ok(if ctrl { KeyPress::ControlDown } else if shift { KeyPress::ShiftDown } else { KeyPress::Down }); } winuser::VK_DELETE => return Ok(KeyPress::Delete), winuser::VK_HOME => return Ok(KeyPress::Home), winuser::VK_END => return Ok(KeyPress::End), winuser::VK_PRIOR => return Ok(KeyPress::PageUp), winuser::VK_NEXT => return Ok(KeyPress::PageDown), winuser::VK_INSERT => return Ok(KeyPress::Insert), winuser::VK_F1 => return Ok(KeyPress::F(1)), winuser::VK_F2 => return Ok(KeyPress::F(2)), winuser::VK_F3 => return Ok(KeyPress::F(3)), winuser::VK_F4 => return Ok(KeyPress::F(4)), winuser::VK_F5 => return Ok(KeyPress::F(5)), winuser::VK_F6 => return Ok(KeyPress::F(6)), winuser::VK_F7 => return Ok(KeyPress::F(7)), winuser::VK_F8 => return Ok(KeyPress::F(8)), winuser::VK_F9 => return Ok(KeyPress::F(9)), winuser::VK_F10 => return Ok(KeyPress::F(10)), winuser::VK_F11 => return Ok(KeyPress::F(11)), winuser::VK_F12 => return Ok(KeyPress::F(12)), // winuser::VK_BACK is correctly handled because the key_event.UnicodeChar is // also set. _ => continue, }; } else if utf16 == 27 { return Ok(KeyPress::Esc); } else { if utf16 >= 0xD800 && utf16 < 0xDC00 { surrogate = utf16; continue; } let orc = if surrogate == 0 { decode_utf16(Some(utf16)).next() } else { decode_utf16([surrogate, utf16].iter().cloned()).next() }; let rc = if let Some(rc) = orc { rc } else { return Err(error::ReadlineError::Eof); }; let c = rc?; if meta { return Ok(KeyPress::Meta(c)); } else { let mut key = keys::char_to_key_press(c); if key == KeyPress::Tab && shift { key = KeyPress::BackTab; } else if key == KeyPress::Char(' ') && ctrl { key = KeyPress::Ctrl(' '); } return Ok(key); } } } } fn read_pasted_text(&mut self) -> Result { unimplemented!() } } pub struct ConsoleRenderer { out: OutputStreamType, handle: HANDLE, cols: usize, // Number of columns in terminal buffer: String, colors_enabled: bool, bell_style: BellStyle, } impl ConsoleRenderer { fn new( handle: HANDLE, out: OutputStreamType, colors_enabled: bool, bell_style: BellStyle, ) -> ConsoleRenderer { // Multi line editing is enabled by ENABLE_WRAP_AT_EOL_OUTPUT mode let (cols, _) = get_win_size(handle); ConsoleRenderer { out, handle, cols, buffer: String::with_capacity(1024), colors_enabled, bell_style, } } fn get_console_screen_buffer_info(&self) -> Result { let mut info = unsafe { mem::zeroed() }; check(unsafe { wincon::GetConsoleScreenBufferInfo(self.handle, &mut info) })?; Ok(info) } fn set_console_cursor_position(&mut self, pos: wincon::COORD) -> Result<()> { check(unsafe { wincon::SetConsoleCursorPosition(self.handle, pos) }) } fn clear(&mut self, length: DWORD, pos: wincon::COORD, attr: WORD) -> Result<()> { let mut _count = 0; check(unsafe { wincon::FillConsoleOutputCharacterA(self.handle, ' ' as CHAR, length, pos, &mut _count) })?; check(unsafe { wincon::FillConsoleOutputAttribute(self.handle, attr, length, pos, &mut _count) }) } fn set_cursor_visible(&mut self, visible: BOOL) -> Result<()> { set_cursor_visible(self.handle, visible) } // You can't have both ENABLE_WRAP_AT_EOL_OUTPUT and // ENABLE_VIRTUAL_TERMINAL_PROCESSING. So we need to wrap manually. fn wrap_at_eol(&mut self, s: &str, mut col: usize) -> usize { let mut esc_seq = 0; for c in s.graphemes(true) { if c == "\n" { col = 0; self.buffer.push_str(c); } 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: &wincon::CONSOLE_SCREEN_BUFFER_INFO, layout: &Layout, ) -> Result<()> { let current_row = layout.cursor.row; let old_rows = layout.end.row; let mut coord = info.dwCursorPosition; coord.X = 0; coord.Y -= current_row as i16; self.set_console_cursor_position(coord)?; self.clear( (info.dwSize.X * (old_rows as i16 + 1)) as DWORD, coord, info.wAttributes, ) } } fn set_cursor_visible(handle: HANDLE, visible: BOOL) -> Result<()> { let mut info = unsafe { mem::zeroed() }; check(unsafe { wincon::GetConsoleCursorInfo(handle, &mut info) })?; if info.bVisible == visible { return Ok(()); } info.bVisible = visible; check(unsafe { wincon::SetConsoleCursorInfo(handle, &info) }) } impl Renderer for ConsoleRenderer { type Reader = ConsoleRawReader; fn move_cursor(&mut self, old: Position, new: Position) -> Result<()> { let mut cursor = self.get_console_screen_buffer_info()?.dwCursorPosition; if new.row > old.row { cursor.Y += (new.row - old.row) as i16; } else { cursor.Y -= (old.row - new.row) as i16; } if new.col > old.col { cursor.X += (new.col - old.col) as i16; } else { cursor.X -= (old.col - new.col) as i16; } self.set_console_cursor_position(cursor) } fn refresh_line( &mut self, prompt: &str, line: &LineBuffer, hint: Option<&str>, old_layout: &Layout, new_layout: &Layout, highlighter: Option<&dyn Highlighter>, ) -> Result<()> { let default_prompt = new_layout.default_prompt; let cursor = new_layout.cursor; let end_pos = new_layout.end; 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 { // 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 { self.buffer.push_str(hint); } } let info = self.get_console_screen_buffer_info()?; self.set_cursor_visible(FALSE)?; // just to avoid flickering let handle = self.handle; scopeguard::defer! { let _ = set_cursor_visible(handle, TRUE); } // position at the start of the prompt, clear to end of previous input self.clear_old_rows(&info, old_layout)?; // display prompt, input line and hint self.write_and_flush(self.buffer.as_bytes())?; // position the cursor let mut coord = self.get_console_screen_buffer_info()?.dwCursorPosition; coord.X = cursor.col as i16; coord.Y -= (end_pos.row - cursor.row) as i16; self.set_console_cursor_position(coord)?; Ok(()) } fn write_and_flush(&self, buf: &[u8]) -> Result<()> { match self.out { OutputStreamType::Stdout => { io::stdout().write_all(buf)?; io::stdout().flush()?; } OutputStreamType::Stderr => { io::stderr().write_all(buf)?; io::stderr().flush()?; } } Ok(()) } /// Characters with 2 column width are correctly handled (not split). fn calculate_position(&self, s: &str, orig: Position) -> Position { let mut pos = orig; for c in s.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 => { io::stderr().write_all(b"\x07")?; io::stderr().flush()?; Ok(()) } _ => Ok(()), } } /// Clear the screen. Used to handle ctrl+l fn clear_screen(&mut self) -> Result<()> { let info = self.get_console_screen_buffer_info()?; let coord = wincon::COORD { X: 0, Y: 0 }; check(unsafe { wincon::SetConsoleCursorPosition(self.handle, coord) })?; let n = info.dwSize.X as DWORD * info.dwSize.Y as DWORD; self.clear(n, coord, info.wAttributes) } fn sigwinch(&self) -> bool { SIGWINCH.compare_and_swap(true, false, atomic::Ordering::SeqCst) } /// Try to get the number of columns in the current terminal, /// or assume 80 if it fails. fn update_size(&mut self) { let (cols, _) = get_win_size(self.handle); self.cols = cols; } fn get_columns(&self) -> usize { self.cols } /// Try to get the number of rows in the current terminal, /// or assume 24 if it fails. fn get_rows(&self) -> usize { let (_, rows) = get_win_size(self.handle); rows } fn colors_enabled(&self) -> bool { self.colors_enabled } fn move_cursor_at_leftmost(&mut self, _: &mut ConsoleRawReader) -> Result<()> { self.write_and_flush(b"")?; // we must do this otherwise the cursor position is not reported correctly let mut info = self.get_console_screen_buffer_info()?; if info.dwCursorPosition.X == 0 { return Ok(()); } debug!(target: "rustyline", "initial cursor location: {:?}, {:?}", info.dwCursorPosition.X, info.dwCursorPosition.Y); info.dwCursorPosition.X = 0; info.dwCursorPosition.Y += 1; let res = self.set_console_cursor_position(info.dwCursorPosition); if let Err(error::ReadlineError::Io(ref e)) = res { if e.raw_os_error() == Some(winerror::ERROR_INVALID_PARAMETER as i32) { warn!(target: "rustyline", "invalid cursor position: ({:?}, {:?}) in ({:?}, {:?})", info.dwCursorPosition.X, info.dwCursorPosition.Y, info.dwSize.X, info.dwSize.Y); println!(); return Ok(()); } } res } } static SIGWINCH: atomic::AtomicBool = atomic::AtomicBool::new(false); #[cfg(not(test))] pub type Terminal = Console; #[derive(Clone, Debug)] pub struct Console { stdin_isatty: bool, stdin_handle: HANDLE, stdstream_isatty: bool, stdstream_handle: HANDLE, pub(crate) color_mode: ColorMode, ansi_colors_supported: bool, stream_type: OutputStreamType, bell_style: BellStyle, } impl Console { fn colors_enabled(&self) -> bool { // TODO ANSI Colors & Windows <10 match self.color_mode { ColorMode::Enabled => self.stdstream_isatty && self.ansi_colors_supported, ColorMode::Forced => true, ColorMode::Disabled => false, } } } impl Term for Console { type Mode = ConsoleMode; type Reader = ConsoleRawReader; type Writer = ConsoleRenderer; fn new( color_mode: ColorMode, stream_type: OutputStreamType, _tab_stop: usize, bell_style: BellStyle, ) -> Console { use std::ptr; let stdin_handle = get_std_handle(STDIN_FILENO); let stdin_isatty = match stdin_handle { Ok(handle) => { // If this function doesn't fail then fd is a TTY get_console_mode(handle).is_ok() } Err(_) => false, }; let stdstream_handle = get_std_handle(if stream_type == OutputStreamType::Stdout { STDOUT_FILENO } else { STDERR_FILENO }); let stdstream_isatty = match stdstream_handle { Ok(handle) => { // If this function doesn't fail then fd is a TTY get_console_mode(handle).is_ok() } Err(_) => false, }; Console { stdin_isatty, stdin_handle: stdin_handle.unwrap_or(ptr::null_mut()), stdstream_isatty, stdstream_handle: stdstream_handle.unwrap_or(ptr::null_mut()), color_mode, ansi_colors_supported: false, stream_type, bell_style, } } /// Checking for an unsupported TERM in windows is a no-op fn is_unsupported(&self) -> bool { false } fn is_stdin_tty(&self) -> bool { self.stdin_isatty } fn is_output_tty(&self) -> bool { self.stdstream_isatty } // pub fn install_sigwinch_handler(&mut self) { // See ReadConsoleInputW && WINDOW_BUFFER_SIZE_EVENT // } /// Enable RAW mode for the terminal. fn enable_raw_mode(&mut self) -> Result { if !self.stdin_isatty { Err(io::Error::new( io::ErrorKind::Other, "no stdio handle available for this process", ))?; } let original_stdin_mode = get_console_mode(self.stdin_handle)?; // Disable these modes let mut raw = original_stdin_mode & !(wincon::ENABLE_LINE_INPUT | wincon::ENABLE_ECHO_INPUT | wincon::ENABLE_PROCESSED_INPUT); // Enable these modes raw |= wincon::ENABLE_EXTENDED_FLAGS; raw |= wincon::ENABLE_INSERT_MODE; raw |= wincon::ENABLE_QUICK_EDIT_MODE; raw |= wincon::ENABLE_WINDOW_INPUT; check(unsafe { consoleapi::SetConsoleMode(self.stdin_handle, raw) })?; let original_stdstream_mode = if self.stdstream_isatty { let original_stdstream_mode = get_console_mode(self.stdstream_handle)?; let mut mode = original_stdstream_mode; if mode & wincon::ENABLE_WRAP_AT_EOL_OUTPUT == 0 { mode |= wincon::ENABLE_WRAP_AT_EOL_OUTPUT; debug!(target: "rustyline", "activate ENABLE_WRAP_AT_EOL_OUTPUT"); unsafe { assert!(consoleapi::SetConsoleMode(self.stdstream_handle, 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!(consoleapi::SetConsoleMode(self.stdstream_handle, mode) != 0); } } else { debug!(target: "rustyline", "ANSI colors already enabled"); } } else if self.color_mode != ColorMode::Disabled { mode |= wincon::ENABLE_VIRTUAL_TERMINAL_PROCESSING; self.ansi_colors_supported = unsafe { consoleapi::SetConsoleMode(self.stdstream_handle, mode) != 0 }; debug!(target: "rustyline", "ansi_colors_supported: {}", self.ansi_colors_supported); } Some(original_stdstream_mode) } else { None }; Ok(ConsoleMode { original_stdin_mode, stdin_handle: self.stdin_handle, original_stdstream_mode, stdstream_handle: self.stdstream_handle, }) } fn create_reader(&self, _: &Config) -> Result { ConsoleRawReader::create() } fn create_writer(&self) -> ConsoleRenderer { ConsoleRenderer::new( self.stdstream_handle, self.stream_type, self.colors_enabled(), self.bell_style, ) } } unsafe impl Send for Console {} unsafe impl Sync for Console {} #[cfg(test)] mod test { use super::Console; #[test] fn test_send() { fn assert_send() {} assert_send::(); } #[test] fn test_sync() { fn assert_sync() {} assert_sync::(); } } rustyline-6.3.0/src/undo.rs010064400007650000024000000325421352702755400140570ustar 00000000000000//! Undo API use std::fmt::Debug; use crate::keymap::RepeatCount; use crate::line_buffer::{ChangeListener, DeleteListener, Direction, LineBuffer}; use log::debug; use unicode_segmentation::UnicodeSegmentation; enum Change { Begin, End, Insert { idx: usize, text: String, }, // QuotedInsert, SelfInsert, Yank Delete { idx: usize, text: String, }, /* BackwardDeleteChar, BackwardKillWord, DeleteChar, * KillLine, KillWholeLine, KillWord, * UnixLikeDiscard, ViDeleteTo */ Replace { idx: usize, old: String, new: String, }, /* CapitalizeWord, Complete, DowncaseWord, Replace, TransposeChars, TransposeWords, * UpcaseWord, YankPop */ } impl Change { fn undo(&self, line: &mut LineBuffer) { match *self { Change::Begin | Change::End => { unreachable!(); } Change::Insert { idx, ref text } => { line.delete_range(idx..idx + text.len()); } Change::Delete { idx, ref text } => { line.insert_str(idx, text); line.set_pos(idx + text.len()); } Change::Replace { idx, ref old, ref new, } => { line.replace(idx..idx + new.len(), old); } } } #[cfg(test)] fn redo(&self, line: &mut LineBuffer) { match *self { Change::Begin | Change::End => { unreachable!(); } Change::Insert { idx, ref text } => { line.insert_str(idx, text); } Change::Delete { idx, ref text } => { line.delete_range(idx..idx + text.len()); } Change::Replace { idx, ref old, ref new, } => { line.replace(idx..idx + old.len(), new); } } } fn insert_seq(&self, indx: usize) -> bool { if let Change::Insert { idx, ref text } = *self { idx + text.len() == indx } else { false } } fn delete_seq(&self, indx: usize, len: usize) -> bool { if let Change::Delete { idx, .. } = *self { // delete or backspace idx == indx || idx == indx + len } else { false } } fn replace_seq(&self, indx: usize) -> bool { if let Change::Replace { idx, ref new, .. } = *self { idx + new.len() == indx } else { false } } } pub struct Changeset { undo_group_level: u32, undos: Vec, // undoable changes redos: Vec, // undone changes, redoable } impl Changeset { pub fn new() -> Self { Self { undo_group_level: 0, undos: Vec::new(), redos: Vec::new(), } } pub fn begin(&mut self) -> usize { debug!(target: "rustyline", "Changeset::begin"); self.redos.clear(); let mark = self.undos.len(); self.undos.push(Change::Begin); self.undo_group_level += 1; mark } /// Returns `true` when changes happen between the last call to `begin` and /// this `end`. pub fn end(&mut self) -> bool { debug!(target: "rustyline", "Changeset::end"); self.redos.clear(); let mut touched = false; while self.undo_group_level > 0 { self.undo_group_level -= 1; if let Some(&Change::Begin) = self.undos.last() { // empty Begin..End self.undos.pop(); } else { self.undos.push(Change::End); touched = true; } } touched } fn insert_char(idx: usize, c: char) -> Change { let mut text = String::new(); text.push(c); Change::Insert { idx, text } } pub fn insert(&mut self, idx: usize, c: char) { debug!(target: "rustyline", "Changeset::insert({}, {:?})", idx, c); self.redos.clear(); if !c.is_alphanumeric() || !self.undos.last().map_or(false, |lc| lc.insert_seq(idx)) { self.undos.push(Self::insert_char(idx, c)); return; } // merge consecutive char insertions when char is alphanumeric let mut last_change = self.undos.pop().unwrap(); if let Change::Insert { ref mut text, .. } = last_change { text.push(c); } else { unreachable!(); } self.undos.push(last_change); } pub fn insert_str + Into + Debug>(&mut self, idx: usize, string: S) { debug!(target: "rustyline", "Changeset::insert_str({}, {:?})", idx, string); self.redos.clear(); if string.as_ref().is_empty() { return; } self.undos.push(Change::Insert { idx, text: string.into(), }); } pub fn delete + Into + Debug>(&mut self, indx: usize, string: S) { debug!(target: "rustyline", "Changeset::delete({}, {:?})", indx, string); self.redos.clear(); if string.as_ref().is_empty() { return; } if !Self::single_char(string.as_ref()) || !self .undos .last() .map_or(false, |lc| lc.delete_seq(indx, string.as_ref().len())) { self.undos.push(Change::Delete { idx: indx, text: string.into(), }); return; } // merge consecutive char deletions when char is alphanumeric let mut last_change = self.undos.pop().unwrap(); if let Change::Delete { ref mut idx, ref mut text, } = last_change { if *idx == indx { text.push_str(string.as_ref()); } else { text.insert_str(0, string.as_ref()); *idx = indx; } } else { unreachable!(); } self.undos.push(last_change); } fn single_char(s: &str) -> bool { let mut graphemes = s.graphemes(true); graphemes.next().map_or(false, |grapheme| { grapheme.chars().all(char::is_alphanumeric) }) && graphemes.next().is_none() } pub fn replace + Into + Debug>(&mut self, indx: usize, old_: S, new_: S) { debug!(target: "rustyline", "Changeset::replace({}, {:?}, {:?})", indx, old_, new_); self.redos.clear(); if !self.undos.last().map_or(false, |lc| lc.replace_seq(indx)) { self.undos.push(Change::Replace { idx: indx, old: old_.into(), new: new_.into(), }); return; } // merge consecutive char replacements let mut last_change = self.undos.pop().unwrap(); if let Change::Replace { ref mut old, ref mut new, .. } = last_change { old.push_str(old_.as_ref()); new.push_str(new_.as_ref()); } else { unreachable!(); } self.undos.push(last_change); } pub fn undo(&mut self, line: &mut LineBuffer, n: RepeatCount) -> bool { debug!(target: "rustyline", "Changeset::undo"); let mut count = 0; let mut waiting_for_begin = 0; let mut undone = false; loop { if let Some(change) = self.undos.pop() { match change { Change::Begin => { waiting_for_begin -= 1; } Change::End => { waiting_for_begin += 1; } _ => { change.undo(line); undone = true; } }; self.redos.push(change); } else { break; } if waiting_for_begin <= 0 { count += 1; if count >= n { break; } } } undone } pub fn truncate(&mut self, len: usize) { debug!(target: "rustyline", "Changeset::truncate({})", len); self.undos.truncate(len); } #[cfg(test)] pub fn redo(&mut self, line: &mut LineBuffer) -> bool { let mut waiting_for_end = 0; let mut redone = false; loop { if let Some(change) = self.redos.pop() { match change { Change::Begin => { waiting_for_end += 1; } Change::End => { waiting_for_end -= 1; } _ => { change.redo(line); redone = true; } }; self.undos.push(change); } else { break; } if waiting_for_end <= 0 { break; } } redone } pub fn last_insert(&self) -> Option { for change in self.undos.iter().rev() { match change { Change::Insert { ref text, .. } => return Some(text.to_owned()), Change::Replace { ref new, .. } => return Some(new.to_owned()), Change::End => { continue; } _ => { return None; } } } None } } impl DeleteListener for Changeset { fn start_killing(&mut self) {} fn delete(&mut self, idx: usize, string: &str, _: Direction) { self.delete(idx, string); } fn stop_killing(&mut self) {} } impl ChangeListener for Changeset { fn insert_char(&mut self, idx: usize, c: char) { self.insert(idx, c); } fn insert_str(&mut self, idx: usize, string: &str) { self.insert_str(idx, string); } fn replace(&mut self, idx: usize, old: &str, new: &str) { self.replace(idx, old, new); } } #[cfg(test)] mod tests { use super::Changeset; use crate::line_buffer::LineBuffer; #[test] fn test_insert_chars() { let mut cs = Changeset::new(); cs.insert(0, 'H'); cs.insert(1, 'i'); assert_eq!(1, cs.undos.len()); assert_eq!(0, cs.redos.len()); cs.insert(0, ' '); assert_eq!(2, cs.undos.len()); } #[test] fn test_insert_strings() { let mut cs = Changeset::new(); cs.insert_str(0, "Hello"); cs.insert_str(5, ", "); assert_eq!(2, cs.undos.len()); assert_eq!(0, cs.redos.len()); } #[test] fn test_undo_insert() { let mut buf = LineBuffer::init("", 0, None); buf.insert_str(0, "Hello"); buf.insert_str(5, ", world!"); let mut cs = Changeset::new(); assert_eq!(buf.as_str(), "Hello, world!"); cs.insert_str(5, ", world!"); cs.undo(&mut buf, 1); assert_eq!(0, cs.undos.len()); assert_eq!(1, cs.redos.len()); assert_eq!(buf.as_str(), "Hello"); cs.redo(&mut buf); assert_eq!(1, cs.undos.len()); assert_eq!(0, cs.redos.len()); assert_eq!(buf.as_str(), "Hello, world!"); } #[test] fn test_undo_delete() { let mut buf = LineBuffer::init("", 0, None); buf.insert_str(0, "Hello"); let mut cs = Changeset::new(); assert_eq!(buf.as_str(), "Hello"); cs.delete(5, ", world!"); cs.undo(&mut buf, 1); assert_eq!(buf.as_str(), "Hello, world!"); cs.redo(&mut buf); assert_eq!(buf.as_str(), "Hello"); } #[test] fn test_delete_chars() { let mut buf = LineBuffer::init("", 0, None); buf.insert_str(0, "Hlo"); let mut cs = Changeset::new(); cs.delete(1, "e"); cs.delete(1, "l"); assert_eq!(1, cs.undos.len()); cs.undo(&mut buf, 1); assert_eq!(buf.as_str(), "Hello"); } #[test] fn test_backspace_chars() { let mut buf = LineBuffer::init("", 0, None); buf.insert_str(0, "Hlo"); let mut cs = Changeset::new(); cs.delete(2, "l"); cs.delete(1, "e"); assert_eq!(1, cs.undos.len()); cs.undo(&mut buf, 1); assert_eq!(buf.as_str(), "Hello"); } #[test] fn test_undo_replace() { let mut buf = LineBuffer::init("", 0, None); buf.insert_str(0, "Hello, world!"); let mut cs = Changeset::new(); assert_eq!(buf.as_str(), "Hello, world!"); buf.replace(1..5, "i"); assert_eq!(buf.as_str(), "Hi, world!"); cs.replace(1, "ello", "i"); cs.undo(&mut buf, 1); assert_eq!(buf.as_str(), "Hello, world!"); cs.redo(&mut buf); assert_eq!(buf.as_str(), "Hi, world!"); } #[test] fn test_last_insert() { let mut cs = Changeset::new(); cs.begin(); cs.delete(0, "Hello"); cs.insert_str(0, "Bye"); cs.end(); let insert = cs.last_insert(); assert_eq!(Some("Bye".to_owned()), insert); } #[test] fn test_end() { let mut cs = Changeset::new(); cs.begin(); assert!(!cs.end()); cs.begin(); cs.insert_str(0, "Hi"); assert!(cs.end()); } } rustyline-6.3.0/src/validate.rs010064400007650000024000000075711366145270200147030ustar 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), } /// Give access to user input. pub struct ValidationContext<'i> { i: &'i mut dyn Invoke, } impl<'i> ValidationContext<'i> { pub(crate) fn new(i: &'i mut dyn Invoke) -> Self { ValidationContext { i } } /// Returns user input. pub fn input(&self) -> &str { self.i.input() } // TODO //fn invoke(&mut self, cmd: Cmd) -> Result { // self.i.invoke(cmd) //} } /// This trait provides an extension interface for determining whether /// the current input buffer is valid. Rustyline uses the method /// provided by this trait to decide whether hitting the enter key /// will end the current editing session and return the current line /// buffer to the caller of `Editor::readline` or variants. pub trait Validator { /// Takes the currently edited `input` and returns a /// `ValidationResult` indicating whether it is valid or not along /// with an option message to display about the result. The most /// common validity check to implement is probably whether the /// input is complete or not, for instance ensuring that all /// delimiters are fully balanced. /// /// If you implement more complex validation checks it's probably /// a good idea to also implement a `Hinter` to provide feedback /// about what is invalid. /// /// For auto-correction like a missing closing quote or to reject invalid /// char while typing, the input will be mutable (TODO). fn validate(&self, ctx: &mut ValidationContext) -> Result { let _ = ctx; Ok(ValidationResult::Valid(None)) } /// Configure whether validation is performed while typing or only /// when user presses the Enter key. /// /// Default is `false`. // TODO we can implement this later. fn validate_while_typing(&self) -> bool { false } } impl Validator for () {} impl<'v, V: ?Sized + Validator> Validator for &'v V { fn validate(&self, ctx: &mut ValidationContext) -> Result { (**self).validate(ctx) } fn validate_while_typing(&self) -> bool { (**self).validate_while_typing() } } /// Simple matching bracket validator. #[derive(Default)] pub struct MatchingBracketValidator { _priv: (), } impl MatchingBracketValidator { /// Constructor pub fn new() -> Self { Self { _priv: () } } } impl Validator for MatchingBracketValidator { fn validate(&self, ctx: &mut ValidationContext) -> Result { Ok(validate_brackets(ctx.input())) } } fn validate_brackets(input: &str) -> ValidationResult { let mut stack = vec![]; for c in input.chars() { match c { '(' | '[' | '{' => stack.push(c), ')' | ']' | '}' => match (stack.pop(), c) { (Some('('), ')') | (Some('['), ']') | (Some('{'), '}') => {} (Some(wanted), _) => { return ValidationResult::Invalid(Some(format!( "Mismatched brackets: {:?} is not properly closed", wanted ))) } (None, c) => { return ValidationResult::Invalid(Some(format!( "Mismatched brackets: {:?} is unpaired", c ))) } }, _ => {} } } if stack.is_empty() { ValidationResult::Valid(None) } else { ValidationResult::Incomplete } }