From a4410fef401b0c32c8c9321384ec771deb49a3d6 Mon Sep 17 00:00:00 2001 From: Fernando Herrera Date: Sun, 27 Mar 2022 14:01:04 +0100 Subject: [PATCH] Help menu (#4992) * nu-completer with suggestions * help menu with scrolling * updates description rows based on space * configuration for help menu * update nu-ansi-term * corrected test for update cells * changed keybinding --- Cargo.lock | 154 ++-- Cargo.toml | 5 +- crates/nu-cli/Cargo.toml | 7 +- crates/nu-cli/src/completions.rs | 167 ++-- crates/nu-cli/src/help_completions.rs | 101 +++ crates/nu-cli/src/help_menu.rs | 718 ++++++++++++++++++ crates/nu-cli/src/lib.rs | 4 + crates/nu-cli/src/reedline_config.rs | 132 +++- crates/nu-cli/src/repl.rs | 7 +- crates/nu-color-config/Cargo.toml | 2 +- crates/nu-command/Cargo.toml | 5 +- crates/nu-command/src/filters/update_cells.rs | 4 +- crates/nu-protocol/src/config.rs | 25 +- crates/nu-table/Cargo.toml | 2 +- docs/sample_config/default_config.nu | 11 + 15 files changed, 1152 insertions(+), 192 deletions(-) create mode 100644 crates/nu-cli/src/help_completions.rs create mode 100644 crates/nu-cli/src/help_menu.rs diff --git a/Cargo.lock b/Cargo.lock index 1d3cb63b2..9323a8452 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -182,9 +182,9 @@ dependencies = [ [[package]] name = "async-stream" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "171374e7e3b2504e0e5236e3b59260560f9fe94bfe9ac39ba5e4e929c5590625" +checksum = "dad5c83079eae9969be7fadefe640a1c566901f05ff91ab221de4b6f68d9507e" dependencies = [ "async-stream-impl", "futures-core", @@ -192,9 +192,9 @@ dependencies = [ [[package]] name = "async-stream-impl" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "648ed8c8d2ce5409ccd57453d9d1b214b342a0d69376a6feda1fd6cae3299308" +checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" dependencies = [ "proc-macro2", "quote", @@ -203,9 +203,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.52" +version = "0.1.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3" +checksum = "ed6aa3524a2dfcf9fe180c51eae2b58738348d819517ceadf95789c51fff7600" dependencies = [ "proc-macro2", "quote", @@ -428,9 +428,9 @@ dependencies = [ [[package]] name = "capnp" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16c262726f68118392269a3f7a5546baf51dcfe5cb3c3f0957b502106bf1a065" +checksum = "21d5d7da973146f1720672faa44f1523cc8f923636190ca1a931c7bc8834de68" [[package]] name = "cc" @@ -568,9 +568,9 @@ checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" [[package]] name = "cpufeatures" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" +checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" dependencies = [ "libc", ] @@ -586,9 +586,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.2" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e54ea8bc3fb1ee042f5aace6e3c6e025d3874866da222930f70ce62aceba0bfa" +checksum = "5aaa7bd5fb665c6864b5f963dd9097905c54125909c7aa94c9e18507cdbe6c53" dependencies = [ "cfg-if", "crossbeam-utils", @@ -607,10 +607,11 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c00d6d2ea26e8b151d99093005cb442fb9a37aeaca582a03ec70946f49ab5ed9" +checksum = "1145cf131a2c6ba0615079ab6a638f7e1973ac9c2634fcbeaaad6114246efe8c" dependencies = [ + "autocfg", "cfg-if", "crossbeam-utils", "lazy_static", @@ -620,9 +621,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e5bed1f1c269533fa816a0a5492b3545209a205ca1a54842be180eb63a16a6" +checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" dependencies = [ "cfg-if", "lazy_static", @@ -630,9 +631,9 @@ dependencies = [ [[package]] name = "crossterm" -version = "0.23.0" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77b75a27dc8d220f1f8521ea69cd55a34d720a200ebb3a624d9aa19193d3b432" +checksum = "f1fd7173631a4e9e2ca8b32ae2fad58aab9843ea5aaf56642661937d87e28a3e" dependencies = [ "bitflags", "crossterm_winapi", @@ -731,9 +732,9 @@ dependencies = [ [[package]] name = "ctor" -version = "0.1.21" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccc0a48a9b826acdf4028595adc9db92caea352f7af011a3034acd172a52a0aa" +checksum = "f877be4f7c9f246b183111634f75baa039715e3f46ce860677d3b19a69fb229c" dependencies = [ "quote", "syn", @@ -832,9 +833,9 @@ dependencies = [ [[package]] name = "dirs-sys" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" dependencies = [ "libc", "redox_users", @@ -914,14 +915,14 @@ dependencies = [ "rustc_version", "toml", "vswhom", - "winreg 0.10.1", + "winreg", ] [[package]] name = "eml-parser" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "031fe36712cec8b81c5b76b555666ce855a4dfc2dcc35bb907046bf2ef545578" +checksum = "43e6fc6e74658e477675b59e61e10e9722cb2b845b0e2834df60f979c865e821" dependencies = [ "regex", ] @@ -966,9 +967,9 @@ dependencies = [ [[package]] name = "erased-serde" -version = "0.3.18" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56047058e1ab118075ca22f9ecd737bcc961aa3566a3019cb71388afa280bd8a" +checksum = "ad132dd8d0d0b546348d7d86cb3191aad14b34e5f979781fc005c80d4ac67ffd" dependencies = [ "serde", ] @@ -1011,9 +1012,9 @@ dependencies = [ [[package]] name = "fd-lock" -version = "3.0.4" +version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02ecad9808e0596f8956d14f7fa868f996290bd01c8d7329d6e5bc2bb76adf8f" +checksum = "46e245f4c8ec30c6415c56cb132c07e69e74f1942f6b4a4061da748b49f486ca" dependencies = [ "cfg-if", "rustix", @@ -1431,9 +1432,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "html5ever" -version = "0.25.1" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aafcf38a1a36118242d29b92e1b08ef84e67e4a5ed06e0a80be20e6a32bfed6b" +checksum = "e5c13fb08e5d4dfc151ee5e88bae63f7773d61852f3bdc73c9f4b9e1bde03148" dependencies = [ "log", "mac", @@ -1494,9 +1495,9 @@ dependencies = [ [[package]] name = "hyper" -version = "0.14.17" +version = "0.14.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "043f0e083e9901b6cc658a77d1eb86f4fc650bbb977a4337dd63192826aa85dd" +checksum = "b26ae0a80afebe130861d90abf98e3814a4f28a4c6ffeb5ab8ebb2be311e0ef2" dependencies = [ "bytes", "futures-channel", @@ -1605,9 +1606,9 @@ dependencies = [ [[package]] name = "io-lifetimes" -version = "0.5.3" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec58677acfea8a15352d42fc87d11d63596ade9239e0a7c9352914417515dbe6" +checksum = "9448015e586b611e5d322f6703812bbca2f1e709d5773ecd38ddb4e3bb649504" [[package]] name = "ipnet" @@ -1735,9 +1736,9 @@ dependencies = [ [[package]] name = "lexical-write-float" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26f6202bff3d35ede41a6200227837468bb92e4ecdd437328b1055ed218fb855" +checksum = "8a89ec1d062e481210c309b672f73a0567b7855f21e7d2fae636df44d12e97f9" dependencies = [ "lexical-util", "lexical-write-integer", @@ -1756,9 +1757,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.119" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4" +checksum = "efaa7b300f3b5fe8eb6bf21ce3895e1751d9665086af2d64b42f19701015ff4f" [[package]] name = "libgit2-sys" @@ -1843,9 +1844,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.14" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8" dependencies = [ "cfg-if", ] @@ -2020,14 +2021,15 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba272f85fa0b41fc91872be579b3bbe0f56b792aa361a380eb669469f68dafb2" +checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9" dependencies = [ "libc", "log", "miow", "ntapi", + "wasi 0.11.0+wasi-snapshot-preview1", "winapi", ] @@ -2717,9 +2719,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "owo-colors" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20448fd678ec04e6ea15bbe0476874af65e98a01515d667aa49f1434dc44ebf4" +checksum = "5e72e30578e0d0993c8ae20823dd9cff2bc5517d2f586a8aef462a581e8a03eb" [[package]] name = "parking_lot" @@ -3111,9 +3113,9 @@ dependencies = [ [[package]] name = "pretty_assertions" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d5b548b725018ab5496482b45cb8bef21e9fed1858a6d674e3a8a0f0bb5d50" +checksum = "57c038cb5319b9c704bf9c227c261d275bfec0ad438118a2787ce47944fb228b" dependencies = [ "ansi_term", "ctor", @@ -3234,9 +3236,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" +checksum = "632d02bff7f874a36f33ea8bb416cd484b90cc66c1194b1a1110d067a7013f58" dependencies = [ "proc-macro2", ] @@ -3405,27 +3407,28 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8380fe0152551244f0747b1bf41737e0f8a74f97a14ccefd1148187271634f3c" +checksum = "8ae183fc1b06c149f0c1793e1eb447c8b04bfe46d48e9e48bfb8d2d7ed64ecf0" dependencies = [ "bitflags", ] [[package]] name = "redox_users" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +checksum = "7776223e2696f1aa4c6b0170e83212f47296a00424305117d013dfe86fb0fe55" dependencies = [ "getrandom 0.2.5", "redox_syscall", + "thiserror", ] [[package]] name = "reedline" version = "0.3.1" -source = "git+https://github.com/nushell/reedline#bc528de132e74594fdd5a9202cf32aee51e921e8" +source = "git+https://github.com/nushell/reedline?branch=main#e982abf7e21dcb41e9e1d715f64259a4817bb1bc" dependencies = [ "chrono", "crossterm", @@ -3473,9 +3476,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.9" +version = "0.11.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f242f1488a539a79bac6dbe7c8609ae43b7914b7736210f239a37cccb32525" +checksum = "46a1f7aa4f35e5e8b4160449f51afc758f0ce6454315a9fa7d0d113e958c41eb" dependencies = [ "base64", "bytes", @@ -3504,7 +3507,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "winreg 0.7.0", + "winreg", ] [[package]] @@ -3598,9 +3601,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.33.4" +version = "0.34.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef7ec6a44fba95d21fa522760c03c16ca5ee95cebb6e4ef579cab3e6d7ba6c06" +checksum = "cd3cc851a13d30a34cb747ba2a0c5101a4b2e8b1677a29b213ee465365ea495e" dependencies = [ "bitflags", "errno", @@ -4125,9 +4128,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.86" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" +checksum = "ea297be220d52398dcc07ce15a209fce436d361735ac1db700cab3b6cdfb9f54" dependencies = [ "proc-macro2", "quote", @@ -4304,7 +4307,7 @@ dependencies = [ "bytes", "libc", "memchr", - "mio 0.8.0", + "mio 0.8.2", "num_cpus", "pin-project-lite", "socket2", @@ -4521,9 +4524,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "utf8-width" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cf7d77f457ef8dfa11e4cd5933c5ddb5dc52a94664071951219a97710f0a32b" +checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1" [[package]] name = "utf8parse" @@ -4647,6 +4650,12 @@ version = "0.10.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "wasm-bindgen" version = "0.2.79" @@ -4725,9 +4734,9 @@ dependencies = [ [[package]] name = "which" -version = "4.2.4" +version = "4.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a5a7e487e921cf220206864a94a89b6c6905bfc19f1057fa26a4cb360e5c1d2" +checksum = "5c4fb54e6113b6a8772ee41c3404fb0301ac79604489467e0a9ce1f3e97c24ae" dependencies = [ "either", "lazy_static", @@ -4881,15 +4890,6 @@ version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316" -[[package]] -name = "winreg" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" -dependencies = [ - "winapi", -] - [[package]] name = "winreg" version = "0.10.1" @@ -4916,9 +4916,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.5.3" +version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50344758e2f40e3a1fcfc8f6f91aa57b5f8ebd8d27919fe6451f15aaaf9ee608" +checksum = "7eb5728b8afd3f280a869ce1d4c554ffaed35f45c231fc41bfbd0381bef50317" [[package]] name = "zip" diff --git a/Cargo.toml b/Cargo.toml index a6c560b9e..bc1c4366e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,7 @@ crossterm_winapi = "0.9.0" ctrlc = "3.2.1" log = "0.4" miette = "4.1.0" -nu-ansi-term = "0.45.0" +nu-ansi-term = "0.45.1" nu-cli = { path="./crates/nu-cli", version = "0.60.1" } nu-color-config = { path = "./crates/nu-color-config", version = "0.60.1" } nu-command = { path="./crates/nu-command", version = "0.60.1" } @@ -55,7 +55,8 @@ nu-term-grid = { path = "./crates/nu-term-grid", version = "0.60.1" } nu-utils = { path = "./crates/nu-utils", version = "0.60.1" } pretty_env_logger = "0.4.0" rayon = "1.5.1" -reedline = { git = "https://github.com/nushell/reedline" } +#reedline = "0.3.0" +reedline = { git = "https://github.com/nushell/reedline", branch = "main" } is_executable = "1.0.1" [dev-dependencies] diff --git a/crates/nu-cli/Cargo.toml b/crates/nu-cli/Cargo.toml index c1e77514f..4c719db20 100644 --- a/crates/nu-cli/Cargo.toml +++ b/crates/nu-cli/Cargo.toml @@ -12,13 +12,16 @@ nu-path = { path = "../nu-path", version = "0.60.1" } nu-parser = { path = "../nu-parser", version = "0.60.1" } nu-protocol = { path = "../nu-protocol", version = "0.60.1" } nu-utils = { path = "../nu-utils", version = "0.60.1" } -nu-ansi-term = "0.45.0" +nu-ansi-term = "0.45.1" nu-color-config = { path = "../nu-color-config", version = "0.60.1" } + crossterm = "0.23.0" crossterm_winapi = "0.9.0" miette = { version = "4.1.0", features = ["fancy"] } thiserror = "1.0.29" -reedline = { git = "https://github.com/nushell/reedline" } +#reedline = "0.3.0" +reedline = { git = "https://github.com/nushell/reedline", branch = "main" } + log = "0.4" is_executable = "1.0.1" diff --git a/crates/nu-cli/src/completions.rs b/crates/nu-cli/src/completions.rs index 371f74ccf..a67237dab 100644 --- a/crates/nu-cli/src/completions.rs +++ b/crates/nu-cli/src/completions.rs @@ -5,7 +5,7 @@ use nu_protocol::{ engine::{EngineState, Stack, StateWorkingSet}, PipelineData, Span, Value, CONFIG_VARIABLE_ID, }; -use reedline::Completer; +use reedline::{Completer, Suggestion}; const SEP: char = std::path::MAIN_SEPARATOR; @@ -83,46 +83,49 @@ impl NuCompleter { prefix: &[u8], span: Span, offset: usize, - ) -> Vec<(reedline::Span, String)> { + ) -> Vec { let mut output = vec![]; let builtins = ["$nu", "$in", "$config", "$env", "$nothing"]; for builtin in builtins { if builtin.as_bytes().starts_with(prefix) { - output.push(( - reedline::Span { + output.push(Suggestion { + value: builtin.to_string(), + description: None, + span: reedline::Span { start: span.start - offset, end: span.end - offset, }, - builtin.to_string(), - )); + }); } } for scope in &working_set.delta.scope { for v in &scope.vars { if v.0.starts_with(prefix) { - output.push(( - reedline::Span { + output.push(Suggestion { + value: String::from_utf8_lossy(v.0).to_string(), + description: None, + span: reedline::Span { start: span.start - offset, end: span.end - offset, }, - String::from_utf8_lossy(v.0).to_string(), - )); + }); } } } for scope in &self.engine_state.scope { for v in &scope.vars { if v.0.starts_with(prefix) { - output.push(( - reedline::Span { + output.push(Suggestion { + value: String::from_utf8_lossy(v.0).to_string(), + description: None, + span: reedline::Span { start: span.start - offset, end: span.end - offset, }, - String::from_utf8_lossy(v.0).to_string(), - )); + }); } } } @@ -138,34 +141,32 @@ impl NuCompleter { span: Span, offset: usize, find_externals: bool, - ) -> Vec<(reedline::Span, String)> { + ) -> Vec { let prefix = working_set.get_span_contents(span); let results = working_set .find_commands_by_prefix(prefix) .into_iter() - .map(move |x| { - ( - reedline::Span { - start: span.start - offset, - end: span.end - offset, - }, - String::from_utf8_lossy(&x).to_string(), - ) + .map(move |x| Suggestion { + value: String::from_utf8_lossy(&x).to_string(), + description: None, + span: reedline::Span { + start: span.start - offset, + end: span.end - offset, + }, }); let results_aliases = working_set .find_aliases_by_prefix(prefix) .into_iter() - .map(move |x| { - ( - reedline::Span { - start: span.start - offset, - end: span.end - offset, - }, - String::from_utf8_lossy(&x).to_string(), - ) + .map(move |x| Suggestion { + value: String::from_utf8_lossy(&x).to_string(), + description: None, + span: reedline::Span { + start: span.start - offset, + end: span.end - offset, + }, }); let mut results = results.chain(results_aliases).collect::>(); @@ -176,19 +177,22 @@ impl NuCompleter { let results_external = self.external_command_completion(&prefix) .into_iter() - .map(move |x| { - ( - reedline::Span { - start: span.start - offset, - end: span.end - offset, - }, - x, - ) + .map(move |x| Suggestion { + value: x, + description: None, + span: reedline::Span { + start: span.start - offset, + end: span.end - offset, + }, }); for external in results_external { if results.contains(&external) { - results.push((external.0, format!("^{}", external.1))) + results.push(Suggestion { + value: format!("^{}", external.value), + description: None, + span: external.span, + }) } else { results.push(external) } @@ -200,7 +204,7 @@ impl NuCompleter { } } - fn completion_helper(&self, line: &str, pos: usize) -> Vec<(reedline::Span, String)> { + fn completion_helper(&self, line: &str, pos: usize) -> Vec { let mut working_set = StateWorkingSet::new(&self.engine_state); let offset = working_set.next_span_start(); let mut line = line.to_string(); @@ -231,7 +235,7 @@ impl NuCompleter { if prefix.starts_with(b"$") { let mut output = self.complete_variables(&working_set, &prefix, new_span, offset); - output.sort_by(|a, b| a.1.cmp(&b.1)); + output.sort_by(|a, b| a.value.cmp(&b.value)); return output; } if prefix.starts_with(b"-") { @@ -248,13 +252,14 @@ impl NuCompleter { short.encode_utf8(&mut named); named.insert(0, b'-'); if named.starts_with(&prefix) { - output.push(( - reedline::Span { + output.push(Suggestion { + value: String::from_utf8_lossy(&named).to_string(), + description: None, + span: reedline::Span { start: new_span.start - offset, end: new_span.end - offset, }, - String::from_utf8_lossy(&named).to_string(), - )); + }); } } @@ -266,16 +271,17 @@ impl NuCompleter { named.insert(0, b'-'); named.insert(0, b'-'); if named.starts_with(&prefix) { - output.push(( - reedline::Span { + output.push(Suggestion { + value: String::from_utf8_lossy(&named).to_string(), + description: None, + span: reedline::Span { start: new_span.start - offset, end: new_span.end - offset, }, - String::from_utf8_lossy(&named).to_string(), - )); + }); } } - output.sort_by(|a, b| a.1.cmp(&b.1)); + output.sort_by(|a, b| a.value.cmp(&b.value)); return output; } } @@ -317,18 +323,19 @@ impl NuCompleter { list: impl Iterator, new_span: Span, offset: usize, - ) -> Vec<(reedline::Span, String)> { + ) -> Vec { list.filter_map(move |x| { let s = x.as_string(); match s { - Ok(s) => Some(( - reedline::Span { + Ok(s) => Some(Suggestion { + value: s, + description: None, + span: reedline::Span { start: new_span.start - offset, end: new_span.end - offset, }, - s, - )), + }), Err(_) => None, } }) @@ -388,17 +395,19 @@ impl NuCompleter { _ => (vec![], CompletionOptions::default()), }; - let mut completions: Vec<(reedline::Span, String)> = completions + let mut completions: Vec = completions .into_iter() .filter(|it| { // Minimise clones for new functionality match (options.case_sensitive, options.positional) { - (true, true) => it.1.as_bytes().starts_with(&prefix), - (true, false) => it.1.contains( + (true, true) => { + it.value.as_bytes().starts_with(&prefix) + } + (true, false) => it.value.contains( std::str::from_utf8(&prefix).unwrap_or(""), ), (false, positional) => { - let value = it.1.to_lowercase(); + let value = it.value.to_lowercase(); let prefix = std::str::from_utf8(&prefix) .unwrap_or("") .to_lowercase(); @@ -413,7 +422,7 @@ impl NuCompleter { .collect(); if options.sort { - completions.sort_by(|a, b| a.1.cmp(&b.1)); + completions.sort_by(|a, b| a.value.cmp(&b.value)); } return completions; @@ -431,17 +440,16 @@ impl NuCompleter { let mut output: Vec<_> = file_path_completion(new_span, &prefix, &cwd) .into_iter() - .map(move |x| { - ( - reedline::Span { - start: x.0.start - offset, - end: x.0.end - offset, - }, - x.1, - ) + .map(move |x| Suggestion { + value: x.1, + description: None, + span: reedline::Span { + start: x.0.start - offset, + end: x.0.end - offset, + }, }) .collect(); - output.sort_by(|a, b| a.1.cmp(&b.1)); + output.sort_by(|a, b| a.value.cmp(&b.value)); return output; } flat_shape => { @@ -542,20 +550,19 @@ impl NuCompleter { (x.0, x.1) } }) - .map(move |x| { - ( - reedline::Span { - start: x.0.start - offset, - end: x.0.end - offset, - }, - x.1, - ) + .map(move |x| Suggestion { + value: x.1, + description: None, + span: reedline::Span { + start: x.0.start - offset, + end: x.0.end - offset, + }, }) .chain(subcommands.into_iter()) .chain(commands.into_iter()) .collect::>(); //output.dedup_by(|a, b| a.1 == b.1); - output.sort_by(|a, b| a.1.cmp(&b.1)); + output.sort_by(|a, b| a.value.cmp(&b.value)); return output; } @@ -570,7 +577,7 @@ impl NuCompleter { } impl Completer for NuCompleter { - fn complete(&self, line: &str, pos: usize) -> Vec<(reedline::Span, String)> { + fn complete(&self, line: &str, pos: usize) -> Vec { self.completion_helper(line, pos) } } diff --git a/crates/nu-cli/src/help_completions.rs b/crates/nu-cli/src/help_completions.rs new file mode 100644 index 000000000..12d336d57 --- /dev/null +++ b/crates/nu-cli/src/help_completions.rs @@ -0,0 +1,101 @@ +use nu_engine::documentation::get_flags_section; +use nu_protocol::engine::EngineState; +use reedline::{Completer, Suggestion}; + +pub const EXAMPLE_MARKER: &str = ">>>>>>"; +pub const EXAMPLE_NEW_LINE: &str = "%%%%%%"; + +pub struct NuHelpCompleter { + engine_state: EngineState, +} + +impl NuHelpCompleter { + pub fn new(engine_state: EngineState) -> Self { + Self { engine_state } + } + + fn completion_helper(&self, line: &str, _pos: usize) -> Vec { + let full_commands = self.engine_state.get_signatures_with_examples(false); + + //Vec<(Signature, Vec, bool, bool)> { + full_commands + .iter() + .filter(|(sig, _, _, _)| { + sig.name.to_lowercase().contains(&line.to_lowercase()) + || sig.usage.to_lowercase().contains(&line.to_lowercase()) + || sig + .extra_usage + .to_lowercase() + .contains(&line.to_lowercase()) + }) + .map(|(sig, examples, _, _)| { + let mut long_desc = String::new(); + + let usage = &sig.usage; + if !usage.is_empty() { + long_desc.push_str(usage); + long_desc.push_str("\r\n\r\n"); + } + + let extra_usage = &sig.extra_usage; + if !extra_usage.is_empty() { + long_desc.push_str(extra_usage); + long_desc.push_str("\r\n\r\n"); + } + + long_desc.push_str(&format!("Usage:\r\n > {}\r\n", sig.call_signature())); + + if !sig.named.is_empty() { + long_desc.push_str(&get_flags_section(sig)) + } + + if !sig.required_positional.is_empty() + || !sig.optional_positional.is_empty() + || sig.rest_positional.is_some() + { + long_desc.push_str("\r\nParameters:\r\n"); + for positional in &sig.required_positional { + long_desc + .push_str(&format!(" {}: {}\r\n", positional.name, positional.desc)); + } + for positional in &sig.optional_positional { + long_desc.push_str(&format!( + " (optional) {}: {}\r\n", + positional.name, positional.desc + )); + } + + if let Some(rest_positional) = &sig.rest_positional { + long_desc.push_str(&format!( + " ...{}: {}\r\n", + rest_positional.name, rest_positional.desc + )); + } + } + + for example in examples { + long_desc.push_str(&format!( + "{}{}\r\n", + EXAMPLE_MARKER, + example.example.replace('\n', EXAMPLE_NEW_LINE) + )) + } + + Suggestion { + value: sig.name.clone(), + description: Some(long_desc), + span: reedline::Span { + start: 0, + end: sig.name.len(), + }, + } + }) + .collect() + } +} + +impl Completer for NuHelpCompleter { + fn complete(&self, line: &str, pos: usize) -> Vec { + self.completion_helper(line, pos) + } +} diff --git a/crates/nu-cli/src/help_menu.rs b/crates/nu-cli/src/help_menu.rs new file mode 100644 index 000000000..c8b9ae47d --- /dev/null +++ b/crates/nu-cli/src/help_menu.rs @@ -0,0 +1,718 @@ +use { + crate::help_completions::{EXAMPLE_MARKER, EXAMPLE_NEW_LINE}, + nu_ansi_term::{ansi::RESET, Style}, + reedline::{ + menu_functions::string_difference, Completer, History, LineBuffer, Menu, MenuEvent, + MenuTextStyle, Painter, Suggestion, + }, +}; + +/// Default values used as reference for the menu. These values are set during +/// the initial declaration of the menu and are always kept as reference for the +/// changeable [`WorkingDetails`] +struct DefaultMenuDetails { + /// Number of columns that the menu will have + pub columns: u16, + /// Column width + pub col_width: Option, + /// Column padding + pub col_padding: usize, + /// Number of rows for commands + pub selection_rows: u16, + /// Number of rows allowed to display the description + pub description_rows: usize, +} + +impl Default for DefaultMenuDetails { + fn default() -> Self { + Self { + columns: 4, + col_width: None, + col_padding: 2, + selection_rows: 4, + description_rows: 10, + } + } +} + +/// Represents the actual column conditions of the menu. These conditions change +/// since they need to accommodate possible different line sizes for the column values +#[derive(Default)] +struct WorkingDetails { + /// Number of columns that the menu will have + pub columns: u16, + /// Column width + pub col_width: usize, + /// Number of rows for description + pub description_rows: usize, +} + +/// Completion menu definition +pub struct NuHelpMenu { + active: bool, + /// Menu coloring + color: MenuTextStyle, + /// Default column details that are set when creating the menu + /// These values are the reference for the working details + default_details: DefaultMenuDetails, + /// Number of minimum rows that are displayed when + /// the required lines is larger than the available lines + min_rows: u16, + /// Working column details keep changing based on the collected values + working_details: WorkingDetails, + /// Menu cached values + values: Vec, + /// column position of the cursor. Starts from 0 + col_pos: u16, + /// row position in the menu. Starts from 0 + row_pos: u16, + /// Menu marker when active + marker: String, + /// Event sent to the menu + event: Option, + /// String collected after the menu is activated + input: Option, + /// Examples to select + examples: Vec, + /// Example index + example_index: Option, + /// Examples may not be shown if there is not enough space in the screen + show_examples: bool, + /// Skipped description rows + skipped_rows: usize, +} + +impl Default for NuHelpMenu { + fn default() -> Self { + Self { + active: false, + color: MenuTextStyle::default(), + default_details: DefaultMenuDetails::default(), + min_rows: 3, + working_details: WorkingDetails::default(), + values: Vec::new(), + col_pos: 0, + row_pos: 0, + marker: "| ".to_string(), + event: None, + input: None, + examples: Vec::new(), + example_index: None, + show_examples: true, + skipped_rows: 0, + } + } +} + +impl NuHelpMenu { + /// Menu builder with new value for text style + pub fn with_text_style(mut self, text_style: Style) -> Self { + self.color.text_style = text_style; + self + } + + /// Menu builder with new value for text style + pub fn with_selected_text_style(mut self, selected_text_style: Style) -> Self { + self.color.selected_text_style = selected_text_style; + self + } + + /// Menu builder with new value for text style + pub fn with_description_text_style(mut self, description_text_style: Style) -> Self { + self.color.description_style = description_text_style; + self + } + + /// Menu builder with new columns value + pub fn with_columns(mut self, columns: u16) -> Self { + self.default_details.columns = columns; + self + } + + /// Menu builder with new column width value + pub fn with_column_width(mut self, col_width: Option) -> Self { + self.default_details.col_width = col_width; + self + } + + /// Menu builder with new column width value + pub fn with_column_padding(mut self, col_padding: usize) -> Self { + self.default_details.col_padding = col_padding; + self + } + + /// Menu builder with new selection rows value + pub fn with_selection_rows(mut self, selection_rows: u16) -> Self { + self.default_details.selection_rows = selection_rows; + self + } + + /// Menu builder with new description rows value + pub fn with_description_rows(mut self, description_rows: usize) -> Self { + self.default_details.description_rows = description_rows; + self + } + + /// Menu builder with marker + pub fn with_marker(mut self, marker: String) -> Self { + self.marker = marker; + self + } + + /// Move menu cursor to the next element + fn move_next(&mut self) { + let mut new_col = self.col_pos + 1; + let mut new_row = self.row_pos; + + if new_col >= self.get_cols() { + new_row += 1; + new_col = 0; + } + + if new_row >= self.get_rows() { + new_row = 0; + new_col = 0; + } + + let position = new_row * self.get_cols() + new_col; + if position >= self.get_values().len() as u16 { + self.reset_position(); + } else { + self.col_pos = new_col; + self.row_pos = new_row; + } + } + + /// Move menu cursor to the previous element + fn move_previous(&mut self) { + let new_col = self.col_pos.checked_sub(1); + + let (new_col, new_row) = match new_col { + Some(col) => (col, self.row_pos), + None => match self.row_pos.checked_sub(1) { + Some(row) => (self.get_cols().saturating_sub(1), row), + None => ( + self.get_cols().saturating_sub(1), + self.get_rows().saturating_sub(1), + ), + }, + }; + + let position = new_row * self.get_cols() + new_col; + if position >= self.get_values().len() as u16 { + self.col_pos = (self.get_values().len() as u16 % self.get_cols()).saturating_sub(1); + self.row_pos = self.get_rows().saturating_sub(1); + } else { + self.col_pos = new_col; + self.row_pos = new_row; + } + } + + /// Menu index based on column and row position + fn index(&self) -> usize { + let index = self.row_pos * self.get_cols() + self.col_pos; + index as usize + } + + /// Get selected value from the menu + fn get_value(&self) -> Option { + self.get_values().get(self.index()).cloned() + } + + /// Calculates how many rows the Menu will use + fn get_rows(&self) -> u16 { + let values = self.get_values().len() as u16; + + if values == 0 { + // When the values are empty the no_records_msg is shown, taking 1 line + return 1; + } + + let rows = values / self.get_cols(); + if values % self.get_cols() != 0 { + rows + 1 + } else { + rows + } + } + + /// Returns working details col width + fn get_width(&self) -> usize { + self.working_details.col_width + } + + /// Reset menu position + fn reset_position(&mut self) { + self.col_pos = 0; + self.row_pos = 0; + self.skipped_rows = 0; + } + + fn no_records_msg(&self, use_ansi_coloring: bool) -> String { + let msg = "TYPE TO START SEACH"; + if use_ansi_coloring { + format!( + "{}{}{}", + self.color.selected_text_style.prefix(), + msg, + RESET + ) + } else { + msg.to_string() + } + } + + /// Returns working details columns + fn get_cols(&self) -> u16 { + self.working_details.columns.max(1) + } + + /// End of line for menu + fn end_of_line(&self, column: u16, index: usize) -> &str { + let is_last = index == self.values.len().saturating_sub(1); + if column == self.get_cols().saturating_sub(1) || is_last { + "\r\n" + } else { + "" + } + } + + /// Update list of examples from the actual value + fn update_examples(&mut self) { + let examples = self + .get_value() + .and_then(|suggestion| suggestion.description) + .unwrap_or_else(|| "".to_string()) + .lines() + .filter(|line| line.starts_with(EXAMPLE_MARKER)) + .map(|line| { + line.replace(EXAMPLE_MARKER, "") + .replace(EXAMPLE_NEW_LINE, "\r\n") + }) + .collect::>(); + + self.examples = examples; + self.example_index = None; + } + + /// Creates default string that represents one suggestion from the menu + fn create_entry_string( + &self, + suggestion: &Suggestion, + index: usize, + column: u16, + empty_space: usize, + use_ansi_coloring: bool, + ) -> String { + if use_ansi_coloring { + if index == self.index() { + format!( + "{}{}{:>empty$}{}{}", + self.color.selected_text_style.prefix(), + &suggestion.value, + "", + RESET, + self.end_of_line(column, index), + empty = empty_space, + ) + } else { + format!( + "{}{}{:>empty$}{}{}", + self.color.text_style.prefix(), + &suggestion.value, + "", + RESET, + self.end_of_line(column, index), + empty = empty_space, + ) + } + } else { + // If no ansi coloring is found, then the selection word is + // the line in uppercase + let (marker, empty_space) = if index == self.index() { + (">", empty_space.saturating_sub(1)) + } else { + ("", empty_space) + }; + + let line = format!( + "{}{}{:>empty$}{}", + marker, + &suggestion.value, + "", + self.end_of_line(column, index), + empty = empty_space, + ); + + if index == self.index() { + line.to_uppercase() + } else { + line + } + } + } + + /// Description string with color + fn create_description_string(&self, use_ansi_coloring: bool) -> String { + let description = self + .get_value() + .and_then(|suggestion| suggestion.description) + .unwrap_or_else(|| "".to_string()) + .lines() + .filter(|line| !line.starts_with(EXAMPLE_MARKER)) + .skip(self.skipped_rows) + .take(self.working_details.description_rows) + .collect::>() + .join("\r\n"); + + if use_ansi_coloring && !description.is_empty() { + format!( + "{}{}{}", + self.color.description_style.prefix(), + description, + RESET, + ) + } else { + description + } + } + + /// Selectable list of examples from the actual value + fn create_example_string(&self, use_ansi_coloring: bool) -> String { + if !self.show_examples { + return "".into(); + } + + let examples: String = self + .examples + .iter() + .enumerate() + .map(|(index, example)| { + if let Some(example_index) = self.example_index { + if index == example_index { + format!( + " {}{}{}\r\n", + self.color.selected_text_style.prefix(), + example, + RESET + ) + } else { + format!(" {}\r\n", example) + } + } else { + format!(" {}\r\n", example) + } + }) + .collect(); + + if examples.is_empty() { + "".into() + } else if use_ansi_coloring { + format!( + "{}\r\n\r\nExamples:\r\n{}{}", + self.color.description_style.prefix(), + RESET, + examples, + ) + } else { + format!("\r\n\r\nExamples:\r\n{}", examples,) + } + } +} + +impl Menu for NuHelpMenu { + /// Menu name + fn name(&self) -> &str { + "help_menu" + } + + /// Menu indicator + fn indicator(&self) -> &str { + self.marker.as_str() + } + + /// Deactivates context menu + fn is_active(&self) -> bool { + self.active + } + + /// The help menu stays active even with one record + fn can_quick_complete(&self) -> bool { + false + } + + /// The help menu does not need to partially complete + fn can_partially_complete( + &mut self, + _values_updated: bool, + _line_buffer: &mut LineBuffer, + _history: &dyn History, + _completer: &dyn Completer, + ) -> bool { + false + } + + /// Selects what type of event happened with the menu + fn menu_event(&mut self, event: MenuEvent) { + match &event { + MenuEvent::Activate(_) => self.active = true, + MenuEvent::Deactivate => { + self.active = false; + self.input = None; + self.values = Vec::new(); + } + _ => {} + }; + + self.event = Some(event); + } + + /// Updates menu values + fn update_values( + &mut self, + line_buffer: &mut LineBuffer, + _history: &dyn History, + completer: &dyn Completer, + ) { + if let Some(old_string) = &self.input { + let (start, input) = string_difference(line_buffer.get_buffer(), old_string); + if !input.is_empty() { + self.reset_position(); + self.values = completer + .complete(input, line_buffer.insertion_point()) + .into_iter() + .map(|suggestion| Suggestion { + value: suggestion.value, + description: suggestion.description, + span: reedline::Span { + start, + end: start + input.len(), + }, + }) + .collect(); + } + } + } + + /// The working details for the menu changes based on the size of the lines + /// collected from the completer + fn update_working_details( + &mut self, + line_buffer: &mut LineBuffer, + history: &dyn History, + completer: &dyn Completer, + painter: &Painter, + ) { + if let Some(event) = self.event.take() { + // Updating all working parameters from the menu before executing any of the + // possible event + let max_width = self.get_values().iter().fold(0, |acc, suggestion| { + let str_len = suggestion.value.len() + self.default_details.col_padding; + if str_len > acc { + str_len + } else { + acc + } + }); + + // If no default width is found, then the total screen width is used to estimate + // the column width based on the default number of columns + let default_width = if let Some(col_width) = self.default_details.col_width { + col_width + } else { + let col_width = painter.screen_width() / self.default_details.columns; + col_width as usize + }; + + // Adjusting the working width of the column based the max line width found + // in the menu values + if max_width > default_width { + self.working_details.col_width = max_width; + } else { + self.working_details.col_width = default_width; + }; + + // The working columns is adjusted based on possible number of columns + // that could be fitted in the screen with the calculated column width + let possible_cols = painter.screen_width() / self.working_details.col_width as u16; + if possible_cols > self.default_details.columns { + self.working_details.columns = self.default_details.columns.max(1); + } else { + self.working_details.columns = possible_cols; + } + + // Updating the working rows to display the description + if self.menu_required_lines(painter.screen_width()) <= painter.remaining_lines() { + self.working_details.description_rows = self.default_details.description_rows; + self.show_examples = true; + } else { + self.working_details.description_rows = painter + .remaining_lines() + .saturating_sub(self.default_details.selection_rows + 1) + as usize; + + self.show_examples = false; + } + + match event { + MenuEvent::Activate(_) => { + self.reset_position(); + self.input = Some(line_buffer.get_buffer().to_string()); + self.update_values(line_buffer, history, completer); + } + MenuEvent::Deactivate => self.active = false, + MenuEvent::Edit(_) => { + self.reset_position(); + self.update_values(line_buffer, history, completer); + self.update_examples() + } + MenuEvent::NextElement => { + self.skipped_rows = 0; + self.move_next(); + self.update_examples(); + } + MenuEvent::PreviousElement => { + self.skipped_rows = 0; + self.move_previous(); + self.update_examples(); + } + MenuEvent::MoveUp => { + if let Some(example_index) = self.example_index { + if let Some(index) = example_index.checked_sub(1) { + self.example_index = Some(index); + } else { + self.example_index = Some(self.examples.len().saturating_sub(1)); + } + } else { + self.example_index = Some(0); + } + } + MenuEvent::MoveDown => { + if let Some(example_index) = self.example_index { + let index = example_index + 1; + if index < self.examples.len() { + self.example_index = Some(index); + } else { + self.example_index = Some(0); + } + } else { + self.example_index = Some(0); + } + } + MenuEvent::MoveLeft => self.skipped_rows = self.skipped_rows.saturating_sub(1), + MenuEvent::MoveRight => { + let skipped = self.skipped_rows + 1; + let description_rows = self + .get_value() + .and_then(|suggestion| suggestion.description) + .unwrap_or_else(|| "".to_string()) + .lines() + .filter(|line| !line.starts_with(EXAMPLE_MARKER)) + .count(); + + let allowed_skips = + description_rows.saturating_sub(self.working_details.description_rows); + + if skipped < allowed_skips { + self.skipped_rows = skipped; + } else { + self.skipped_rows = allowed_skips; + } + } + MenuEvent::PreviousPage | MenuEvent::NextPage => {} + } + } + } + + /// The buffer gets replaced in the Span location + fn replace_in_buffer(&self, line_buffer: &mut LineBuffer) { + if let Some(Suggestion { value, span, .. }) = self.get_value() { + let string_len = if let Some(example_index) = self.example_index { + let example = self + .examples + .get(example_index) + .expect("the example index is always checked"); + line_buffer.replace(span.start..span.end, example); + example.len() + } else { + line_buffer.replace(span.start..span.end, &value); + value.len() + }; + + let mut offset = line_buffer.insertion_point(); + offset += string_len.saturating_sub(span.end - span.start); + line_buffer.set_insertion_point(offset); + } + } + + /// Minimum rows that should be displayed by the menu + fn min_rows(&self) -> u16 { + self.get_rows().min(self.min_rows) + } + + /// Gets values from filler that will be displayed in the menu + fn get_values(&self) -> &[Suggestion] { + &self.values + } + + fn menu_required_lines(&self, _terminal_columns: u16) -> u16 { + let example_lines = self + .examples + .iter() + .fold(0, |acc, example| example.lines().count() + acc); + + self.default_details.selection_rows + + self.default_details.description_rows as u16 + + example_lines as u16 + + 3 + } + + fn menu_string(&self, _available_lines: u16, use_ansi_coloring: bool) -> String { + if self.get_values().is_empty() { + self.no_records_msg(use_ansi_coloring) + } else { + // The skip values represent the number of lines that should be skipped + // while printing the menu + let available_lines = self.default_details.selection_rows; + let skip_values = if self.row_pos >= available_lines { + let skip_lines = self.row_pos.saturating_sub(available_lines) + 1; + (skip_lines * self.get_cols()) as usize + } else { + 0 + }; + + // It seems that crossterm prefers to have a complete string ready to be printed + // rather than looping through the values and printing multiple things + // This reduces the flickering when printing the menu + let available_values = (available_lines * self.get_cols()) as usize; + let selection_values: String = self + .get_values() + .iter() + .skip(skip_values) + .take(available_values) + .enumerate() + .map(|(index, suggestion)| { + // Correcting the enumerate index based on the number of skipped values + let index = index + skip_values; + let column = index as u16 % self.get_cols(); + let empty_space = self.get_width().saturating_sub(suggestion.value.len()); + + self.create_entry_string( + suggestion, + index, + column, + empty_space, + use_ansi_coloring, + ) + }) + .collect(); + + format!( + "{}{}{}", + selection_values, + self.create_description_string(use_ansi_coloring), + self.create_example_string(use_ansi_coloring) + ) + } + } +} diff --git a/crates/nu-cli/src/lib.rs b/crates/nu-cli/src/lib.rs index d251ce10d..af9b0867c 100644 --- a/crates/nu-cli/src/lib.rs +++ b/crates/nu-cli/src/lib.rs @@ -3,6 +3,8 @@ mod completions; mod config_files; mod errors; mod eval_file; +mod help_completions; +mod help_menu; mod nu_highlight; mod print; mod prompt; @@ -18,6 +20,8 @@ pub use completions::NuCompleter; pub use config_files::eval_config_contents; pub use errors::CliError; pub use eval_file::evaluate_file; +pub use help_completions::NuHelpCompleter; +pub use help_menu::NuHelpMenu; pub use nu_highlight::NuHighlight; pub use print::Print; pub use prompt::NushellPrompt; diff --git a/crates/nu-cli/src/reedline_config.rs b/crates/nu-cli/src/reedline_config.rs index 1dd0caedc..8c2405513 100644 --- a/crates/nu-cli/src/reedline_config.rs +++ b/crates/nu-cli/src/reedline_config.rs @@ -1,9 +1,10 @@ +use super::NuHelpMenu; use crossterm::event::{KeyCode, KeyModifiers}; use nu_color_config::lookup_ansi_color_style; use nu_protocol::{extract_value, Config, ParsedKeybinding, ShellError, Span, Value}; use reedline::{ default_emacs_keybindings, default_vi_insert_keybindings, default_vi_normal_keybindings, - CompletionMenu, EditCommand, HistoryMenu, Keybindings, Reedline, ReedlineEvent, + Completer, CompletionMenu, EditCommand, HistoryMenu, Keybindings, Reedline, ReedlineEvent, }; // Creates an input object for the completion menu based on the dictionary @@ -64,7 +65,7 @@ pub(crate) fn add_completion_menu(line_editor: Reedline, config: &Config) -> Ree None => completion_menu, }; - line_editor.with_menu(Box::new(completion_menu)) + line_editor.with_menu(Box::new(completion_menu), None) } // Creates an input object for the history menu based on the dictionary @@ -120,10 +121,119 @@ pub(crate) fn add_history_menu(line_editor: Reedline, config: &Config) -> Reedli None => history_menu, }; - line_editor.with_menu(Box::new(history_menu)) + line_editor.with_menu(Box::new(history_menu), None) +} + +// Creates an input object for the help menu based on the dictionary +// stored in the config variable +pub(crate) fn add_help_menu( + line_editor: Reedline, + help_completer: Box, + config: &Config, +) -> Reedline { + let mut help_menu = NuHelpMenu::default(); + + help_menu = match config + .help_config + .get("columns") + .and_then(|value| value.as_integer().ok()) + { + Some(value) => help_menu.with_columns(value as u16), + None => help_menu, + }; + + help_menu = help_menu.with_column_width( + config + .help_config + .get("col_width") + .and_then(|value| value.as_integer().ok()) + .map(|value| value as usize), + ); + + help_menu = match config + .help_config + .get("col_padding") + .and_then(|value| value.as_integer().ok()) + { + Some(value) => help_menu.with_column_padding(value as usize), + None => help_menu, + }; + + help_menu = match config + .help_config + .get("selection_rows") + .and_then(|value| value.as_integer().ok()) + { + Some(value) => help_menu.with_selection_rows(value as u16), + None => help_menu, + }; + + help_menu = match config + .help_config + .get("description_rows") + .and_then(|value| value.as_integer().ok()) + { + Some(value) => help_menu.with_description_rows(value as usize), + None => help_menu, + }; + + help_menu = match config + .help_config + .get("text_style") + .and_then(|value| value.as_string().ok()) + { + Some(value) => help_menu.with_text_style(lookup_ansi_color_style(&value)), + None => help_menu, + }; + + help_menu = match config + .help_config + .get("selected_text_style") + .and_then(|value| value.as_string().ok()) + { + Some(value) => help_menu.with_selected_text_style(lookup_ansi_color_style(&value)), + None => help_menu, + }; + + help_menu = match config + .help_config + .get("description_text_style") + .and_then(|value| value.as_string().ok()) + { + Some(value) => help_menu.with_description_text_style(lookup_ansi_color_style(&value)), + None => help_menu, + }; + + help_menu = match config + .help_config + .get("marker") + .and_then(|value| value.as_string().ok()) + { + Some(value) => help_menu.with_marker(value), + None => help_menu, + }; + + line_editor.with_menu(Box::new(help_menu), Some(help_completer)) } fn add_menu_keybindings(keybindings: &mut Keybindings) { + // Completer menu keybindings + keybindings.add_binding( + KeyModifiers::NONE, + KeyCode::Tab, + ReedlineEvent::UntilFound(vec![ + ReedlineEvent::Menu("completer_menu".to_string()), + ReedlineEvent::MenuNext, + ]), + ); + + keybindings.add_binding( + KeyModifiers::SHIFT, + KeyCode::BackTab, + ReedlineEvent::MenuPrevious, + ); + + // History menu keybinding keybindings.add_binding( KeyModifiers::CONTROL, KeyCode::Char('x'), @@ -142,19 +252,11 @@ fn add_menu_keybindings(keybindings: &mut Keybindings) { ]), ); + // Help menu keybinding keybindings.add_binding( - KeyModifiers::NONE, - KeyCode::Tab, - ReedlineEvent::UntilFound(vec![ - ReedlineEvent::Menu("completion_menu".to_string()), - ReedlineEvent::MenuNext, - ]), - ); - - keybindings.add_binding( - KeyModifiers::SHIFT, - KeyCode::BackTab, - ReedlineEvent::MenuPrevious, + KeyModifiers::CONTROL, + KeyCode::Char('i'), + ReedlineEvent::Menu("help_menu".to_string()), ); } diff --git a/crates/nu-cli/src/repl.rs b/crates/nu-cli/src/repl.rs index c80364c05..fd059d3bb 100644 --- a/crates/nu-cli/src/repl.rs +++ b/crates/nu-cli/src/repl.rs @@ -1,5 +1,5 @@ -use crate::reedline_config::{add_completion_menu, add_history_menu}; -use crate::{prompt_update, reedline_config}; +use crate::reedline_config::{add_completion_menu, add_help_menu, add_history_menu}; +use crate::{prompt_update, reedline_config, NuHelpCompleter}; use crate::{ reedline_config::KeybindingsMode, util::{eval_source, report_error}, @@ -160,6 +160,9 @@ pub fn evaluate_repl( line_editor = add_completion_menu(line_editor, &config); line_editor = add_history_menu(line_editor, &config); + let help_completer = Box::new(NuHelpCompleter::new(engine_state.clone())); + line_editor = add_help_menu(line_editor, help_completer, &config); + if is_perf_true { info!("setup colors {}:{}:{}", file!(), line!(), column!()); } diff --git a/crates/nu-color-config/Cargo.toml b/crates/nu-color-config/Cargo.toml index 7f395db96..e10825eac 100644 --- a/crates/nu-color-config/Cargo.toml +++ b/crates/nu-color-config/Cargo.toml @@ -8,7 +8,7 @@ version = "0.60.1" [dependencies] nu-protocol = { path = "../nu-protocol", version = "0.60.1" } -nu-ansi-term = "0.45.0" +nu-ansi-term = "0.45.1" nu-json = { path = "../nu-json", version = "0.60.1" } nu-table = { path = "../nu-table", version = "0.60.1" } serde = { version="1.0.123", features=["derive"] } diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index b8704fb42..a7c579d12 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -23,7 +23,7 @@ nu-table = { path = "../nu-table", version = "0.60.1" } nu-term-grid = { path = "../nu-term-grid", version = "0.60.1" } nu-test-support = { path = "../nu-test-support", version = "0.60.1" } nu-utils = { path = "../nu-utils", version = "0.60.1" } -nu-ansi-term = "0.45.0" +nu-ansi-term = "0.45.1" # Potential dependencies for extras base64 = "0.13.0" @@ -77,7 +77,8 @@ unicode-segmentation = "1.8.0" url = "2.2.1" uuid = { version = "0.8.2", features = ["v4"] } which = { version = "4.2.2", optional = true } -reedline = { git = "https://github.com/nushell/reedline" } +#reedline = "0.3.0" +reedline = { git = "https://github.com/nushell/reedline", branch = "main" } zip = { version="0.5.9", optional = true } [target.'cfg(unix)'.dependencies] diff --git a/crates/nu-command/src/filters/update_cells.rs b/crates/nu-command/src/filters/update_cells.rs index 7d29842ad..bf7ebda4e 100644 --- a/crates/nu-command/src/filters/update_cells.rs +++ b/crates/nu-command/src/filters/update_cells.rs @@ -43,7 +43,7 @@ impl Command for UpdateCells { example: r#"[ ["2021-04-16", "2021-06-10", "2021-09-18", "2021-10-15", "2021-11-16", "2021-11-17", "2021-11-18"]; [ 37, 0, 0, 0, 37, 0, 0] -] | update cells {|value| +] | update cells { |value| if $value == 0 { "" } else { @@ -80,7 +80,7 @@ impl Command for UpdateCells { example: r#"[ ["2021-04-16", "2021-06-10", "2021-09-18", "2021-10-15", "2021-11-16", "2021-11-17", "2021-11-18"]; [ 37, 0, 0, 0, 37, 0, 0] -] | update cells -c ["2021-11-18", "2021-11-17"] {|value| +] | update cells -c ["2021-11-18", "2021-11-17"] { |value| if $value == 0 { "" } else { diff --git a/crates/nu-protocol/src/config.rs b/crates/nu-protocol/src/config.rs index 140e74618..b74e98d63 100644 --- a/crates/nu-protocol/src/config.rs +++ b/crates/nu-protocol/src/config.rs @@ -33,6 +33,7 @@ pub struct Config { pub menu_config: HashMap, pub keybindings: Vec, pub history_config: HashMap, + pub help_config: HashMap, pub rm_always_trash: bool, } @@ -55,8 +56,9 @@ impl Default for Config { max_history_size: 1000, log_level: String::new(), menu_config: HashMap::new(), - keybindings: Vec::new(), history_config: HashMap::new(), + help_config: HashMap::new(), + keybindings: Vec::new(), rm_always_trash: false, } } @@ -211,13 +213,6 @@ impl Value { eprintln!("$config.menu_config is not a record") } } - "keybindings" => { - if let Ok(keybindings) = create_keybindings(value, &config) { - config.keybindings = keybindings; - } else { - eprintln!("$config.keybindings is not a valid keybindings list") - } - } "history_config" => { if let Ok(map) = create_map(value, &config) { config.history_config = map; @@ -225,6 +220,20 @@ impl Value { eprintln!("$config.history_config is not a record") } } + "help_config" => { + if let Ok(map) = create_map(value, &config) { + config.help_config = map; + } else { + eprintln!("$config.help_config is not a record") + } + } + "keybindings" => { + if let Ok(keybindings) = create_keybindings(value, &config) { + config.keybindings = keybindings; + } else { + eprintln!("$config.keybindings is not a valid keybindings list") + } + } x => { eprintln!("$config.{} is an unknown config setting", x) } diff --git a/crates/nu-table/Cargo.toml b/crates/nu-table/Cargo.toml index 7f334ec76..979f1397b 100644 --- a/crates/nu-table/Cargo.toml +++ b/crates/nu-table/Cargo.toml @@ -12,7 +12,7 @@ name = "table" path = "src/main.rs" [dependencies] -nu-ansi-term = "0.45.0" +nu-ansi-term = "0.45.1" nu-protocol = { path = "../nu-protocol", version = "0.60.1" } regex = "1.4" unicode-width = "0.1.8" diff --git a/docs/sample_config/default_config.nu b/docs/sample_config/default_config.nu index c83010885..e96099c4a 100644 --- a/docs/sample_config/default_config.nu +++ b/docs/sample_config/default_config.nu @@ -212,6 +212,17 @@ let $config = { selected_text_style: green_reverse marker: "? " } + help_config: { + columns: 4 + col_width: 20 # Optional value. If missing all the screen width is used to calculate column width + col_padding: 2 + selection_rows: 4 + description_rows: 10 + text_style: green + selected_text_style: green_reverse + description_text_style: yellow + marker: "% " + } keybindings: [ { name: completion_menu