diff --git a/Cargo.lock b/Cargo.lock index ca43bf1..e5f3e13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -54,6 +54,12 @@ version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + [[package]] name = "assert_cmd" version = "2.0.8" @@ -85,6 +91,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "beef" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" + [[package]] name = "bitflags" version = "1.3.2" @@ -153,7 +165,7 @@ dependencies = [ "num-integer", "num-traits", "serde", - "time", + "time 0.1.45", "wasm-bindgen", "winapi", ] @@ -191,9 +203,13 @@ version = "3.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" dependencies = [ + "atty", "bitflags", "clap_lex", "indexmap", + "once_cell", + "strsim", + "termcolor", "textwrap", ] @@ -258,6 +274,20 @@ dependencies = [ "itertools", ] +[[package]] +name = "crossbeam" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2801af0d36612ae591caa9568261fddce32ce6e08a7275ea334a06a4ad021a2c" +dependencies = [ + "cfg-if", + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + [[package]] name = "crossbeam-channel" version = "0.5.6" @@ -288,10 +318,20 @@ dependencies = [ "autocfg", "cfg-if", "crossbeam-utils", - "memoffset", + "memoffset 0.7.1", "scopeguard", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.14" @@ -370,6 +410,82 @@ dependencies = [ "syn", ] +[[package]] +name = "darling" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0dd3cd20dc6b5a876612a6e5accfe7f3dd883db6d07acfbf14c128f61550dfa" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a784d2ccaf7c98501746bf0be29b2022ba41fd62a2e622af997a03e9f972859f" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7618812407e9402654622dd402b0a89dff9ba93badd6540781526117b92aab7e" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "defer-drop" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f613ec9fa66a6b28cdb1842b27f9adf24f39f9afc4dcdd9fdecee4aca7945c57" +dependencies = [ + "crossbeam-channel", + "once_cell", +] + +[[package]] +name = "derive_builder" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07adf7be193b71cc36b193d0f5fe60b918a3a9db4dad0449f57bcfd519704a3" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f91d4cfa921f1c05904dc3c57b4a32c38aed3340cce209f3a6fd1478babafc4" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f0314b72bed045f3a68671b3c86328386762c93f82d98c65c3cb5e5f573dd68" +dependencies = [ + "derive_builder_core", + "syn", +] + [[package]] name = "difflib" version = "0.4.0" @@ -385,6 +501,16 @@ dependencies = [ "dirs-sys", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + [[package]] name = "dirs-sys" version = "0.3.7" @@ -396,6 +522,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "doc-comment" version = "0.3.3" @@ -408,6 +545,19 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +[[package]] +name = "env_logger" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + [[package]] name = "erased-serde" version = "0.3.24" @@ -417,6 +567,12 @@ dependencies = [ "serde", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "fuzzy-matcher" version = "0.3.7" @@ -489,6 +645,12 @@ dependencies = [ "libm", ] +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "iana-time-zone" version = "0.1.53" @@ -513,6 +675,12 @@ dependencies = [ "cxx-build", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "indexmap" version = "1.9.2" @@ -627,6 +795,15 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + [[package]] name = "memoffset" version = "0.7.1" @@ -694,6 +871,31 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c" +[[package]] +name = "nix" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags", + "cfg-if", + "libc", + "memoffset 0.6.5", + "pin-utils", +] + [[package]] name = "nom" version = "7.1.2" @@ -808,6 +1010,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkg-config" version = "0.3.26" @@ -958,6 +1166,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustversion" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70" + [[package]] name = "ryu" version = "1.0.12" @@ -1029,6 +1243,12 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" + [[package]] name = "signal-hook" version = "0.3.14" @@ -1059,6 +1279,35 @@ dependencies = [ "libc", ] +[[package]] +name = "skim" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cebed5f897cd6c0d80fbe30adb36c0abf7400e93043a63ae56458495642b3485" +dependencies = [ + "atty", + "beef", + "bitflags", + "chrono", + "clap", + "crossbeam", + "defer-drop", + "derive_builder", + "env_logger", + "fuzzy-matcher", + "lazy_static", + "log", + "nix 0.25.1", + "rayon", + "regex", + "shlex", + "time 0.3.17", + "timer", + "tuikit", + "unicode-width", + "vte", +] + [[package]] name = "smallvec" version = "1.10.0" @@ -1081,6 +1330,12 @@ dependencies = [ "unicode_categories", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "syn" version = "1.0.107" @@ -1092,6 +1347,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + [[package]] name = "termcolor" version = "1.1.3" @@ -1158,6 +1424,31 @@ dependencies = [ "winapi", ] +[[package]] +name = "time" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" +dependencies = [ + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + +[[package]] +name = "timer" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31d42176308937165701f50638db1c31586f183f1aab416268216577aec7306b" +dependencies = [ + "chrono", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -1193,6 +1484,20 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "tuikit" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e19c6ab038babee3d50c8c12ff8b910bdb2196f62278776422f50390d8e53d8" +dependencies = [ + "bitflags", + "lazy_static", + "log", + "nix 0.24.3", + "term", + "unicode-width", +] + [[package]] name = "unicase" version = "2.6.0" @@ -1242,12 +1547,39 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc7ed8ba44ca06be78ea1ad2c3682a43349126c8818054231ee6f4748012aed2" +[[package]] +name = "utf8parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" + [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "vte" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aae21c12ad2ec2d168c236f369c38ff332bc1134f7246350dca641437365045" +dependencies = [ + "arrayvec", + "utf8parse", + "vte_generate_state_changes", +] + +[[package]] +name = "vte_generate_state_changes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "wait-timeout" version = "0.2.0" @@ -1469,7 +1801,6 @@ dependencies = [ "criterion", "crossterm", "dirs", - "fuzzy-matcher", "gethostname", "humansize", "indexmap", @@ -1485,6 +1816,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "skim", "snailquote", "textwrap", "tui", diff --git a/Cargo.toml b/Cargo.toml index 3e86358..0349304 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,13 +32,13 @@ dirs = "4.0.0" ansi-to-tui = "2.0.0" regex = "1.7.1" gethostname = "0.4.1" -fuzzy-matcher = "0.3.7" serde_json = "1.0.91" path-absolutize = "3.0.14" which = "4.3.0" nu-ansi-term = "0.46.0" textwrap = "0.16" snailquote = "0.3.1" +skim = "0.10.2" [dependencies.lscolors] version = "0.13.0" diff --git a/docs/en/src/SUMMARY.md b/docs/en/src/SUMMARY.md index 0677e98..b3a8abc 100644 --- a/docs/en/src/SUMMARY.md +++ b/docs/en/src/SUMMARY.md @@ -22,6 +22,7 @@ - [Input Operation][39] - [Borders][31] - [Style][11] + - [Searching][41] - [Sorting][12] - [Filtering][13] - [Column Renderer][26] @@ -79,3 +80,4 @@ [38]: messages.md [39]: input-operation.md [40]: xplr.util.md +[41]: searching.md diff --git a/docs/en/src/default-key-bindings.md b/docs/en/src/default-key-bindings.md index dca8af9..33a5444 100644 --- a/docs/en/src/default-key-bindings.md +++ b/docs/en/src/default-key-bindings.md @@ -23,7 +23,7 @@ of [modes][4] and the key mappings for each mode. | G | | go to bottom | | V | ctrl-a | select/unselect all | | ctrl-d | | duplicate as | -| ctrl-i | tab | next visited path | +| ctrl-i | | next visited path | | ctrl-n | | next selection | | ctrl-o | | last visited path | | ctrl-p | | prev selection | @@ -49,41 +49,12 @@ of [modes][4] and the key mappings for each mode. | ~ | | go home | | [0-9] | | input | -### search - -| key | remaps | action | -| ------ | ------ | ---------------- | -| ctrl-n | down | down | -| ctrl-p | up | up | -| enter | | submit | -| esc | | cancel | -| left | | back | -| right | | enter | -| tab | | toggle selection | - -### go_to - -| key | remaps | action | -| --- | ------ | -------------- | -| f | | follow symlink | -| g | | top | -| i | | initial $PWD | -| p | | path | -| x | | open in gui | - -### go_to_path - -| key | remaps | action | -| ----- | ------ | ------------ | -| enter | | submit | -| tab | | try complete | - -### duplicate_as +### debug_error -| key | remaps | action | -| ----- | ------ | ------------ | -| enter | | submit | -| tab | | try complete | +| key | remaps | action | +| ----- | ------ | ------------------- | +| enter | | open logs in editor | +| q | | quit | ### sort @@ -109,33 +80,25 @@ of [modes][4] and the key mappings for each mode. | r | | by relative path | | s | | by size | -### debug_error - -| key | remaps | action | -| ----- | ------ | ------------------- | -| enter | | open logs in editor | -| q | | quit | - -### delete +### relative_path_does_not_match_regex -| key | remaps | action | -| --- | ------ | ------------ | -| D | | force delete | -| d | | delete | +| key | remaps | action | +| ----- | ------ | ------ | +| enter | | submit | -### create +### switch_layout -| key | remaps | action | -| --- | ------ | ---------------- | -| d | | create directory | -| f | | create file | +| key | remaps | action | +| --- | ------ | -------------------- | +| 1 | | default | +| 2 | | no help menu | +| 3 | | no selection panel | +| 4 | | no help or selection | -### create_directory +### recover -| key | remaps | action | -| ----- | ------ | ------------ | -| enter | | submit | -| tab | | try complete | +| key | remaps | action | +| --- | ------ | ------ | ### vroot @@ -148,33 +111,14 @@ of [modes][4] and the key mappings for each mode. | v | | toggle vroot | | ~ | | vroot $HOME | -### relative_path_does_match_regex - -| key | remaps | action | -| ----- | ------ | ------ | -| enter | | submit | - -### quit - -| key | remaps | action | -| ----- | ------ | ----------------------- | -| enter | | just quit | -| f | | quit printing focus | -| p | | quit printing pwd | -| r | | quit printing result | -| s | | quit printing selection | - -### filter +### go_to_path -| key | remaps | action | -| --------- | ------ | ---------------------------------- | -| R | | relative path does not match regex | -| backspace | | remove last filter | -| ctrl-r | | reset filters | -| ctrl-u | | clear filters | -| r | | relative path does match regex | +| key | remaps | action | +| ----- | ------ | ------------ | +| enter | | submit | +| tab | | try complete | -### create_file +### rename | key | remaps | action | | ----- | ------ | ------------ | @@ -193,41 +137,33 @@ of [modes][4] and the key mappings for each mode. | s | | softlink here | | u | | clear selection | -### switch_layout - -| key | remaps | action | -| --- | ------ | -------------------- | -| 1 | | default | -| 2 | | no help menu | -| 3 | | no selection panel | -| 4 | | no help or selection | - -### rename +### create_file | key | remaps | action | | ----- | ------ | ------------ | | enter | | submit | | tab | | try complete | -### relative_path_does_not_match_regex +### delete -| key | remaps | action | -| ----- | ------ | ------ | -| enter | | submit | +| key | remaps | action | +| --- | ------ | ------------ | +| D | | force delete | +| d | | delete | -### number +### create_directory -| key | remaps | action | -| ----- | ------ | -------- | -| down | j | to down | -| enter | | to index | -| k | up | to up | -| [0-9] | | input | +| key | remaps | action | +| ----- | ------ | ------------ | +| enter | | submit | +| tab | | try complete | -### recover +### create -| key | remaps | action | -| --- | ------ | ------ | +| key | remaps | action | +| --- | ------ | ---------------- | +| d | | create directory | +| f | | create file | ### action @@ -242,3 +178,72 @@ of [modes][4] and the key mappings for each mode. | s | | selection operations | | v | | vroot | | [0-9] | | go to index | + +### filter + +| key | remaps | action | +| --------- | ------ | ---------------------------------- | +| R | | relative path does not match regex | +| backspace | | remove last filter | +| ctrl-r | | reset filters | +| ctrl-u | | clear filters | +| r | | relative path does match regex | + +### search + +| key | remaps | action | +| ------ | ------ | ----------------------- | +| ctrl-a | | toggle search algorithm | +| ctrl-f | | fuzzy search | +| ctrl-n | down | down | +| ctrl-p | up | up | +| ctrl-r | | regex search | +| ctrl-s | | sort (no search order) | +| ctrl-z | | toggle ordering | +| enter | | submit | +| esc | | cancel | +| left | | back | +| right | | enter | +| tab | | toggle selection | + +### duplicate_as + +| key | remaps | action | +| ----- | ------ | ------------ | +| enter | | submit | +| tab | | try complete | + +### go_to + +| key | remaps | action | +| --- | ------ | -------------- | +| f | | follow symlink | +| g | | top | +| i | | initial $PWD | +| p | | path | +| x | | open in gui | + +### relative_path_does_match_regex + +| key | remaps | action | +| ----- | ------ | ------ | +| enter | | submit | + +### quit + +| key | remaps | action | +| ----- | ------ | ----------------------- | +| enter | | just quit | +| f | | quit printing focus | +| p | | quit printing pwd | +| r | | quit printing result | +| s | | quit printing selection | + +### number + +| key | remaps | action | +| ----- | ------ | -------- | +| down | j | to down | +| enter | | to index | +| k | up | to up | +| [0-9] | | input | diff --git a/docs/en/src/general-config.md b/docs/en/src/general-config.md index 40aa3c3..ecd73ec 100644 --- a/docs/en/src/general-config.md +++ b/docs/en/src/general-config.md @@ -193,6 +193,18 @@ Style for each item in the selection list. Type: [Style](https://xplr.dev/en/style) +#### xplr.config.general.search.algorithm + +The default search algorithm + +Type: [Search Algorithm](https://xplr.dev/en/searching#algorithm) + +#### xplr.config.general.search.unordered + +The default search ordering + +Type: boolean + #### xplr.config.general.default_ui.prefix The content that is placed before the item name for each row by default. @@ -334,12 +346,24 @@ Type: nullable mapping of the following key-value pairs: - format: nullable string - style: [Style](https://xplr.dev/en/style) -#### xplr.config.general.sort_and_filter_ui.search_identifier +#### xplr.config.general.sort_and_filter_ui.search_identifiers The identifiers used to denote applied search input. Type: { format = nullable string, style = [Style](https://xplr.dev/en/style) } +#### xplr.config.general.sort_and_filter_ui.search_direction_identifiers.ordered.format + +The shape of ordered indicator for search ordering identifiers in Sort & filter panel. + +Type: nullable string + +#### xplr.config.general.sort_and_filter_ui.search_direction_identifiers.unordered.format + +The shape of unordered indicator for search ordering identifiers in Sort & filter panel. + +Type: nullable string + #### xplr.config.general.panel_ui.default.title.format The content for panel title by default. diff --git a/docs/en/src/lua-function-calls.md b/docs/en/src/lua-function-calls.md index 4278d1e..89bc1f8 100644 --- a/docs/en/src/lua-function-calls.md +++ b/docs/en/src/lua-function-calls.md @@ -368,26 +368,9 @@ Type: list of [Node Sorter Applicable][81] ### searcher -Type: nullable [Node Searcher][82] +The searcher to use (if any). -## Node Searcher - -Node Searcher contains the following fields: - -- [pattern][83] -- [recoverable_focus][84] - -### pattern - -The patters used to search. - -Type: string - -### recoverable_focus - -Where to focus when search is cancelled. - -Type: nullable string +Type: nullable [Node Searcher Applicable][82] ## Also Ssee: @@ -457,7 +440,5 @@ Type: nullable string [79]: #searcher [80]: filtering.md#node-filter-applicable [81]: sorting.md#node-sorter-applicable -[82]: #node-searcher -[83]: #pattern -[84]: #recoverable_focus +[82]: searching.md#node-searcher-applicable [85]: xplr.util.md diff --git a/docs/en/src/messages.md b/docs/en/src/messages.md index 43f2ec8..a89c5e1 100644 --- a/docs/en/src/messages.md +++ b/docs/en/src/messages.md @@ -1030,6 +1030,28 @@ Example: ### Search Operations +#### Search + +Search files using the current or default (fuzzy) search algorithm. +You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. +It gets reset automatically when changing directory. + +Type: { Search = "string" } + +Example: + +- Lua: `{ Search = "pattern" }` +- YAML: `Search: pattern` + +#### SearchFromInput + +Calls `Search` with the input taken from the input buffer. + +Example: + +- Lua: `"SearchFromInput"` +- YAML: `SearchFromInput` + #### SearchFuzzy Search files using fuzzy match algorithm. @@ -1048,12 +1070,126 @@ Example: Calls `SearchFuzzy` with the input taken from the input buffer. You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. +It gets reset automatically when changing directory. Example: - Lua: `"SearchFuzzyFromInput"` - YAML: `SearchFuzzyFromInput` +#### SearchFuzzyUnordered + +Like `SearchFuzzy`, but doesn't not perform rank based sorting. +You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. +It gets reset automatically when changing directory. + +Type: { SearchFuzzyUnordered = "string" } + +Example: + +- Lua: `{ SearchFuzzyUnordered = "pattern" }` +- YAML: `SearchFuzzyUnordered: pattern` + +#### SearchFuzzyUnorderedFromInput + +Calls `SearchFuzzyUnordered` with the input taken from the input buffer. +You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. +It gets reset automatically when changing directory. + +Example: + +- Lua: `"SearchFuzzyUnorderedFromInput"` +- YAML: `SearchFuzzyUnorderedFromInput` + +#### SearchRegex + +Search files using regex match algorithm. +It keeps the filters, but overrides the sorters. +You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. +It gets reset automatically when changing directory. + +Type: { SearchRegex = "string" } + +Example: + +- Lua: `{ SearchRegex = "pattern" }` +- YAML: `SearchRegex: pattern` + +#### SearchRegexFromInput + +Calls `SearchRegex` with the input taken from the input buffer. +You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. +It gets reset automatically when changing directory. + +Example: + +- Lua: `"SearchRegexFromInput"` +- YAML: `SearchRegexFromInput` + +#### SearchRegexUnordered + +Like `SearchRegex`, but doesn't not perform rank based sorting. +You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. +It gets reset automatically when changing directory. + +Type: { SearchRegexUnordered = "string" } + +Example: + +- Lua: `{ SearchRegexUnordered = "pattern" }` +- YAML: `SearchRegexUnordered: pattern` + +#### SearchRegexUnorderedFromInput + +Calls `SearchRegexUnordered` with the input taken from the input buffer. +You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. +It gets reset automatically when changing directory. + +Example: + +- Lua: `"SearchRegexUnorderedFromInput"` +- YAML: `SearchRegexUnorderedFromInput` + +#### ToggleSearchAlgorithm + +Toggles between different search algorithms, without changing the input +buffer +You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. + +Example: + +- Lua: `"ToggleSearchAlgorithm"` +- YAML: `ToggleSearchAlgorithm` + +#### EnableSearchOrder + +Enables ranked search without changing the input buffer. +You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. + +Example: + +- Lua: `"EnableOrderedSearch"` +- YAML: `EnableSearchOrder` + +#### DisableSearchOrder + +Disabled ranked search without changing the input buffer. +You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. + +Example: + +- Lua: `"DisableSearchOrder"` +- YAML: `DisableSearchOrder` + +#### ToggleSearchOrder + +Toggles ranked search without changing the input buffer. + +Example: + +- Lua: `"ToggleSearchOrder"` +- YAML: `ToggleSearchOrder` + #### AcceptSearch Accepts the search by keeping the latest focus while in search mode. diff --git a/docs/en/src/searching.md b/docs/en/src/searching.md new file mode 100644 index 0000000..bfb8fd7 --- /dev/null +++ b/docs/en/src/searching.md @@ -0,0 +1,77 @@ +# Searching + +xplr supports searching paths using different algorithm. The search mechanism +generally appears between filters and sorters in the `Sort & filter` panel. + +Example: + +``` +fzy:foo↓ +``` + +This line means that the nodes visible on the table are being filtered using the +[fuzzy matching][1] algorithm on the input `foo`. The arrow means that ranking based +ordering is being applied, i.e. [sorters][2] are being ignored. + +## Node Searcher Applicable + +Node Searcher contains the following fields: + +- [pattern][3] +- [recoverable_focus][4] +- [algorithm][5] +- [unordered][7] + +### pattern + +The patters used to search. + +Type: string + +### recoverable_focus + +Where to focus when search is cancelled. + +Type: nullable string + +### algorithm + +Search algorithm to use. Defaults to the value set in +[xplr.config.general.search.algorithm][8]. + +It can be one of the following: + +- Fuzzy +- Regex + +### unordered + +Whether to skip ordering the search result by algorithm based ranking. Defaults +to the value set in [xplr.config.general.search.unordered][9]. + +Type: boolean + +## Example: + +```lua +local searcher = { + pattern = "pattern to search", + recoverable_focus = "/path/to/focus/on/cancel", + algorithm = "Fuzzy", + unordered = false, +} + +xplr.util.explore({ searcher = searcher }) +``` + +See [xplr.util.explore][6]. + +[1]: https://en.wikipedia.org/wiki/Approximate_string_matching +[2]: sorting.md +[3]: #pattern +[4]: #recoverable_focus +[5]: #algorithm +[6]: xplr.util.md#xplrutilexplore +[7]: #unordered +[8]: general-config.md#xplrconfiggeneralsearchalgorithm +[9]: general-config.md#xplrconfiggeneralsearchunordered diff --git a/src/app.rs b/src/app.rs index 838c243..d575440 100644 --- a/src/app.rs +++ b/src/app.rs @@ -9,7 +9,7 @@ pub use crate::msg::in_::external::Command; pub use crate::msg::in_::external::ExplorerConfig; pub use crate::msg::in_::external::NodeFilter; pub use crate::msg::in_::external::NodeFilterApplicable; -use crate::msg::in_::external::NodeSearcher; +use crate::msg::in_::external::NodeSearcherApplicable; pub use crate::msg::in_::external::NodeSorter; pub use crate::msg::in_::external::NodeSorterApplicable; pub use crate::msg::in_::ExternalMsg; @@ -19,6 +19,7 @@ pub use crate::msg::out::MsgOut; pub use crate::node::Node; pub use crate::node::ResolvedNode; pub use crate::pipe::Pipe; +use crate::search::SearchAlgorithm; use crate::ui::Layout; use anyhow::{bail, Result}; use chrono::{DateTime, Local}; @@ -526,8 +527,32 @@ impl App { ReverseNodeSorters => self.reverse_node_sorters(), ResetNodeSorters => self.reset_node_sorters(), ClearNodeSorters => self.clear_node_sorters(), - SearchFuzzy(p) => self.search_fuzzy(p), - SearchFuzzyFromInput => self.search_fuzzy_from_input(), + Search(p) => self.search(p), + SearchFromInput => self.search_from_input(), + SearchFuzzy(p) => self.search_with(p, SearchAlgorithm::Fuzzy, false), + SearchFuzzyFromInput => { + self.search_from_input_with(SearchAlgorithm::Fuzzy, false) + } + SearchRegex(p) => self.search_with(p, SearchAlgorithm::Regex, false), + SearchRegexFromInput => { + self.search_from_input_with(SearchAlgorithm::Regex, false) + } + SearchFuzzyUnordered(p) => { + self.search_with(p, SearchAlgorithm::Fuzzy, true) + } + SearchFuzzyUnorderedFromInput => { + self.search_from_input_with(SearchAlgorithm::Fuzzy, true) + } + SearchRegexUnordered(p) => { + self.search_with(p, SearchAlgorithm::Regex, true) + } + SearchRegexUnorderedFromInput => { + self.search_from_input_with(SearchAlgorithm::Regex, true) + } + EnableSearchOrder => self.enable_search_order(), + DisableSearchOrder => self.disable_search_order(), + ToggleSearchOrder => self.toggle_search_order(), + ToggleSearchAlgorithm => self.toggle_search_algorithm(), AcceptSearch => self.accept_search(), CancelSearch => self.cancel_search(), EnableMouse => self.enable_mouse(), @@ -1611,7 +1636,34 @@ impl App { Ok(self) } - pub fn search_fuzzy(mut self, pattern: String) -> Result { + pub fn search(self, pattern: String) -> Result { + let (algorithm, unordered) = self + .explorer_config + .searcher + .as_ref() + .map(|s| (s.algorithm, s.unordered)) + .unwrap_or(( + self.config.general.search.algorithm, + self.config.general.search.unordered, + )); + + self.search_with(pattern, algorithm, unordered) + } + + fn search_from_input(self) -> Result { + if let Some(pattern) = self.input.buffer.as_ref().map(Input::to_string) { + self.search(pattern) + } else { + Ok(self) + } + } + + pub fn search_with( + mut self, + pattern: String, + algorithm: SearchAlgorithm, + unordered: bool, + ) -> Result { let rf = self .explorer_config .searcher @@ -1619,18 +1671,54 @@ impl App { .map(|s| s.recoverable_focus.clone()) .unwrap_or_else(|| self.focused_node().map(|n| n.absolute_path.clone())); - self.explorer_config.searcher = Some(NodeSearcher::new(pattern, rf)); + self.explorer_config.searcher = Some(NodeSearcherApplicable::new( + pattern, rf, algorithm, unordered, + )); Ok(self) } - fn search_fuzzy_from_input(self) -> Result { + fn search_from_input_with( + self, + algorithm: SearchAlgorithm, + unordered: bool, + ) -> Result { if let Some(pattern) = self.input.buffer.as_ref().map(Input::to_string) { - self.search_fuzzy(pattern) + self.search_with(pattern, algorithm, unordered) } else { Ok(self) } } + fn enable_search_order(mut self) -> Result { + self.explorer_config.searcher = self + .explorer_config + .searcher + .map(|s| s.enable_search_order()); + Ok(self) + } + + fn disable_search_order(mut self) -> Result { + self.explorer_config.searcher = self + .explorer_config + .searcher + .map(|s| s.disable_search_order()); + Ok(self) + } + + fn toggle_search_order(mut self) -> Result { + self.explorer_config.searcher = self + .explorer_config + .searcher + .map(|s| s.toggle_search_order()); + Ok(self) + } + + fn toggle_search_algorithm(mut self) -> Result { + self.explorer_config.searcher = + self.explorer_config.searcher.map(|s| s.toggle_algorithm()); + Ok(self) + } + fn accept_search(mut self) -> Result { let focus = self .directory_buffer diff --git a/src/config.rs b/src/config.rs index 6e12206..b27d46c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,6 +4,7 @@ use crate::app::NodeFilter; use crate::app::NodeSorter; use crate::app::NodeSorterApplicable; use crate::node::Node; +use crate::search::SearchAlgorithm; use crate::ui::Border; use crate::ui::BorderType; use crate::ui::Constraint; @@ -188,6 +189,16 @@ pub struct SelectionConfig { pub item: UiElement, } +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SearchConfig { + #[serde(default)] + pub algorithm: SearchAlgorithm, + + #[serde(default)] + pub unordered: bool, +} + #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct LogsConfig { @@ -214,6 +225,16 @@ pub struct SortDirectionIdentifiersUi { pub reverse: UiElement, } +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SearchDirectionIdentifiersUi { + #[serde(default)] + pub ordered: UiElement, + + #[serde(default)] + pub unordered: UiElement, +} + #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct SortAndFilterUi { @@ -233,7 +254,10 @@ pub struct SortAndFilterUi { pub filter_identifiers: HashMap, #[serde(default)] - pub search_identifier: Option, + pub search_direction_identifiers: SearchDirectionIdentifiersUi, + + #[serde(default)] + pub search_identifiers: HashMap, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] @@ -294,6 +318,9 @@ pub struct GeneralConfig { #[serde(default)] pub selection: SelectionConfig, + #[serde(default)] + pub search: SearchConfig, + #[serde(default)] pub default_ui: UiConfig, diff --git a/src/explorer.rs b/src/explorer.rs index fb4a996..ac2c8db 100644 --- a/src/explorer.rs +++ b/src/explorer.rs @@ -2,21 +2,14 @@ use crate::app::{ DirectoryBuffer, ExplorerConfig, ExternalMsg, InternalMsg, MsgIn, Node, Task, }; use anyhow::{Error, Result}; -use fuzzy_matcher::skim::SkimMatcherV2; -use fuzzy_matcher::FuzzyMatcher; -use lazy_static::lazy_static; use std::fs; use std::path::PathBuf; use std::sync::mpsc::Sender; use std::thread; -lazy_static! { - static ref FUZZY_MATCHER: SkimMatcherV2 = SkimMatcherV2::default(); -} - pub fn explore(parent: &PathBuf, config: &ExplorerConfig) -> Result> { let dirs = fs::read_dir(parent)?; - let mut nodes = dirs + let nodes = dirs .filter_map(|d| { d.ok().map(|e| { e.path() @@ -26,26 +19,24 @@ pub fn explore(parent: &PathBuf, config: &ExplorerConfig) -> Result> { }) }) .map(|name| Node::new(parent.to_string_lossy().to_string(), name)) - .filter(|n| config.filter(n)) - .collect::>(); - - nodes = if let Some(pattern) = config.searcher.as_ref().map(|s| &s.pattern) { - let mut nodes = nodes - .into_iter() - .filter_map(|n| { - FUZZY_MATCHER - .fuzzy_match(&n.relative_path, pattern) - .map(|score| (n, score)) - }) - .collect::>(); + .filter(|n| config.filter(n)); - nodes.sort_by(|(_, s1), (_, s2)| s2.cmp(s1)); - nodes.into_iter().map(|(n, _)| n).collect::>() + let mut nodes = if let Some(searcher) = config.searcher.as_ref() { + searcher.search(nodes) } else { - nodes.sort_by(|a, b| config.sort(a, b)); - nodes + nodes.collect() }; + let is_ordered_search = config + .searcher + .as_ref() + .map(|s| !s.unordered) + .unwrap_or(false); + + if !is_ordered_search { + nodes.sort_by(|a, b| config.sort(a, b)); + } + Ok(nodes) } diff --git a/src/init.lua b/src/init.lua index 8a25abe..217d0ae 100644 --- a/src/init.lua +++ b/src/init.lua @@ -256,6 +256,16 @@ xplr.config.general.selection.item.format = "builtin.fmt_general_selection_item" -- Type: [Style](https://xplr.dev/en/style) xplr.config.general.selection.item.style = {} +-- The default search algorithm +-- +-- Type: [Search Algorithm](https://xplr.dev/en/searching#algorithm) +xplr.config.general.search.algorithm = "Fuzzy" + +-- The default search ordering +-- +-- Type: boolean +xplr.config.general.search.unordered = false + -- The content that is placed before the item name for each row by default. -- -- Type: nullable string @@ -462,11 +472,22 @@ xplr.config.general.sort_and_filter_ui.filter_identifiers = { -- The identifiers used to denote applied search input. -- -- Type: { format = nullable string, style = [Style](https://xplr.dev/en/style) } -xplr.config.general.sort_and_filter_ui.search_identifier = { - format = "search:", - style = {}, +xplr.config.general.sort_and_filter_ui.search_identifiers = { + Fuzzy = { format = "fzy:", style = {} }, + Regex = { format = "reg:", style = {} }, } +-- The shape of ordered indicator for search ordering identifiers in Sort & filter panel. +-- +-- Type: nullable string +xplr.config.general.sort_and_filter_ui.search_direction_identifiers.ordered.format = +"↓" + +-- The shape of unordered indicator for search ordering identifiers in Sort & filter panel. +-- +-- Type: nullable string +xplr.config.general.sort_and_filter_ui.search_direction_identifiers.unordered.format = "" + -- The content for panel title by default. -- -- Type: nullable string @@ -2116,6 +2137,42 @@ xplr.config.modes.builtin.search = { "FocusNext", }, }, + ["ctrl-z"] = { + help = "toggle ordering", + messages = { + "ToggleSearchOrder", + "ExplorePwdAsync", + }, + }, + ["ctrl-a"] = { + help = "toggle search algorithm", + messages = { + "ToggleSearchAlgorithm", + "ExplorePwdAsync", + }, + }, + ["ctrl-r"] = { + help = "regex search", + messages = { + "SearchRegexFromInput", + "ExplorePwdAsync", + }, + }, + ["ctrl-f"] = { + help = "fuzzy search", + messages = { + "SearchFuzzyFromInput", + "ExplorePwdAsync", + }, + }, + ["ctrl-s"] = { + help = "sort (no search order)", + messages = { + "DisableSearchOrder", + "ExplorePwdAsync", + { SwitchModeBuiltinKeepingInputBuffer = "sort" }, + }, + }, ["right"] = { help = "enter", messages = { @@ -2155,7 +2212,7 @@ xplr.config.modes.builtin.search = { default = { messages = { "UpdateInputBufferFromKey", - "SearchFuzzyFromInput", + "SearchFromInput", "ExplorePwdAsync", }, }, @@ -2365,7 +2422,12 @@ xplr.config.modes.builtin.sort = { ["enter"] = { help = "submit", messages = { - "PopMode", + "PopModeKeepingInputBuffer", + }, + }, + ["esc"] = { + messages = { + "PopModeKeepingInputBuffer", }, }, ["m"] = { diff --git a/src/lib.rs b/src/lib.rs index 91e3871..ce1a5a0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,6 +17,7 @@ pub mod permissions; pub mod pipe; pub mod pwd_watcher; pub mod runner; +pub mod search; pub mod ui; pub mod yaml; diff --git a/src/msg/in_/external.rs b/src/msg/in_/external.rs index 7788fe1..3b128b1 100644 --- a/src/msg/in_/external.rs +++ b/src/msg/in_/external.rs @@ -1,8 +1,11 @@ -use crate::{app::Node, input::InputOperation}; +use crate::app::Node; +use crate::input::InputOperation; +use crate::search::PathItem; +use crate::search::SearchAlgorithm; use indexmap::IndexSet; use regex::Regex; use serde::{Deserialize, Serialize}; -use std::cmp::Ordering; +use std::{cmp::Ordering, sync::Arc}; #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub enum ExternalMsg { @@ -921,6 +924,26 @@ pub enum ExternalMsg { /// ### Search Operations -------------------------------------------------- + /// Search files using the current or default (fuzzy) search algorithm. + /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. + /// It gets reset automatically when changing directory. + /// + /// Type: { Search = "string" } + /// + /// Example: + /// + /// - Lua: `{ Search = "pattern" }` + /// - YAML: `Search: pattern` + Search(String), + + /// Calls `Search` with the input taken from the input buffer. + /// + /// Example: + /// + /// - Lua: `"SearchFromInput"` + /// - YAML: `SearchFromInput` + SearchFromInput, + /// Search files using fuzzy match algorithm. /// It keeps the filters, but overrides the sorters. /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. @@ -936,6 +959,7 @@ pub enum ExternalMsg { /// Calls `SearchFuzzy` with the input taken from the input buffer. /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. + /// It gets reset automatically when changing directory. /// /// Example: /// @@ -943,6 +967,109 @@ pub enum ExternalMsg { /// - YAML: `SearchFuzzyFromInput` SearchFuzzyFromInput, + /// Like `SearchFuzzy`, but doesn't not perform rank based sorting. + /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. + /// It gets reset automatically when changing directory. + /// + /// Type: { SearchFuzzyUnordered = "string" } + /// + /// Example: + /// + /// - Lua: `{ SearchFuzzyUnordered = "pattern" }` + /// - YAML: `SearchFuzzyUnordered: pattern` + SearchFuzzyUnordered(String), + + /// Calls `SearchFuzzyUnordered` with the input taken from the input buffer. + /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. + /// It gets reset automatically when changing directory. + /// + /// Example: + /// + /// - Lua: `"SearchFuzzyUnorderedFromInput"` + /// - YAML: `SearchFuzzyUnorderedFromInput` + SearchFuzzyUnorderedFromInput, + + /// Search files using regex match algorithm. + /// It keeps the filters, but overrides the sorters. + /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. + /// It gets reset automatically when changing directory. + /// + /// Type: { SearchRegex = "string" } + /// + /// Example: + /// + /// - Lua: `{ SearchRegex = "pattern" }` + /// - YAML: `SearchRegex: pattern` + SearchRegex(String), + + /// Calls `SearchRegex` with the input taken from the input buffer. + /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. + /// It gets reset automatically when changing directory. + /// + /// Example: + /// + /// - Lua: `"SearchRegexFromInput"` + /// - YAML: `SearchRegexFromInput` + SearchRegexFromInput, + + /// Like `SearchRegex`, but doesn't not perform rank based sorting. + /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. + /// It gets reset automatically when changing directory. + /// + /// Type: { SearchRegexUnordered = "string" } + /// + /// Example: + /// + /// - Lua: `{ SearchRegexUnordered = "pattern" }` + /// - YAML: `SearchRegexUnordered: pattern` + SearchRegexUnordered(String), + + /// Calls `SearchRegexUnordered` with the input taken from the input buffer. + /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. + /// It gets reset automatically when changing directory. + /// + /// Example: + /// + /// - Lua: `"SearchRegexUnorderedFromInput"` + /// - YAML: `SearchRegexUnorderedFromInput` + SearchRegexUnorderedFromInput, + + /// Toggles between different search algorithms, without changing the input + /// buffer + /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. + /// + /// Example: + /// + /// - Lua: `"ToggleSearchAlgorithm"` + /// - YAML: `ToggleSearchAlgorithm` + ToggleSearchAlgorithm, + + /// Enables ranked search without changing the input buffer. + /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. + /// + /// Example: + /// + /// - Lua: `"EnableOrderedSearch"` + /// - YAML: `EnableSearchOrder` + EnableSearchOrder, + + /// Disabled ranked search without changing the input buffer. + /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. + /// + /// Example: + /// + /// - Lua: `"DisableSearchOrder"` + /// - YAML: `DisableSearchOrder` + DisableSearchOrder, + + /// Toggles ranked search without changing the input buffer. + /// + /// Example: + /// + /// - Lua: `"ToggleSearchOrder"` + /// - YAML: `ToggleSearchOrder` + ToggleSearchOrder, + /// Accepts the search by keeping the latest focus while in search mode. /// Automatically calls `ExplorePwd`. /// @@ -1651,18 +1778,78 @@ impl NodeFilterApplicable { } #[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)] -pub struct NodeSearcher { +pub struct NodeSearcherApplicable { pub pattern: String, #[serde(default)] pub recoverable_focus: Option, + + #[serde(default)] + pub algorithm: SearchAlgorithm, + + #[serde(default)] + pub unordered: bool, } -impl NodeSearcher { - pub fn new(pattern: String, recoverable_focus: Option) -> Self { +impl NodeSearcherApplicable { + pub fn new( + pattern: String, + recoverable_focus: Option, + algorithm: SearchAlgorithm, + unordered: bool, + ) -> Self { Self { pattern, recoverable_focus, + algorithm, + unordered, + } + } + + pub fn search(&self, nodes: I) -> Vec + where + I: IntoIterator, + { + let engine = self.algorithm.engine(&self.pattern); + let ranked_nodes = nodes.into_iter().filter_map(|n| { + let item = Arc::new(PathItem::from(n.relative_path.clone())); + engine.match_item(item).map(|res| (n, res.rank)) + }); + + if self.unordered { + ranked_nodes.map(|(n, _)| n).collect() + } else { + let mut ranked_nodes = ranked_nodes.collect::>(); + ranked_nodes.sort_by(|(_, s1), (_, s2)| s2.cmp(s1)); + ranked_nodes.into_iter().map(|(n, _)| n).collect() + } + } + + pub fn enable_search_order(self) -> Self { + Self { + unordered: false, + ..self + } + } + + pub fn disable_search_order(self) -> Self { + Self { + unordered: true, + ..self + } + } + + pub fn toggle_search_order(self) -> Self { + Self { + unordered: !self.unordered, + ..self + } + } + + pub fn toggle_algorithm(self) -> Self { + Self { + algorithm: self.algorithm.toggle(), + ..self } } } @@ -1676,7 +1863,7 @@ pub struct ExplorerConfig { pub sorters: IndexSet, #[serde(default)] - pub searcher: Option, + pub searcher: Option, } impl ExplorerConfig { diff --git a/src/search.rs b/src/search.rs new file mode 100644 index 0000000..14b6adb --- /dev/null +++ b/src/search.rs @@ -0,0 +1,50 @@ +use lazy_static::lazy_static; +use serde::{Deserialize, Serialize}; +use skim::prelude::{ExactOrFuzzyEngineFactory, RegexEngineFactory}; +use skim::{MatchEngine, MatchEngineFactory, SkimItem}; + +lazy_static! { + static ref FUZZY_FACTORY: ExactOrFuzzyEngineFactory = + ExactOrFuzzyEngineFactory::builder().build(); + static ref REGEX_FACTORY: RegexEngineFactory = RegexEngineFactory::builder().build(); +} + +pub struct PathItem { + path: String, +} + +impl From for PathItem { + fn from(value: String) -> Self { + Self { path: value } + } +} + +impl SkimItem for PathItem { + fn text(&self) -> std::borrow::Cow { + std::borrow::Cow::from(&self.path) + } +} + +#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub enum SearchAlgorithm { + #[default] + Fuzzy, + Regex, +} + +impl SearchAlgorithm { + pub fn toggle(self) -> Self { + match self { + Self::Fuzzy => Self::Regex, + Self::Regex => Self::Fuzzy, + } + } + + pub fn engine(&self, pattern: &str) -> Box { + match self { + Self::Fuzzy => FUZZY_FACTORY.create_engine(pattern), + Self::Regex => REGEX_FACTORY.create_engine(pattern), + } + } +} diff --git a/src/ui.rs b/src/ui.rs index a9ebe12..92de37b 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1017,11 +1017,7 @@ fn draw_sort_n_filter( let ui = app.config.general.sort_and_filter_ui.to_owned(); let filter_by: &IndexSet = &app.explorer_config.filters; let sort_by: &IndexSet = &app.explorer_config.sorters; - let search = app - .explorer_config - .searcher - .as_ref() - .map(|s| s.pattern.clone()); + let search = app.explorer_config.searcher.as_ref(); let defaultui = &ui.default_identifier; let forwardui = defaultui @@ -1031,6 +1027,15 @@ fn draw_sort_n_filter( .to_owned() .extend(&ui.sort_direction_identifiers.reverse); + let orderedui = defaultui + .to_owned() + .extend(&ui.search_direction_identifiers.ordered); + let unorderedui = defaultui + .to_owned() + .extend(&ui.search_direction_identifiers.unordered); + + let is_ordered_search = search.as_ref().map(|s| !s.unordered).unwrap_or(false); + let mut spans = filter_by .iter() .map(|f| { @@ -1048,12 +1053,36 @@ fn draw_sort_n_filter( }) .unwrap_or((Span::raw("f"), Span::raw(""))) }) + .chain(search.iter().map(|s| { + ui.search_identifiers + .get(&s.algorithm) + .map(|u| { + let direction = if s.unordered { + &unorderedui + } else { + &orderedui + }; + let ui = defaultui.to_owned().extend(u); + let f = ui + .format + .as_ref() + .map(|f| format!("{f}{p}", p = &s.pattern)) + .unwrap_or_else(|| s.pattern.clone()); + ( + Span::styled(f, ui.style.into()), + Span::styled( + direction.format.to_owned().unwrap_or_default(), + direction.style.to_owned().into(), + ), + ) + }) + .unwrap_or((Span::raw("/"), Span::raw(&s.pattern))) + })) .chain( sort_by .iter() .map(|s| { let direction = if s.reverse { &reverseui } else { &forwardui }; - ui.sorter_identifiers .get(&s.sorter) .map(|u| { @@ -1071,23 +1100,8 @@ fn draw_sort_n_filter( }) .unwrap_or((Span::raw("s"), Span::raw(""))) }) - .take(if search.is_some() { 0 } else { sort_by.len() }), + .take(if !is_ordered_search { sort_by.len() } else { 0 }), ) - .chain(search.iter().map(|s| { - ui.search_identifier - .as_ref() - .map(|u| { - let ui = defaultui.to_owned().extend(u); - ( - Span::styled( - ui.format.to_owned().unwrap_or_default(), - ui.style.to_owned().into(), - ), - Span::styled(s, ui.style.into()), - ) - }) - .unwrap_or((Span::raw("/"), Span::raw(s))) - })) .zip(std::iter::repeat(Span::styled( ui.separator.format.to_owned().unwrap_or_default(), ui.separator.style.to_owned().into(),