Repository: ynqa/jnv (language: rust)

Issue:
Painfully slow on 16MB (minified) JSON
I love the idea of this tool, and the demo looks really cool too!

Installation worked flawlessly with `cargo install jnv`, but when I fired it up on a real JSON I had looked at with `jq` earlier, I noticed it was painfully slow, unusably slow, response times were on the order of 5-10 seconds after each key stroke.

Of course this is v0.1.0 - but just wanted to give feedback nonetheless from my real world test drive.

I'm on an M1 Pro with 32GB RAM which is fairly high end.

I tried reducing the display depth but that didn't help.


Discussion / hints:
@corneliusroemer Thanks for your reporting! 👍 

Could you share the actual JSON, if possible? I'd like to test it myself and understand the current data size limits.
Sure, here we go: [test.json](https://github.com/ynqa/jnv/files/14657897/test.json)

I've meanwhile found [`fx`](https://fx.wtf/) to be very good for such large JSON files, don't even notice any slowdown at all.
@corneliusroemer [fx](https://github.com/antonmedv/fx) and [jless](https://github.com/PaulJuliusMartinez/jless) are indeed famous for their ability to display content very quickly.

On the other hand, `jnv` relies on the processing speed of libjq, so it can take time to handle very large JSON files. For example, you can see how long it takes by running `cat test.json | jq`.

I believe there can be a division of use cases: utilize [fx](https://github.com/antonmedv/fx) or [jless](https://github.com/PaulJuliusMartinez/jless) when you want to get an overview of the large JSON, and use `jnv` when you want to try out complex queries with jq for the mid-size JSON.

[fx](https://github.com/antonmedv/fx) and [jless](https://github.com/PaulJuliusMartinez/jless) do allow for field lookup with .path.to.field and have some simple DSL query capabilities, but I understand that they cannot handle complex queries like jq can.

- https://fx.wtf/advanced-usage
- https://github.com/PaulJuliusMartinez/jless/issues/70
Thanks for the detailed reply. I suppose some `async/await` could help with not freezing the TUI - but the added complexity may well not be worth it. The tool would still be usable with the 16MB file if it simply didn't show output until say 3s after the last keystroke.

The biggest usability problem for large files is that keystrokes are delayed.
> I suppose some async/await could help with not freezing the TUI
> The biggest usability problem for large files is that keystrokes are delayed

@corneliusroemer You're absolutely right; there's a potential to degrade user experience. 

`jnv` utilizes the mechanisms from [promkit](https://github.com/ynqa/promkit), where I'm considering implementing a system that can handle keystrokes and data structure editing concurrently using async/await at *promkit* (I've tried this before, but it's been a bit challenging and is currently pending).




Hey, I wrote a similar project a while ago: [Jex](https://github.com/rocurley/jex). While the UI looks a lot better here, I didn't have this problem. I believe the difference is that I parsed the JSON once at load and then used the parsed structure from there on out.

Looking at your code, you already parse the output to json so you're in a pretty good spot there. Even if you don't think it's worth it to switch your rendering code to use JQ's internal representation, I bet you could convert from JQ's representation to yours a lot faster if you didn't go through a string representation between the two.
I would recommend Jaq, it's native rust and you can won't have to convert each time, and was built to fix some speed cases with many filters compared to jq.

https://docs.rs/jaq-interpret/latest/jaq_interpret/
@rockwotj Regarding the replacement with jaq, I have created https://github.com/ynqa/jnv/issues/24, so please provide your opinions/reactions there 🙏
did the switch to `jaq` change the performance on this case?
> did the switch to jaq change the performance on this case?

@extrawurst Switching to jaq didn’t provide significant performance benefits.

On the other hand, I’ve noticed that the current JSON transformation process is very slow: from serde_json to promkit (which can be understood as serde_json::Value with added information for collapsed/expanded states).

Currently, I am reviewing the code of fast JSON viewers like fx and jless, particularly the following sections:

- fx: https://github.com/antonmedv/fx/tree/master/internal/jsonx
- jless: https://github.com/PaulJuliusMartinez/jless/blob/main/src/jsonparser.rs

If you know of any other resources or structures for JSON viewing that might be helpful, I’d appreciate it if you could share them.

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 2dee4ef..41fac3a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2,6 +2,15 @@
 # It is not intended for manual editing.
 version = 3
 
+[[package]]
+name = "addr2line"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678"
+dependencies = [
+ "gimli",
+]
+
 [[package]]
 name = "adler"
 version = "1.0.2"
@@ -52,9 +61,9 @@ dependencies = [
 
 [[package]]
 name = "anstyle"
-version = "1.0.6"
+version = "1.0.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
+checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
 
 [[package]]
 name = "anstyle-parse"
@@ -86,15 +95,15 @@ dependencies = [
 
 [[package]]
 name = "anyhow"
-version = "1.0.82"
+version = "1.0.95"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519"
+checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
 
 [[package]]
 name = "arboard"
-version = "3.4.0"
+version = "3.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9fb4009533e8ff8f1450a5bcbc30f4242a1d34442221f72314bea1f5dc9c7f89"
+checksum = "df099ccb16cd014ff054ac1bf392c67feeef57164b05c42f037cd40f5d4357f4"
 dependencies = [
  "clipboard-win",
  "core-graphics",
@@ -108,12 +117,38 @@ dependencies = [
  "x11rb",
 ]
 
+[[package]]
+name = "async-trait"
+version = "0.1.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "autocfg"
 version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
 
+[[package]]
+name = "backtrace"
+version = "0.3.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a"
+dependencies = [
+ "addr2line",
+ "cc",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+]
+
 [[package]]
 name = "base64"
 version = "0.21.7"
@@ -162,6 +197,21 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
 
+[[package]]
+name = "bytes"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
+
+[[package]]
+name = "cc"
+version = "1.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e"
+dependencies = [
+ "shlex",
+]
+
 [[package]]
 name = "cfg-if"
 version = "1.0.0"
@@ -179,9 +229,9 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "4.5.4"
+version = "4.5.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
+checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84"
 dependencies = [
  "clap_builder",
  "clap_derive",
@@ -189,9 +239,9 @@ dependencies = [
 
 [[package]]
 name = "clap_builder"
-version = "4.5.2"
+version = "4.5.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
+checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838"
 dependencies = [
  "anstream",
  "anstyle",
@@ -201,9 +251,9 @@ dependencies = [
 
 [[package]]
 name = "clap_derive"
-version = "4.5.4"
+version = "4.5.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64"
+checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
 dependencies = [
  "heck",
  "proc-macro2",
@@ -213,9 +263,9 @@ dependencies = [
 
 [[package]]
 name = "clap_lex"
-version = "0.7.0"
+version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
+checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
 
 [[package]]
 name = "clipboard-win"
@@ -315,6 +365,8 @@ dependencies = [
  "bitflags 2.5.0",
  "crossterm_winapi",
  "filedescriptor",
+ "futures-core",
+ "libc",
  "mio",
  "parking_lot",
  "rustix",
@@ -438,6 +490,101 @@ version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
 
+[[package]]
+name = "futures"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
+
+[[package]]
+name = "futures-task"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
+
+[[package]]
+name = "futures-timer"
+version = "3.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
+
+[[package]]
+name = "futures-util"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
 [[package]]
 name = "gethostname"
 version = "0.4.3"
@@ -459,6 +606,12 @@ dependencies = [
  "wasi",
 ]
 
+[[package]]
+name = "gimli"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
+
 [[package]]
 name = "hashbrown"
 version = "0.14.3"
@@ -580,17 +733,22 @@ dependencies = [
 
 [[package]]
 name = "jnv"
-version = "0.4.2"
+version = "0.5.0"
 dependencies = [
  "anyhow",
  "arboard",
+ "async-trait",
  "clap",
+ "crossterm",
+ "futures",
+ "futures-timer",
  "jaq-core",
  "jaq-interpret",
  "jaq-parse",
  "jaq-std",
  "promkit",
- "radix_trie",
+ "tokio",
+ "tokio-stream",
 ]
 
 [[package]]
@@ -785,6 +943,15 @@ dependencies = [
  "objc2-metal",
 ]
 
+[[package]]
+name = "object"
+version = "0.36.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "once_cell"
 version = "1.19.0"
@@ -814,6 +981,18 @@ dependencies = [
  "windows-targets 0.48.5",
 ]
 
+[[package]]
+name = "pin-project-lite"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
 [[package]]
 name = "png"
 version = "0.17.13"
@@ -844,9 +1023,9 @@ dependencies = [
 
 [[package]]
 name = "promkit"
-version = "0.6.0"
+version = "0.6.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "60f08645b9c51e58efade657a071855d6dc3e39e7a67642043e1548a453319ce"
+checksum = "a8e3e630da08ae2df2829e2ec38a1cc49ea12c3c3ca8143e58d37ab64a6e613e"
 dependencies = [
  "anyhow",
  "crossterm",
@@ -935,6 +1114,12 @@ version = "0.8.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
 
+[[package]]
+name = "rustc-demangle"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
+
 [[package]]
 name = "rustix"
 version = "0.38.34"
@@ -992,6 +1177,12 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
 [[package]]
 name = "signal-hook"
 version = "0.3.17"
@@ -1028,12 +1219,31 @@ version = "0.3.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
 
+[[package]]
+name = "slab"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
+dependencies = [
+ "autocfg",
+]
+
 [[package]]
 name = "smallvec"
 version = "1.13.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
 
+[[package]]
+name = "socket2"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
 [[package]]
 name = "strsim"
 version = "0.11.1"
@@ -1113,6 +1323,46 @@ dependencies = [
  "time-core",
 ]
 
+[[package]]
+name = "tokio"
+version = "1.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "libc",
+ "mio",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+]
+
 [[package]]
 name = "unicode-ident"
 version = "1.0.12"
diff --git a/Cargo.toml b/Cargo.toml
index 9c5d56b..23c6210 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "jnv"
-version = "0.4.2"
+version = "0.5.0"
 authors = ["ynqa <un.pensiero.vano@gmail.com>"]
 edition = "2021"
 description = "JSON navigator and interactive filter leveraging jq"
@@ -9,15 +9,21 @@ license = "MIT"
 readme = "README.md"
 
 [dependencies]
-anyhow = "1.0.82"
-clap = { version = "4.5.4", features = ["derive"] }
-arboard = "3.4.0"
+anyhow = "1.0.95"
+arboard = "3.4.1"
+async-trait = "0.1.83"
+clap = { version = "4.5.23", features = ["derive"] }
+# See https://github.com/crossterm-rs/crossterm/issues/935
+crossterm = { version = "0.28.1", features = ["use-dev-tty", "event-stream", "libc"] }
+futures = "0.3.30"
+futures-timer = "3.0.3"
 jaq-core = "1.2.1"
 jaq-interpret = "1.2.1"
 jaq-parse = "1.0.2"
 jaq-std = "1.2.1"
-promkit = "0.6.0"
-radix_trie = "0.2.1"
+promkit = "0.6.2"
+tokio = { version = "1.42.0", features = ["full"] }
+tokio-stream = "0.1.16"
 
 # The profile that 'cargo dist' will build with
 [profile.dist]
diff --git a/README.md b/README.md
index 93cb940..8867199 100644
--- a/README.md
+++ b/README.md
@@ -123,34 +123,53 @@ jnv data.json
 
 ## Keymap
 
-| Key                  | Action
-| :-                   | :-
-| <kbd>Ctrl + C</kbd>  | Exit `jnv`
-| <kbd>Tab</kbd>       | jq filter auto-completion
-| <kbd>←</kbd>         | Move the cursor one character to the left
-| <kbd>→</kbd>         | Move the cursor one character to the right
-| <kbd>Ctrl + A</kbd>  | Move the cursor to the start of the filter
-| <kbd>Ctrl + E</kbd>  | Move the cursor to the end of the filter
-| <kbd>Backspace</kbd> | Delete a character of filter at the cursor position
-| <kbd>Ctrl + U</kbd>  | Delete all characters of filter
-| <kbd>↑</kbd>, <kbd>Ctrl + K</kbd> | Move the cursor one entry up in JSON viewer
-| <kbd>↓</kbd>, <kbd>Ctrl + J</kbd> | Move the cursor one entry down in JSON viewer
-| <kbd>Ctrl + H</kbd>  | Move to the last entry in JSON viewer
-| <kbd>Ctrl + L</kbd>  | Move to the first entry in JSON viewer
-| <kbd>Enter</kbd>     | Toggle expand/collapse in JSON viewer
-| <kbd>Ctrl + P</kbd>  | Expand all folds in JSON viewer
-| <kbd>Ctrl + N</kbd>  | Collapse all folds in JSON viewer
-| <kbd>Alt + B</kbd>   | Move the cursor to the previous nearest character within set(`.`,`\|`,`(`,`)`,`[`,`]`)
-| <kbd>Alt + F</kbd>   | Move the cursor to the next nearest character within set(`.`,`\|`,`(`,`)`,`[`,`]`)
-| <kbd>Ctrl + W</kbd>  | Erase to the previous nearest character within set(`.`,`\|`,`(`,`)`,`[`,`]`)
-| <kbd>Alt + D</kbd>   | Erase to the next nearest character within set(`.`,`\|`,`(`,`)`,`[`,`]`)
-| <kbd>Ctrl + O</kbd>  | Copy current JSON content to clipboard
-| <kbd>Ctrl + Q</kbd>  | Copy current query to clipboard
+| Key | Action |
+| :- | :- |
+| <kbd>Ctrl + C</kbd> | Exit |
+| <kbd>Ctrl + Q</kbd> | Copy jq filter to clipboard |
+| <kbd>Ctrl + O</kbd> | Copy JSON to clipboard |
+| <kbd>Shift + ↑</kbd>, <kbd>Shift + ↓</kbd> | Switch to another mode |
+
+### Editor mode (default)
+
+| Key | Action |
+| :- | :- |
+| <kbd>Tab</kbd> | Enter suggestion |
+| <kbd>←</kbd> | Move cursor left |
+| <kbd>→</kbd> | Move cursor right |
+| <kbd>Ctrl + A</kbd> | Move cursor to line start |
+| <kbd>Ctrl + E</kbd> | Move cursor to line end |
+| <kbd>Backspace</kbd> | Delete character before cursor |
+| <kbd>Ctrl + U</kbd> | Clear entire line |
+| <kbd>Alt + B</kbd>   | Move the cursor to the previous nearest character within set(`.`,`\|`,`(`,`)`,`[`,`]`) |
+| <kbd>Alt + F</kbd>   | Move the cursor to the next nearest character within set(`.`,`\|`,`(`,`)`,`[`,`]`) |
+| <kbd>Ctrl + W</kbd>  | Erase to the previous nearest character within set(`.`,`\|`,`(`,`)`,`[`,`]`) |
+| <kbd>Alt + D</kbd>   | Erase to the next nearest character within set(`.`,`\|`,`(`,`)`,`[`,`]`) |
+
+#### Suggestion in Editor (after <kbd>Tab</kbd>)
+
+| Key | Action |
+| :- | :- |
+| <kbd>Tab</kbd>, <kbd>↓</kbd> | Select next suggestion |
+| <kbd>↑</kbd> | Select previous suggestion |
+| Others | Return to editor |
+
+### JSON viewer mode
+
+| Key | Action |
+| :- | :- |
+| <kbd>↑</kbd>, <kbd>Ctrl + K</kbd> | Move up |
+| <kbd>↓</kbd>, <kbd>Ctrl + J</kbd> | Move down |
+| <kbd>Ctrl + H</kbd> | Move to last entry |
+| <kbd>Ctrl + L</kbd> | Move to first entry |
+| <kbd>Enter</kbd> | Toggle fold |
+| <kbd>Ctrl + P</kbd> | Expand all |
+| <kbd>Ctrl + N</kbd> | Collapse all |
 
 ## Usage
 
 ```bash
-JSON navigator and interactive filter leveraging jq
+SON navigator and interactive filter leveraging jq
 
 Usage: jnv [OPTIONS] [INPUT]
 
@@ -165,22 +184,13 @@ Arguments:
   [INPUT]  Optional path to a JSON file. If not provided or if "-" is specified, reads from standard input
 
 Options:
-  -e, --edit-mode <EDIT_MODE>
-          Edit mode for the interface ('insert' or 'overwrite'). [default: insert]
-  -i, --indent <INDENT>
-          Number of spaces used for indentation in the visualized data. [default: 2]
-  -n, --no-hint
-          Disables the display of hints.
-  -d, --expand-depth <JSON_EXPAND_DEPTH>
-          Initial depth to which JSON nodes are expanded in the visualization. [default: 3]
-  -s, --limit-length <JSON_LIMIT_LENGTH>
-          Limit length of JSON array in the visualization. [default: 50]
-  -l, --suggestion-list-length <SUGGESTION_LIST_LENGTH>
-          Number of suggestions visible in the list. [default: 3]
-  -h, --help
-          Print help (see more with '--help')
-  -V, --version
-          Print version
+  -e, --edit-mode <EDIT_MODE>      Edit mode for the interface ('insert' or 'overwrite'). [default: insert]
+  -i, --indent <INDENT>            Number of spaces used for indentation in the visualized data. [default: 2]
+  -n, --no-hint                    Disables the display of hints.
+      --max-streams <MAX_STREAMS>  Maximum number of JSON streams to display
+      --suggestions <SUGGESTIONS>  Number of autocomplete suggestions to show [default: 3]
+  -h, --help                       Print help (see more with '--help')
+  -V, --version                    Print version
 ```
 
 ## Stargazers over time
diff --git a/src/editor.rs b/src/editor.rs
new file mode 100644
index 0000000..849cf7b
--- /dev/null
+++ b/src/editor.rs
@@ -0,0 +1,313 @@
+use std::{future::Future, pin::Pin};
+
+use crossterm::{
+    event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers},
+    style::{Color, ContentStyle},
+};
+use promkit::{pane::Pane, style::StyleBuilder, text, text_editor, PaneFactory};
+
+use crate::search::IncrementalSearcher;
+
+pub struct Editor {
+    keybind: Keybind,
+    state: text_editor::State,
+    focus_theme: EditorTheme,
+    defocus_theme: EditorTheme,
+    guide: text::State,
+    searcher: IncrementalSearcher,
+}
+
+pub struct EditorTheme {
+    pub prefix: String,
+
+    /// Style applied to the prompt string.
+    pub prefix_style: ContentStyle,
+    /// Style applied to the currently selected character.
+    pub active_char_style: ContentStyle,
+    /// Style applied to characters that are not currently selected.
+    pub inactive_char_style: ContentStyle,
+}
+
+impl Editor {
+    pub fn new(
+        state: text_editor::State,
+        searcher: IncrementalSearcher,
+        focus_theme: EditorTheme,
+        defocus_theme: EditorTheme,
+    ) -> Self {
+        Self {
+            keybind: BOXED_EDITOR_KEYBIND,
+            state,
+            focus_theme,
+            defocus_theme,
+            guide: text::State {
+                text: Default::default(),
+                style: Default::default(),
+            },
+            searcher,
+        }
+    }
+
+    pub fn focus(&mut self) {
+        self.state.prefix = self.focus_theme.prefix.clone();
+        self.state.prefix_style = self.focus_theme.prefix_style;
+        self.state.inactive_char_style = self.focus_theme.inactive_char_style;
+        self.state.active_char_style = self.focus_theme.active_char_style;
+    }
+
+    pub fn defocus(&mut self) {
+        self.state.prefix = self.defocus_theme.prefix.clone();
+        self.state.prefix_style = self.defocus_theme.prefix_style;
+        self.state.inactive_char_style = self.defocus_theme.inactive_char_style;
+        self.state.active_char_style = self.defocus_theme.active_char_style;
+
+        self.searcher.leave_search();
+        self.keybind = BOXED_EDITOR_KEYBIND;
+
+        self.guide.text = Default::default();
+    }
+
+    pub fn text(&self) -> String {
+        self.state.texteditor.text_without_cursor().to_string()
+    }
+
+    pub fn create_editor_pane(&self, width: u16, height: u16) -> Pane {
+        self.state.create_pane(width, height)
+    }
+
+    pub fn create_searcher_pane(&self, width: u16, height: u16) -> Pane {
+        self.searcher.create_pane(width, height)
+    }
+
+    pub fn create_guide_pane(&self, width: u16, height: u16) -> Pane {
+        self.guide.create_pane(width, height)
+    }
+
+    pub async fn operate(&mut self, event: &Event) -> anyhow::Result<()> {
+        (self.keybind)(event, self).await
+    }
+}
+
+pub type Keybind = for<'a> fn(
+    &'a Event,
+    &'a mut Editor,
+) -> Pin<Box<dyn Future<Output = anyhow::Result<()>> + Send + 'a>>;
+
+const BOXED_EDITOR_KEYBIND: Keybind =
+    |event, editor| -> Pin<Box<dyn Future<Output = anyhow::Result<()>> + Send + '_>> {
+        Box::pin(edit(event, editor))
+    };
+const BOXED_SEARCHER_KEYBIND: Keybind =
+    |event, editor| -> Pin<Box<dyn Future<Output = anyhow::Result<()>> + Send + '_>> {
+        Box::pin(search(event, editor))
+    };
+
+pub async fn edit<'a>(event: &'a Event, editor: &'a mut Editor) -> anyhow::Result<()> {
+    editor.guide.text = Default::default();
+
+    match event {
+        Event::Key(KeyEvent {
+            code: KeyCode::Tab,
+            modifiers: KeyModifiers::NONE,
+            kind: KeyEventKind::Press,
+            state: KeyEventState::NONE,
+        }) => {
+            let prefix = editor.state.texteditor.text_without_cursor().to_string();
+            match editor.searcher.start_search(&prefix) {
+                Ok(result) => match result.head_item {
+                    Some(head) => {
+                        if result.load_state.loaded {
+                            editor.guide.text = format!(
+                                "Loaded all ({}) suggestions",
+                                result.load_state.loaded_item_len
+                            );
+                            editor.guide.style = StyleBuilder::new().fgc(Color::Green).build();
+                        } else {
+                            editor.guide.text = format!(
+                                "Loaded partially ({}) suggestions",
+                                result.load_state.loaded_item_len
+                            );
+                            editor.guide.style = StyleBuilder::new().fgc(Color::Green).build();
+                        }
+                        editor.state.texteditor.replace(&head);
+                        editor.keybind = BOXED_SEARCHER_KEYBIND;
+                    }
+                    None => {
+                        editor.guide.text = format!("No suggestion found for '{}'", prefix);
+                        editor.guide.style = StyleBuilder::new().fgc(Color::Yellow).build();
+                    }
+                },
+                Err(e) => {
+                    editor.guide.text = format!("Failed to lookup suggestions: {}", e);
+                    editor.guide.style = StyleBuilder::new().fgc(Color::Yellow).build();
+                }
+            }
+        }
+
+        // Move cursor.
+        Event::Key(KeyEvent {
+            code: KeyCode::Left,
+            modifiers: KeyModifiers::NONE,
+            kind: KeyEventKind::Press,
+            state: KeyEventState::NONE,
+        }) => {
+            editor.state.texteditor.backward();
+        }
+        Event::Key(KeyEvent {
+            code: KeyCode::Right,
+            modifiers: KeyModifiers::NONE,
+            kind: KeyEventKind::Press,
+            state: KeyEventState::NONE,
+        }) => {
+            editor.state.texteditor.forward();
+        }
+        Event::Key(KeyEvent {
+            code: KeyCode::Char('a'),
+            modifiers: KeyModifiers::CONTROL,
+            kind: KeyEventKind::Press,
+            state: KeyEventState::NONE,
+        }) => {
+            editor.state.texteditor.move_to_head();
+        }
+        Event::Key(KeyEvent {
+            code: KeyCode::Char('e'),
+            modifiers: KeyModifiers::CONTROL,
+            kind: KeyEventKind::Press,
+            state: KeyEventState::NONE,
+        }) => {
+            editor.state.texteditor.move_to_tail();
+        }
+
+        // Move cursor to the nearest character.
+        Event::Key(KeyEvent {
+            code: KeyCode::Char('b'),
+            modifiers: KeyModifiers::ALT,
+            kind: KeyEventKind::Press,
+            state: KeyEventState::NONE,
+        }) => {
+            editor
+                .state
+                .texteditor
+                .move_to_previous_nearest(&editor.state.word_break_chars);
+        }
+
+        Event::Key(KeyEvent {
+            code: KeyCode::Char('f'),
+            modifiers: KeyModifiers::ALT,
+            kind: KeyEventKind::Press,
+            state: KeyEventState::NONE,
+        }) => {
+            editor
+                .state
+                .texteditor
+                .move_to_next_nearest(&editor.state.word_break_chars);
+        }
+
+        // Erase char(s).
+        Event::Key(KeyEvent {
+            code: KeyCode::Backspace,
+            modifiers: KeyModifiers::NONE,
+            kind: KeyEventKind::Press,
+            state: KeyEventState::NONE,
+        }) => {
+            editor.state.texteditor.erase();
+        }
+        Event::Key(KeyEvent {
+            code: KeyCode::Char('u'),
+            modifiers: KeyModifiers::CONTROL,
+            kind: KeyEventKind::Press,
+            state: KeyEventState::NONE,
+        }) => {
+            editor.state.texteditor.erase_all();
+        }
+
+        // Erase to the nearest character.
+        Event::Key(KeyEvent {
+            code: KeyCode::Char('w'),
+            modifiers: KeyModifiers::CONTROL,
+            kind: KeyEventKind::Press,
+            state: KeyEventState::NONE,
+        }) => {
+            editor
+                .state
+                .texteditor
+                .erase_to_previous_nearest(&editor.state.word_break_chars);
+        }
+
+        Event::Key(KeyEvent {
+            code: KeyCode::Char('d'),
+            modifiers: KeyModifiers::ALT,
+            kind: KeyEventKind::Press,
+            state: KeyEventState::NONE,
+        }) => {
+            editor
+                .state
+                .texteditor
+                .erase_to_next_nearest(&editor.state.word_break_chars);
+        }
+
+        // Input char.
+        Event::Key(KeyEvent {
+            code: KeyCode::Char(ch),
+            modifiers: KeyModifiers::NONE,
+            kind: KeyEventKind::Press,
+            state: KeyEventState::NONE,
+        })
+        | Event::Key(KeyEvent {
+            code: KeyCode::Char(ch),
+            modifiers: KeyModifiers::SHIFT,
+            kind: KeyEventKind::Press,
+            state: KeyEventState::NONE,
+        }) => match editor.state.edit_mode {
+            text_editor::Mode::Insert => editor.state.texteditor.insert(*ch),
+            text_editor::Mode::Overwrite => editor.state.texteditor.overwrite(*ch),
+        },
+
+        _ => {}
+    }
+    Ok(())
+}
+
+pub async fn search<'a>(event: &'a Event, editor: &'a mut Editor) -> anyhow::Result<()> {
+    match event {
+        Event::Key(KeyEvent {
+            code: KeyCode::Tab,
+            modifiers: KeyModifiers::NONE,
+            kind: KeyEventKind::Press,
+            state: KeyEventState::NONE,
+        })
+        | Event::Key(KeyEvent {
+            code: KeyCode::Down,
+            modifiers: KeyModifiers::NONE,
+            kind: KeyEventKind::Press,
+            state: KeyEventState::NONE,
+        }) => {
+            editor.searcher.down_with_load();
+            editor
+                .state
+                .texteditor
+                .replace(&editor.searcher.get_current_item());
+        }
+
+        Event::Key(KeyEvent {
+            code: KeyCode::Up,
+            modifiers: KeyModifiers::NONE,
+            kind: KeyEventKind::Press,
+            state: KeyEventState::NONE,
+        }) => {
+            editor.searcher.up();
+            editor
+                .state
+                .texteditor
+                .replace(&editor.searcher.get_current_item());
+        }
+
+        _ => {
+            editor.searcher.leave_search();
+            editor.keybind = BOXED_EDITOR_KEYBIND;
+            return edit(event, editor).await;
+        }
+    }
+
+    Ok(())
+}
diff --git a/src/jnv.rs b/src/jnv.rs
deleted file mode 100644
index 4a43b53..0000000
--- a/src/jnv.rs
+++ /dev/null
@@ -1,340 +0,0 @@
-use std::cell::RefCell;
-
-use anyhow::Result;
-
-use arboard::Clipboard;
-use jaq_interpret::{Ctx, FilterT, ParseCtx, RcIter, Val};
-
-use promkit::{
-    crossterm::{
-        event::Event,
-        style::{Attribute, Attributes, Color, ContentStyle},
-    },
-    jsonstream::{self, JsonStream},
-    jsonz::{self, format::RowFormatter, Value},
-    listbox,
-    pane::Pane,
-    serde_json,
-    snapshot::Snapshot,
-    style::StyleBuilder,
-    suggest::Suggest,
-    switch::ActiveKeySwitcher,
-    text, text_editor, PaneFactory, Prompt, PromptSignal,
-};
-
-use crate::trie::FilterTrie;
-
-mod keymap;
-
-fn run_jaq(
-    query: &str,
-    json_stream: &[serde_json::Value],
-) -> anyhow::Result<Vec<serde_json::Value>> {
-    let mut ret = Vec::<serde_json::Value>::new();
-
-    for input in json_stream {
-        let mut ctx = ParseCtx::new(Vec::new());
-        ctx.insert_natives(jaq_core::core());
-        ctx.insert_defs(jaq_std::std());
-
-        let (f, errs) = jaq_parse::parse(query, jaq_parse::main());
-        if !errs.is_empty() {
-            let error_message = errs
-                .iter()
-                .map(|e| e.to_string())
-                .collect::<Vec<_>>()
-                .join(", ");
-            return Err(anyhow::anyhow!(error_message));
-        }
-
-        let f = ctx.compile(f.unwrap());
-        let inputs = RcIter::new(core::iter::empty());
-        let mut out = f.run((Ctx::new([], &inputs), Val::from(input.clone())));
-
-        while let Some(Ok(val)) = out.next() {
-            ret.push(val.into());
-        }
-    }
-
-    Ok(ret)
-}
-
-pub struct JsonTheme {
-    /// Style for {}.
-    pub curly_brackets_style: ContentStyle,
-    /// Style for [].
-    pub square_brackets_style: ContentStyle,
-    /// Style for "key".
-    pub key_style: ContentStyle,
-    /// Style for string values.
-    pub string_value_style: ContentStyle,
-    /// Style for number values.
-    pub number_value_style: ContentStyle,
-    /// Style for boolean values.
-    pub boolean_value_style: ContentStyle,
-    /// Style for null values.
-    pub null_value_style: ContentStyle,
-
-    /// Attribute for the selected line.
-    pub active_item_attribute: Attribute,
-    /// Attribute for unselected lines.
-    pub inactive_item_attribute: Attribute,
-
-    /// Number of lines available for rendering.
-    pub lines: Option<usize>,
-
-    /// The number of spaces used for indentation in the rendered JSON structure.
-    /// This value multiplies with the indentation level of a JSON element to determine
-    /// the total indentation space. For example, an `indent` value of 4 means each
-    /// indentation level will be 4 spaces wide.
-    pub indent: usize,
-}
-
-pub struct Jnv {
-    input_stream: Vec<serde_json::Value>,
-
-    // Keybindings
-    keymap: RefCell<ActiveKeySwitcher<keymap::Keymap>>,
-
-    // For Rendering
-    filter_editor: Snapshot<text_editor::State>,
-    hint_message: Snapshot<text::State>,
-    suggestions: listbox::State,
-    json: jsonstream::State,
-
-    // Store the filter history
-    trie: FilterTrie,
-    // Store the filter suggestions
-    suggest: Suggest,
-
-    no_hint: bool,
-}
-
-impl Jnv {
-    #[allow(clippy::too_many_arguments)]
-    pub fn try_new(
-        input_stream: Vec<serde_json::Value>,
-        filter_editor: text_editor::State,
-        hint_message: text::State,
-        suggestions: listbox::State,
-        json_theme: JsonTheme,
-        no_hint: bool,
-    ) -> Result<Prompt<Self>> {
-        let mut trie = FilterTrie::default();
-        trie.insert(".", input_stream.clone());
-        let stream = JsonStream::new(&input_stream);
-        let suggest = Suggest::from_iter(jsonz::get_all_paths(&input_stream));
-
-        Ok(Prompt {
-            renderer: Self {
-                keymap: RefCell::new(
-                    ActiveKeySwitcher::new("default", self::keymap::default as keymap::Keymap)
-                        .register("on_suggest", self::keymap::on_suggest),
-                ),
-                filter_editor: Snapshot::<text_editor::State>::new(filter_editor),
-                hint_message: Snapshot::<text::State>::new(hint_message),
-                suggestions,
-                json: jsonstream::State {
-                    stream,
-                    formatter: RowFormatter {
-                        curly_brackets_style: json_theme.curly_brackets_style,
-                        square_brackets_style: json_theme.square_brackets_style,
-                        key_style: json_theme.key_style,
-                        string_value_style: json_theme.string_value_style,
-                        number_value_style: json_theme.number_value_style,
-                        boolean_value_style: json_theme.boolean_value_style,
-                        null_value_style: json_theme.null_value_style,
-                        active_item_attribute: json_theme.active_item_attribute,
-                        inactive_item_attribute: json_theme.inactive_item_attribute,
-                        indent: json_theme.indent,
-                    },
-                    lines: json_theme.lines,
-                },
-                trie,
-                suggest,
-                no_hint,
-                input_stream,
-            },
-        })
-    }
-
-    fn update_hint_message(&mut self, text: String, style: ContentStyle) {
-        if !self.no_hint {
-            self.hint_message
-                .after_mut()
-                .replace(text::State { text, style })
-        }
-    }
-
-    fn store_to_clipboard(&mut self, content: &str, hint: &str) {
-        match Clipboard::new() {
-            Ok(mut clipboard) => match clipboard.set_text(content) {
-                Ok(_) => {
-                    self.update_hint_message(
-                        hint.to_string(),
-                        StyleBuilder::new().fgc(Color::Grey).build(),
-                    );
-                }
-                Err(e) => {
-                    self.update_hint_message(
-                        format!("Failed to copy to clipboard: {}", e),
-                        StyleBuilder::new()
-                            .fgc(Color::Red)
-                            .attrs(Attributes::from(Attribute::Bold))
-                            .build(),
-                    );
-                }
-            },
-            // arboard fails (in the specific environment like linux?) on Clipboard::new()
-            // suppress the errors (but still show them) not to break the prompt
-            // https://github.com/1Password/arboard/issues/153
-            Err(e) => {
-                self.update_hint_message(
-                    format!("Failed to setup clipboard: {}", e),
-                    StyleBuilder::new()
-                        .fgc(Color::Red)
-                        .attrs(Attributes::from(Attribute::Bold))
-                        .build(),
-                );
-            }
-        }
-    }
-
-    fn store_content_to_clipboard(&mut self) {
-        self.store_to_clipboard(
-            &self.json.formatter.format_raw_json(self.json.stream.rows()),
-            "Copied selected content to clipboard!",
-        );
-    }
-
-    fn store_query_to_clipboard(&mut self) {
-        self.store_to_clipboard(
-            &self
-                .filter_editor
-                .after()
-                .texteditor
-                .text_without_cursor()
-                .to_string(),
-            "Copied jq query to clipboard!",
-        );
-    }
-}
-
-impl promkit::Finalizer for Jnv {
-    type Return = String;
-
-    fn finalize(&mut self) -> anyhow::Result<Self::Return> {
-        Ok(self
-            .filter_editor
-            .after()
-            .texteditor
-            .text_without_cursor()
-            .to_string())
-    }
-}
-
-impl promkit::Renderer for Jnv {
-    fn create_panes(&self, width: u16, height: u16) -> Vec<Pane> {
-        vec![
-            self.filter_editor.create_pane(width, height),
-            self.hint_message.create_pane(width, height),
-            self.suggestions.create_pane(width, height),
-            self.json.create_pane(width, height),
-        ]
-    }
-
-    fn evaluate(&mut self, event: &Event) -> anyhow::Result<PromptSignal> {
-        let keymap = *self.keymap.borrow_mut().get();
-        let signal = keymap(event, self);
-        let filter = self
-            .filter_editor
-            .after()
-            .texteditor
-            .text_without_cursor()
-            .to_string();
-
-        // Check if the query has changed
-        if filter
-            != self
-                .filter_editor
-                .borrow_before()
-                .texteditor
-                .text_without_cursor()
-                .to_string()
-        {
-            self.hint_message.reset_after_to_init();
-
-            match self.trie.exact_search(&filter) {
-                Some(jsonl) => {
-                    self.json.stream = JsonStream::new(jsonl);
-                    self.update_hint_message(
-                        format!(
-                            "JSON query ('{}') was already executed. Result was retrieved from cache.",
-                            &filter
-                        ),
-                        StyleBuilder::new()
-                            .fgc(Color::DarkGrey)
-                            .attrs(Attributes::from(Attribute::Bold))
-                            .build(),
-                    );
-                }
-                None => {
-                    match run_jaq(&filter, &self.input_stream) {
-                        Ok(ret) => {
-                            if ret.is_empty() {
-                                self.update_hint_message(
-                                    format!(
-                                        "JSON query ('{}') was executed, but no results were returned.",
-                                        &filter
-                                    ),
-                                    StyleBuilder::new()
-                                        .fgc(Color::Red)
-                                        .attrs(Attributes::from(Attribute::Bold))
-                                        .build(),
-                                );
-                                if let Some(searched) = self.trie.prefix_search(&filter) {
-                                    self.json.stream = JsonStream::new(searched);
-                                }
-                            } else {
-                                let stream = JsonStream::new(ret.iter());
-
-                                let is_null =
-                                    stream.rows().iter().all(|node| node.v == Value::Null);
-                                if is_null {
-                                    self.update_hint_message(
-                                        format!("JSON query resulted in 'null', which may indicate a typo or incorrect query: '{}'", &filter),
-                                        StyleBuilder::new()
-                                            .fgc(Color::Yellow)
-                                            .attrs(Attributes::from(Attribute::Bold))
-                                            .build(),
-                                    );
-                                    if let Some(searched) = self.trie.prefix_search(&filter) {
-                                        self.json.stream = JsonStream::new(searched);
-                                    }
-                                } else {
-                                    // SUCCESS!
-                                    self.trie.insert(&filter, ret);
-                                    self.json.stream = stream;
-                                }
-                            }
-                        }
-                        Err(_) => {
-                            self.update_hint_message(
-                                format!("Failed to execute jq query '{}'", &filter),
-                                StyleBuilder::new()
-                                    .fgc(Color::Red)
-                                    .attrs(Attributes::from(Attribute::Bold))
-                                    .build(),
-                            );
-                            if let Some(searched) = self.trie.prefix_search(&filter) {
-                                self.json.stream = JsonStream::new(searched);
-                            }
-                            return signal;
-                        }
-                    }
-                }
-            }
-        }
-        signal
-    }
-}
diff --git a/src/jnv/keymap.rs b/src/jnv/keymap.rs
deleted file mode 100644
index c0169e1..0000000
--- a/src/jnv/keymap.rs
+++ /dev/null
@@ -1,302 +0,0 @@
-use promkit::{
-    crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers},
-    listbox::Listbox,
-    text_editor, PromptSignal,
-};
-
-pub type Keymap = fn(&Event, &mut crate::jnv::Jnv) -> anyhow::Result<PromptSignal>;
-
-pub fn default(event: &Event, jnv: &mut crate::jnv::Jnv) -> anyhow::Result<PromptSignal> {
-    let filter_editor = jnv.filter_editor.after_mut();
-
-    match event {
-        Event::Key(KeyEvent {
-            code: KeyCode::Tab,
-            modifiers: KeyModifiers::NONE,
-            kind: KeyEventKind::Press,
-            state: KeyEventState::NONE,
-        }) => {
-            let query = filter_editor.texteditor.text_without_cursor().to_string();
-            if let Some(mut candidates) = jnv.suggest.prefix_search(query) {
-                candidates.sort_by(|a, b| a.len().cmp(&b.len()).then_with(|| a.cmp(b)));
-
-                jnv.suggestions.listbox = Listbox::from_displayable(candidates);
-                filter_editor
-                    .texteditor
-                    .replace(&jnv.suggestions.listbox.get().to_string());
-
-                jnv.keymap.borrow_mut().switch("on_suggest");
-            }
-        }
-
-        Event::Key(KeyEvent {
-            code: KeyCode::Char('c'),
-            modifiers: KeyModifiers::CONTROL,
-            kind: KeyEventKind::Press,
-            state: KeyEventState::NONE,
-        }) => return Ok(PromptSignal::Quit),
-
-        // Move cursor.
-        Event::Key(KeyEvent {
-            code: KeyCode::Left,
-            modifiers: KeyModifiers::NONE,
-            kind: KeyEventKind::Press,
-            state: KeyEventState::NONE,
-        }) => {
-            filter_editor.texteditor.backward();
-        }
-        Event::Key(KeyEvent {
-            code: KeyCode::Right,
-            modifiers: KeyModifiers::NONE,
-            kind: KeyEventKind::Press,
-            state: KeyEventState::NONE,
-        }) => {
-            filter_editor.texteditor.forward();
-        }
-        Event::Key(KeyEvent {
-            code: KeyCode::Char('a'),
-            modifiers: KeyModifiers::CONTROL,
-            kind: KeyEventKind::Press,
-            state: KeyEventState::NONE,
-        }) => filter_editor.texteditor.move_to_head(),
-        Event::Key(KeyEvent {
-            code: KeyCode::Char('e'),
-            modifiers: KeyModifiers::CONTROL,
-            kind: KeyEventKind::Press,
-            state: KeyEventState::NONE,
-        }) => filter_editor.texteditor.move_to_tail(),
-
-        Event::Key(KeyEvent {
-            code: KeyCode::Char('b'),
-            modifiers: KeyModifiers::ALT,
-            kind: KeyEventKind::Press,
-            state: KeyEventState::NONE,
-        }) => filter_editor
-            .texteditor
-            .move_to_previous_nearest(&filter_editor.word_break_chars),
-
-        Event::Key(KeyEvent {
-            code: KeyCode::Char('f'),
-            modifiers: KeyModifiers::ALT,
-            kind: KeyEventKind::Press,
-            state: KeyEventState::NONE,
-        }) => filter_editor
-            .texteditor
-            .move_to_next_nearest(&filter_editor.word_break_chars),
-
-        // Erase char(s).
-        Event::Key(KeyEvent {
-            code: KeyCode::Backspace,
-            modifiers: KeyModifiers::NONE,
-            kind: KeyEventKind::Press,
-            state: KeyEventState::NONE,
-        }) => filter_editor.texteditor.erase(),
-        Event::Key(KeyEvent {
-            code: KeyCode::Char('u'),
-            modifiers: KeyModifiers::CONTROL,
-            kind: KeyEventKind::Press,
-            state: KeyEventState::NONE,
-        }) => filter_editor.texteditor.erase_all(),
-
-        // Erase to the nearest character.
-        Event::Key(KeyEvent {
-            code: KeyCode::Char('w'),
-            modifiers: KeyModifiers::CONTROL,
-            kind: KeyEventKind::Press,
-            state: KeyEventState::NONE,
-        }) => filter_editor
-            .texteditor
-            .erase_to_previous_nearest(&filter_editor.word_break_chars),
-
-        Event::Key(KeyEvent {
-            code: KeyCode::Char('d'),
-            modifiers: KeyModifiers::ALT,
-            kind: KeyEventKind::Press,
-            state: KeyEventState::NONE,
-        }) => filter_editor
-            .texteditor
-            .erase_to_next_nearest(&filter_editor.word_break_chars),
-
-        // Move up.
-        Event::Key(KeyEvent {
-            code: KeyCode::Up,
-            modifiers: KeyModifiers::NONE,
-            kind: KeyEventKind::Press,
-            state: KeyEventState::NONE,
-        })
-        | Event::Key(KeyEvent {
-            code: KeyCode::Char('k'),
-            modifiers: KeyModifiers::CONTROL,
-            kind: KeyEventKind::Press,
-            state: KeyEventState::NONE,
-        }) => {
-            jnv.json.stream.up();
-        }
-
-        // Move down.
-        Event::Key(KeyEvent {
-            code: KeyCode::Down,
-            modifiers: KeyModifiers::NONE,
-            kind: KeyEventKind::Press,
-            state: KeyEventState::NONE,
-        })
-        | Event::Key(KeyEvent {
-            code: KeyCode::Char('j'),
-            modifiers: KeyModifiers::CONTROL,
-            kind: KeyEventKind::Press,
-            state: KeyEventState::NONE,
-        }) => {
-            jnv.json.stream.down();
-        }
-
-        // Move to tail
-        Event::Key(KeyEvent {
-            code: KeyCode::Char('h'),
-            modifiers: KeyModifiers::CONTROL,
-            kind: KeyEventKind::Press,
-            state: KeyEventState::NONE,
-        }) => {
-            jnv.json.stream.tail();
-        }
-
-        // Move to head
-        Event::Key(KeyEvent {
-            code: KeyCode::Char('l'),
-            modifiers: KeyModifiers::CONTROL,
-            kind: KeyEventKind::Press,
-            state: KeyEventState::NONE,
-        }) => {
-            jnv.json.stream.head();
-        }
-
-        // Toggle collapse/expand
-        Event::Key(KeyEvent {
-            code: KeyCode::Enter,
-            modifiers: KeyModifiers::NONE,
-            kind: KeyEventKind::Press,
-            state: KeyEventState::NONE,
-        }) => {
-            jnv.json.stream.toggle();
-        }
-
-        Event::Key(KeyEvent {
-            code: KeyCode::Char('p'),
-            modifiers: KeyModifiers::CONTROL,
-            kind: KeyEventKind::Press,
-            state: KeyEventState::NONE,
-        }) => {
-            jnv.json.stream.set_nodes_visibility(false);
-        }
-
-        Event::Key(KeyEvent {
-            code: KeyCode::Char('o'),
-            modifiers: KeyModifiers::CONTROL,
-            kind: KeyEventKind::Press,
-            state: KeyEventState::NONE,
-        }) => {
-            jnv.store_content_to_clipboard();
-        }
-
-        Event::Key(KeyEvent {
-            code: KeyCode::Char('q'),
-            modifiers: KeyModifiers::CONTROL,
-            kind: KeyEventKind::Press,
-            state: KeyEventState::NONE,
-        }) => {
-            jnv.store_query_to_clipboard();
-        }
-
-        Event::Key(KeyEvent {
-            code: KeyCode::Char('n'),
-            modifiers: KeyModifiers::CONTROL,
-            kind: KeyEventKind::Press,
-            state: KeyEventState::NONE,
-        }) => {
-            jnv.json.stream.set_nodes_visibility(true);
-        }
-
-        // Input char.
-        Event::Key(KeyEvent {
-            code: KeyCode::Char(ch),
-            modifiers: KeyModifiers::NONE,
-            kind: KeyEventKind::Press,
-            state: KeyEventState::NONE,
-        })
-        | Event::Key(KeyEvent {
-            code: KeyCode::Char(ch),
-            modifiers: KeyModifiers::SHIFT,
-            kind: KeyEventKind::Press,
-            state: KeyEventState::NONE,
-        }) => match filter_editor.edit_mode {
-            text_editor::Mode::Insert => filter_editor.texteditor.insert(*ch),
-            text_editor::Mode::Overwrite => filter_editor.texteditor.overwrite(*ch),
-        },
-
-        _ => (),
-    }
-    Ok(PromptSignal::Continue)
-}
-
-pub fn on_suggest(event: &Event, jnv: &mut crate::jnv::Jnv) -> anyhow::Result<PromptSignal> {
-    let query_editor_after_mut = jnv.filter_editor.after_mut();
-
-    match event {
-        Event::Key(KeyEvent {
-            code: KeyCode::Char('c'),
-            modifiers: KeyModifiers::CONTROL,
-            kind: KeyEventKind::Press,
-            state: KeyEventState::NONE,
-        }) => return Ok(PromptSignal::Quit),
-
-        Event::Key(KeyEvent {
-            code: KeyCode::Tab,
-            modifiers: KeyModifiers::NONE,
-            kind: KeyEventKind::Press,
-            state: KeyEventState::NONE,
-        })
-        | Event::Key(KeyEvent {
-            code: KeyCode::Down,
-            modifiers: KeyModifiers::NONE,
-            kind: KeyEventKind::Press,
-            state: KeyEventState::NONE,
-        }) => {
-            jnv.suggestions.listbox.forward();
-            query_editor_after_mut
-                .texteditor
-                .replace(&jnv.suggestions.listbox.get().to_string());
-        }
-
-        Event::Key(KeyEvent {
-            code: KeyCode::Up,
-            modifiers: KeyModifiers::NONE,
-            kind: KeyEventKind::Press,
-            state: KeyEventState::NONE,
-        }) => {
-            jnv.suggestions.listbox.backward();
-            query_editor_after_mut
-                .texteditor
-                .replace(&jnv.suggestions.listbox.get().to_string());
-        }
-
-        _ => {
-            jnv.suggestions.listbox = Listbox::from_displayable(Vec::<String>::new());
-            jnv.keymap.borrow_mut().switch("default");
-
-            // This block is specifically designed to prevent the default action of toggling collapse/expand
-            // from being executed when the Enter key is pressed. This is done from the perspective of user
-            // experimentation, ensuring that pressing Enter while in the suggest mode does not trigger
-            // the default behavior associated with the Enter key in the default mode.
-            if let Event::Key(KeyEvent {
-                code: KeyCode::Enter,
-                modifiers: KeyModifiers::NONE,
-                kind: KeyEventKind::Press,
-                state: KeyEventState::NONE,
-            }) = event
-            {
-            } else {
-                return default(event, jnv);
-            }
-        }
-    }
-    Ok(PromptSignal::Continue)
-}
diff --git a/src/json.rs b/src/json.rs
new file mode 100644
index 0000000..fa54118
--- /dev/null
+++ b/src/json.rs
@@ -0,0 +1,262 @@
+use crossterm::{
+    event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers},
+    style::{Attribute, Attributes},
+};
+use jaq_interpret::{Ctx, FilterT, ParseCtx, RcIter, Val};
+use promkit::{
+    crossterm::style::Color,
+    jsonstream::{self, JsonStream},
+    jsonz::{self, format::RowFormatter},
+    pane::Pane,
+    serde_json::{self, Deserializer, Value},
+    style::StyleBuilder,
+    text, PaneFactory,
+};
+
+use crate::{
+    processor::{ViewProvider, Visualizer},
+    search::SearchProvider,
+};
+
+#[derive(Clone)]
+pub struct Json {
+    state: jsonstream::State,
+    json: &'static [serde_json::Value],
+}
+
+impl Json {
+    pub fn new(
+        formatter: RowFormatter,
+        input_stream: &'static [serde_json::Value],
+    ) -> anyhow::Result<Self> {
+        Ok(Self {
+            json: input_stream,
+            state: jsonstream::State {
+                stream: JsonStream::new(input_stream.iter()),
+                formatter,
+                lines: Default::default(),
+            },
+        })
+    }
+
+    fn operate(&mut self, event: &Event) {
+        match event {
+            // Move up.
+            Event::Key(KeyEvent {
+                code: KeyCode::Up,
+                modifiers: KeyModifiers::NONE,
+                kind: KeyEventKind::Press,
+                state: KeyEventState::NONE,
+            })
+            | Event::Key(KeyEvent {
+                code: KeyCode::Char('k'),
+                modifiers: KeyModifiers::CONTROL,
+                kind: KeyEventKind::Press,
+                state: KeyEventState::NONE,
+            }) => {
+                self.state.stream.up();
+            }
+
+            // Move down.
+            Event::Key(KeyEvent {
+                code: KeyCode::Down,
+                modifiers: KeyModifiers::NONE,
+                kind: KeyEventKind::Press,
+                state: KeyEventState::NONE,
+            })
+            | Event::Key(KeyEvent {
+                code: KeyCode::Char('j'),
+                modifiers: KeyModifiers::CONTROL,
+                kind: KeyEventKind::Press,
+                state: KeyEventState::NONE,
+            }) => {
+                self.state.stream.down();
+            }
+
+            // Move to tail
+            Event::Key(KeyEvent {
+                code: KeyCode::Char('h'),
+                modifiers: KeyModifiers::CONTROL,
+                kind: KeyEventKind::Press,
+                state: KeyEventState::NONE,
+            }) => {
+                self.state.stream.tail();
+            }
+
+            // Move to head
+            Event::Key(KeyEvent {
+                code: KeyCode::Char('l'),
+                modifiers: KeyModifiers::CONTROL,
+                kind: KeyEventKind::Press,
+                state: KeyEventState::NONE,
+            }) => {
+                self.state.stream.head();
+            }
+
+            // Toggle collapse/expand
+            Event::Key(KeyEvent {
+                code: KeyCode::Enter,
+                modifiers: KeyModifiers::NONE,
+                kind: KeyEventKind::Press,
+                state: KeyEventState::NONE,
+            }) => {
+                self.state.stream.toggle();
+            }
+
+            Event::Key(KeyEvent {
+                code: KeyCode::Char('p'),
+                modifiers: KeyModifiers::CONTROL,
+                kind: KeyEventKind::Press,
+                state: KeyEventState::NONE,
+            }) => {
+                self.state.stream.set_nodes_visibility(false);
+            }
+
+            Event::Key(KeyEvent {
+                code: KeyCode::Char('n'),
+                modifiers: KeyModifiers::CONTROL,
+                kind: KeyEventKind::Press,
+                state: KeyEventState::NONE,
+            }) => {
+                self.state.stream.set_nodes_visibility(true);
+            }
+
+            _ => (),
+        }
+    }
+}
+
+#[async_trait::async_trait]
+impl Visualizer for Json {
+    async fn content_to_copy(&self) -> String {
+        self.state
+            .formatter
+            .format_raw_json(self.state.stream.rows())
+    }
+
+    async fn create_init_pane(&mut self, area: (u16, u16)) -> Pane {
+        self.state.create_pane(area.0, area.1)
+    }
+
+    async fn create_pane_from_event(&mut self, area: (u16, u16), event: &Event) -> Pane {
+        self.operate(event);
+        self.state.create_pane(area.0, area.1)
+    }
+
+    async fn create_panes_from_query(
+        &mut self,
+        area: (u16, u16),
+        input: String,
+    ) -> (Option<Pane>, Option<Pane>) {
+        match run_jaq(&input, self.json) {
+            Ok(ret) => {
+                let mut guide = None;
+                if ret.iter().all(|val| *val == Value::Null) {
+                    guide = Some(text::State {
+                        text: format!("jq returned 'null', which may indicate a typo or incorrect filter: `{}`", input),
+                        style: StyleBuilder::new()
+                            .fgc(Color::Yellow)
+                            .attrs(Attributes::from(Attribute::Bold))
+                            .build(),
+                    }.create_pane(area.0, area.1));
+                }
+
+                self.state.stream = JsonStream::new(ret.iter());
+
+                (guide, Some(self.state.create_pane(area.0, area.1)))
+            }
+            Err(e) => (
+                Some(
+                    text::State {
+                        text: format!("jq failed: `{}`", e),
+                        style: StyleBuilder::new()
+                            .fgc(Color::Red)
+                            .attrs(Attributes::from(Attribute::Bold))
+                            .build(),
+                    }
+                    .create_pane(area.0, area.1),
+                ),
+                None,
+            ),
+        }
+    }
+}
+
+fn run_jaq(
+    query: &str,
+    json_stream: &'static [serde_json::Value],
+) -> anyhow::Result<Vec<serde_json::Value>> {
+    let mut ret = Vec::<serde_json::Value>::new();
+
+    for input in json_stream {
+        let mut ctx = ParseCtx::new(Vec::new());
+        ctx.insert_natives(jaq_core::core());
+        ctx.insert_defs(jaq_std::std());
+
+        let (f, errs) = jaq_parse::parse(query, jaq_parse::main());
+        if !errs.is_empty() {
+            let error_message = errs
+                .iter()
+                .map(|e| e.to_string())
+                .collect::<Vec<_>>()
+                .join(", ");
+            return Err(anyhow::anyhow!(error_message));
+        }
+
+        let f = ctx.compile(f.unwrap());
+        let inputs = RcIter::new(core::iter::empty());
+        let mut out = f.run((Ctx::new([], &inputs), Val::from(input.clone())));
+
+        while let Some(Ok(val)) = out.next() {
+            ret.push(val.into());
+        }
+    }
+
+    Ok(ret)
+}
+
+#[derive(Clone)]
+pub struct JsonStreamProvider {
+    formatter: RowFormatter,
+    max_streams: Option<usize>,
+}
+
+impl JsonStreamProvider {
+    pub fn new(formatter: RowFormatter, max_streams: Option<usize>) -> Self {
+        Self {
+            formatter,
+            max_streams,
+        }
+    }
+
+    fn deserialize_json(&self, json_str: &str) -> anyhow::Result<Vec<serde_json::Value>> {
+        let deserializer: serde_json::StreamDeserializer<'_, serde_json::de::StrRead<'_>, Value> =
+            Deserializer::from_str(json_str).into_iter::<serde_json::Value>();
+        let results = match self.max_streams {
+            Some(l) => deserializer.take(l).collect::<Result<Vec<_>, _>>(),
+            None => deserializer.collect::<Result<Vec<_>, _>>(),
+        };
+        results.map_err(anyhow::Error::from)
+    }
+}
+
+#[async_trait::async_trait]
+impl ViewProvider for JsonStreamProvider {
+    async fn provide(&mut self, item: &'static str) -> anyhow::Result<Json> {
+        let stream = self.deserialize_json(item)?;
+        let static_stream = Box::leak(stream.into_boxed_slice());
+        Json::new(std::mem::take(&mut self.formatter), static_stream)
+    }
+}
+
+#[async_trait::async_trait]
+impl SearchProvider for JsonStreamProvider {
+    async fn provide(
+        &mut self,
+        item: &str,
+    ) -> anyhow::Result<Box<dyn Iterator<Item = String> + Send>> {
+        let stream = self.deserialize_json(item)?;
+        let static_stream = Box::leak(stream.into_boxed_slice());
+        Ok(Box::new(jsonz::get_all_paths(static_stream.iter())))
+    }
+}
diff --git a/src/main.rs b/src/main.rs
index 90c1860..73a7582 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -3,22 +3,33 @@ use std::{
     fs::File,
     io::{self, Read},
     path::PathBuf,
+    time::Duration,
 };
 
 use anyhow::{anyhow, Result};
 use clap::Parser;
-
+use crossterm::style::{Attribute, Attributes, Color};
 use promkit::{
-    crossterm::style::{Attribute, Attributes, Color},
-    listbox,
-    serde_json::{self, Deserializer},
+    jsonz::format::RowFormatter,
+    listbox::{self, Listbox},
     style::StyleBuilder,
-    text, text_editor,
+    text_editor,
 };
 
-mod jnv;
-use jnv::{Jnv, JsonTheme};
-mod trie;
+mod editor;
+use editor::{Editor, EditorTheme};
+mod json;
+use json::JsonStreamProvider;
+mod processor;
+use processor::{
+    init::ViewInitializer, monitor::ContextMonitor, spinner::SpinnerSpawner, Context, Processor,
+    ViewProvider, Visualizer,
+};
+mod prompt;
+mod render;
+use render::{PaneIndex, Renderer, EMPTY_PANE};
+mod search;
+use search::{IncrementalSearcher, SearchProvider};
 
 /// JSON navigator and interactive filter leveraging jq
 #[derive(Parser)]
@@ -89,28 +100,27 @@ pub struct Args {
     pub no_hint: bool,
 
     #[arg(
-        short = 's',
-        long = "limit-length",
-        default_value = "50",
-        help = "Limit length of JSON array in the visualization.",
+        long = "max-streams",
+        help = "Maximum number of JSON streams to display",
         long_help = "
-        Specifies the limit length to load JSON array in the visualization.
-        Note: Increasing this length can significantly slow down the display for large datasets.
+        Sets the maximum number of JSON streams to load and display.
+        Limiting this value improves performance for large datasets.
+        If not set, all streams will be displayed.
         "
     )]
-    pub json_limit_length: Option<usize>,
+    pub max_streams: Option<usize>,
 
     #[arg(
-        short = 'l',
-        long = "suggestion-list-length",
+        long = "suggestions",
         default_value = "3",
-        help = "Number of suggestions visible in the list.",
+        help = "Number of autocomplete suggestions to show",
         long_help = "
-        Controls the number of suggestions displayed in the list,
-        aiding users in making selections more efficiently.
+        Sets the number of autocomplete suggestions displayed during incremental search.
+        Higher values show more suggestions but may occupy more screen space.
+        Adjust this value based on your screen size and preference.
         "
     )]
-    pub suggestion_list_length: usize,
+    pub suggestions: usize,
 }
 
 fn edit_mode_validator(val: &str) -> Result<text_editor::Mode> {
@@ -147,96 +157,83 @@ fn parse_input(args: &Args) -> Result<String> {
     Ok(ret)
 }
 
-/// Deserializes a JSON string into a vector of `serde_json::Value`.
-///
-/// This function takes a JSON string as input and attempts to parse it into a vector
-/// of `serde_json::Value`, which represents any valid JSON value (e.g., object, array, string, number).
-/// It leverages `serde_json::Deserializer` to parse the string and collect the results.
-///
-/// # Arguments
-/// * `json_str` - A string slice that holds the JSON data to be deserialized.
-///
-/// # Returns
-/// An `anyhow::Result` wrapping a vector of `serde_json::Value`. On success, it contains the parsed
-/// JSON data. On failure, it contains an error detailing what went wrong during parsing.
-fn deserialize_json(
-    json_str: &str,
-    limit_length: Option<usize>,
-) -> anyhow::Result<Vec<serde_json::Value>> {
-    let deserializer = Deserializer::from_str(json_str).into_iter::<serde_json::Value>();
-    let results = match limit_length {
-        Some(l) => deserializer.take(l).collect::<Result<Vec<_>, _>>(),
-        None => deserializer.collect::<Result<Vec<_>, _>>(),
-    };
-    results.map_err(anyhow::Error::from)
-}
-
-fn main() -> Result<()> {
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
     let args = Args::parse();
-
     let input = parse_input(&args)?;
-    let input_stream = deserialize_json(&input, args.json_limit_length)?;
-
-    let filter_editor = text_editor::State {
-        texteditor: Default::default(),
-        history: Default::default(),
-        prefix: String::from("❯❯ "),
-        mask: Default::default(),
-        prefix_style: StyleBuilder::new().fgc(Color::Blue).build(),
-        active_char_style: StyleBuilder::new().bgc(Color::Magenta).build(),
-        inactive_char_style: StyleBuilder::new().build(),
-        edit_mode: args.edit_mode,
-        word_break_chars: HashSet::from(['.', '|', '(', ')', '[', ']']),
-        lines: Default::default(),
-    };
-
-    let hint_message = text::State {
-        text: Default::default(),
-        style: StyleBuilder::new()
-            .fgc(Color::Green)
-            .attrs(Attributes::from(Attribute::Bold))
-            .build(),
-    };
-
-    let suggestions = listbox::State {
-        listbox: listbox::Listbox::from_displayable(Vec::<String>::new()),
-        cursor: String::from("❯ "),
-        active_item_style: Some(
-            StyleBuilder::new()
-                .fgc(Color::Grey)
-                .bgc(Color::Yellow)
-                .build(),
+
+    prompt::run(
+        Box::leak(input.into_boxed_str()),
+        Duration::from_millis(300),
+        Duration::from_millis(600),
+        Duration::from_millis(200),
+        &mut JsonStreamProvider::new(
+            RowFormatter {
+                curly_brackets_style: StyleBuilder::new()
+                    .attrs(Attributes::from(Attribute::Bold))
+                    .build(),
+                square_brackets_style: StyleBuilder::new()
+                    .attrs(Attributes::from(Attribute::Bold))
+                    .build(),
+                key_style: StyleBuilder::new().fgc(Color::Cyan).build(),
+                string_value_style: StyleBuilder::new().fgc(Color::Green).build(),
+                number_value_style: StyleBuilder::new().build(),
+                boolean_value_style: StyleBuilder::new().build(),
+                null_value_style: StyleBuilder::new().fgc(Color::Grey).build(),
+                active_item_attribute: Attribute::Bold,
+                inactive_item_attribute: Attribute::Dim,
+                indent: args.indent,
+            },
+            args.max_streams,
         ),
-        inactive_item_style: Some(StyleBuilder::new().fgc(Color::Grey).build()),
-        lines: Some(args.suggestion_list_length),
-    };
-
-    let json_theme = JsonTheme {
-        curly_brackets_style: StyleBuilder::new()
-            .attrs(Attributes::from(Attribute::Bold))
-            .build(),
-        square_brackets_style: StyleBuilder::new()
-            .attrs(Attributes::from(Attribute::Bold))
-            .build(),
-        key_style: StyleBuilder::new().fgc(Color::Cyan).build(),
-        string_value_style: StyleBuilder::new().fgc(Color::Green).build(),
-        number_value_style: StyleBuilder::new().build(),
-        boolean_value_style: StyleBuilder::new().build(),
-        null_value_style: StyleBuilder::new().fgc(Color::Grey).build(),
-        active_item_attribute: Attribute::Bold,
-        inactive_item_attribute: Attribute::Dim,
-        lines: Default::default(),
-        indent: args.indent,
-    };
-
-    let mut prompt = Jnv::try_new(
-        input_stream,
-        filter_editor,
-        hint_message,
-        suggestions,
-        json_theme,
+        text_editor::State {
+            texteditor: Default::default(),
+            history: Default::default(),
+            prefix: String::from("❯❯ "),
+            mask: Default::default(),
+            prefix_style: StyleBuilder::new().fgc(Color::Blue).build(),
+            active_char_style: StyleBuilder::new().bgc(Color::Magenta).build(),
+            inactive_char_style: StyleBuilder::new().build(),
+            edit_mode: args.edit_mode,
+            word_break_chars: HashSet::from(['.', '|', '(', ')', '[', ']']),
+            lines: Default::default(),
+        },
+        EditorTheme {
+            prefix: String::from("❯❯ "),
+            prefix_style: StyleBuilder::new().fgc(Color::Blue).build(),
+            active_char_style: StyleBuilder::new().bgc(Color::Magenta).build(),
+            inactive_char_style: StyleBuilder::new().build(),
+        },
+        EditorTheme {
+            prefix: String::from("▼"),
+            prefix_style: StyleBuilder::new()
+                .fgc(Color::Blue)
+                .attrs(Attributes::from(Attribute::Dim))
+                .build(),
+            active_char_style: StyleBuilder::new()
+                .attrs(Attributes::from(Attribute::Dim))
+                .build(),
+            inactive_char_style: StyleBuilder::new()
+                .attrs(Attributes::from(Attribute::Dim))
+                .build(),
+        },
+        listbox::State {
+            listbox: Listbox::from_displayable(Vec::<String>::new()),
+            cursor: String::from("❯ "),
+            active_item_style: Some(
+                StyleBuilder::new()
+                    .fgc(Color::Grey)
+                    .bgc(Color::Yellow)
+                    .build(),
+            ),
+            inactive_item_style: Some(StyleBuilder::new().fgc(Color::Grey).build()),
+            lines: Some(args.suggestions),
+        },
+        100,
+        50000,
         args.no_hint,
-    )?;
-    let _ = prompt.run()?;
+    )
+    .await?;
+
     Ok(())
 }
diff --git a/src/processor.rs b/src/processor.rs
new file mode 100644
index 0000000..df01def
--- /dev/null
+++ b/src/processor.rs
@@ -0,0 +1,144 @@
+use std::sync::Arc;
+
+use async_trait::async_trait;
+use crossterm::event::Event;
+use promkit::pane::Pane;
+use tokio::{sync::Mutex, task::JoinHandle};
+
+use crate::{PaneIndex, Renderer, EMPTY_PANE};
+pub mod init;
+pub use init::ViewProvider;
+pub mod monitor;
+pub mod spinner;
+
+#[derive(PartialEq)]
+enum State {
+    Idle,
+    Loading,
+    Processing,
+}
+
+#[async_trait]
+pub trait Visualizer: Send + Sync + 'static {
+    async fn content_to_copy(&self) -> String;
+    async fn create_init_pane(&mut self, area: (u16, u16)) -> Pane;
+    async fn create_pane_from_event(&mut self, area: (u16, u16), event: &Event) -> Pane;
+    async fn create_panes_from_query(
+        &mut self,
+        area: (u16, u16),
+        query: String,
+    ) -> (Option<Pane>, Option<Pane>);
+}
+
+pub struct Context {
+    state: State,
+    area: (u16, u16),
+    current_task: Option<JoinHandle<()>>,
+}
+
+impl Context {
+    pub fn new(area: (u16, u16)) -> Self {
+        Self {
+            state: State::Idle,
+            area,
+            current_task: None,
+        }
+    }
+}
+
+pub struct Processor {
+    shared: Arc<Mutex<Context>>,
+}
+
+impl Processor {
+    pub fn new(shared: Arc<Mutex<Context>>) -> Self {
+        Self { shared }
+    }
+
+    fn spawn_process_task(
+        &self,
+        query: String,
+        shared_visualizer: Arc<Mutex<impl Visualizer>>,
+        shared_renderer: Arc<Mutex<Renderer>>,
+    ) -> JoinHandle<()> {
+        let shared = self.shared.clone();
+        tokio::spawn(async move {
+            {
+                let mut shared_state = shared.lock().await;
+                shared_state.state = State::Processing;
+            }
+
+            let (maybe_guide, maybe_resp) = {
+                let shared_state = shared.lock().await;
+                let area = shared_state.area;
+                drop(shared_state);
+
+                let mut visualizer = shared_visualizer.lock().await;
+                visualizer.create_panes_from_query(area, query).await
+            };
+
+            // Set state to Idle to prevent overwriting by spinner frames in terminal.
+            {
+                let mut shared_state = shared.lock().await;
+                shared_state.state = State::Idle;
+            }
+            {
+                // TODO: error handling
+                let _ = shared_renderer.lock().await.update_and_draw([
+                    (
+                        PaneIndex::ProcessorGuide,
+                        maybe_guide.unwrap_or(EMPTY_PANE.to_owned()),
+                    ),
+                    (
+                        PaneIndex::Processor,
+                        maybe_resp.unwrap_or(EMPTY_PANE.to_owned()),
+                    ),
+                ]);
+            }
+        })
+    }
+
+    pub async fn render_on_resize(
+        &self,
+        shared_visualizer: Arc<Mutex<impl Visualizer>>,
+        area: (u16, u16),
+        query: String,
+        shared_renderer: Arc<Mutex<Renderer>>,
+    ) {
+        {
+            let mut shared_state = self.shared.lock().await;
+            shared_state.area = area;
+            if let Some(task) = shared_state.current_task.take() {
+                task.abort();
+            }
+        }
+
+        let process_task = self.spawn_process_task(query, shared_visualizer, shared_renderer);
+
+        {
+            let mut shared_state = self.shared.lock().await;
+            shared_state.current_task = Some(process_task);
+        }
+    }
+
+    pub async fn render_result(
+        &self,
+        shared_visualizer: Arc<Mutex<impl Visualizer>>,
+        query: String,
+        shared_renderer: Arc<Mutex<Renderer>>,
+    ) {
+        {
+            let mut shared_state = self.shared.lock().await;
+            if let Some(task) = shared_state.current_task.take() {
+                task.abort();
+            }
+        }
+
+        let process_task = self.spawn_process_task(query, shared_visualizer, shared_renderer);
+
+        {
+            let mut shared_state = self.shared.lock().await;
+            shared_state.current_task = Some(process_task);
+        }
+    }
+}
diff --git a/src/processor/init.rs b/src/processor/init.rs
new file mode 100644
index 0000000..6c570ca
--- /dev/null
+++ b/src/processor/init.rs
@@ -0,0 +1,56 @@
+use std::sync::Arc;
+
+use async_trait::async_trait;
+use tokio::sync::Mutex;
+
+use super::{Context, State, Visualizer};
+use crate::{PaneIndex, Renderer};
+
+#[async_trait]
+pub trait ViewProvider {
+    async fn provide(&mut self, item: &'static str) -> anyhow::Result<impl Visualizer>;
+}
+
+pub struct ViewInitializer {
+    shared: Arc<Mutex<Context>>,
+}
+
+impl ViewInitializer {
+    pub fn new(shared: Arc<Mutex<Context>>) -> Self {
+        Self { shared }
+    }
+
+    pub async fn initialize<'a, T: ViewProvider>(
+        &self,
+        provider: &'a mut T,
+        item: &'static str,
+        area: (u16, u16),
+        shared_renderer: Arc<Mutex<Renderer>>,
+    ) -> anyhow::Result<impl Visualizer + 'a> {
+        {
+            let mut shared_state = self.shared.lock().await;
+            if let Some(task) = shared_state.current_task.take() {
+                task.abort();
+            }
+            shared_state.state = State::Loading;
+        }
+
+        let mut visualizer = provider.provide(item).await?;
+        let pane = visualizer.create_init_pane(area).await;
+
+        // Set state to Idle to prevent overwriting by spinner frames in terminal.
+        {
+            let mut shared_state = self.shared.lock().await;
+            shared_state.state = State::Idle;
+        }
+        {
+            // TODO: error handling
+            let _ = shared_renderer
+                .lock()
+                .await
+                .update_and_draw([(PaneIndex::Processor, pane)]);
+        }
+
+        Ok(visualizer)
+    }
+}
diff --git a/src/processor/monitor.rs b/src/processor/monitor.rs
new file mode 100644
index 0000000..b72a68a
--- /dev/null
+++ b/src/processor/monitor.rs
@@ -0,0 +1,20 @@
+use std::sync::Arc;
+
+use tokio::sync::Mutex;
+
+use super::{Context, State};
+
+pub struct ContextMonitor {
+    shared: Arc<Mutex<Context>>,
+}
+
+impl ContextMonitor {
+    pub fn new(shared: Arc<Mutex<Context>>) -> Self {
+        Self { shared }
+    }
+
+    pub async fn is_idle(&self) -> bool {
+        let context = self.shared.lock().await;
+        context.state == State::Idle
+    }
+}
diff --git a/src/processor/spinner.rs b/src/processor/spinner.rs
new file mode 100644
index 0000000..42821b5
--- /dev/null
+++ b/src/processor/spinner.rs
@@ -0,0 +1,57 @@
+use std::sync::Arc;
+
+use promkit::pane::Pane;
+use tokio::{sync::Mutex, task::JoinHandle, time::Duration};
+
+use super::{Context, State};
+use crate::{PaneIndex, Renderer};
+
+const LOADING_FRAMES: [&str; 10] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
+
+pub struct SpinnerSpawner {
+    shared: Arc<Mutex<Context>>,
+}
+
+impl SpinnerSpawner {
+    pub fn new(shared: Arc<Mutex<Context>>) -> Self {
+        Self { shared }
+    }
+
+    pub fn spawn_spin_task(
+        &self,
+        shared_renderer: Arc<Mutex<Renderer>>,
+        spin_duration: Duration,
+    ) -> JoinHandle<()> {
+        let shared = self.shared.clone();
+        let mut frame_index = 0;
+        tokio::spawn(async move {
+            let mut interval = tokio::time::interval(spin_duration);
+            loop {
+                interval.tick().await;
+
+                {
+                    let shared_state = shared.lock().await;
+                    if shared_state.state == State::Idle {
+                        continue;
+                    }
+                }
+
+                frame_index = (frame_index + 1) % LOADING_FRAMES.len();
+
+                let pane = Pane::new(
+                    vec![promkit::grapheme::StyledGraphemes::from(
+                        LOADING_FRAMES[frame_index],
+                    )],
+                    0,
+                );
+                {
+                    // TODO: error handling
+                    let _ = shared_renderer
+                        .lock()
+                        .await
+                        .update_and_draw([(PaneIndex::Processor, pane)]);
+                }
+            }
+        })
+    }
+}
diff --git a/src/prompt.rs b/src/prompt.rs
new file mode 100644
index 0000000..a4037f6
--- /dev/null
+++ b/src/prompt.rs
@@ -0,0 +1,420 @@
+use std::{io, sync::Arc, time::Duration};
+
+use arboard::Clipboard;
+use crossterm::{
+    self, cursor,
+    event::{Event, EventStream, KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers},
+    execute,
+    style::Color,
+    terminal::{self, disable_raw_mode, enable_raw_mode},
+};
+use futures::StreamExt;
+use futures_timer::Delay;
+use promkit::{listbox, style::StyleBuilder, text, text_editor, PaneFactory};
+use tokio::{
+    sync::{mpsc, Mutex, RwLock},
+    task::JoinHandle,
+};
+
+use crate::{
+    Context, ContextMonitor, Editor, EditorTheme, IncrementalSearcher, PaneIndex, Processor,
+    Renderer, SearchProvider, SpinnerSpawner, ViewInitializer, ViewProvider, Visualizer,
+    EMPTY_PANE,
+};
+
+fn spawn_debouncer<T: Send + 'static>(
+    mut debounce_rx: mpsc::Receiver<T>,
+    last_tx: mpsc::Sender<T>,
+    duration: Duration,
+) -> tokio::task::JoinHandle<()> {
+    tokio::spawn(async move {
+        let mut last_query = None;
+        loop {
+            let delay = Delay::new(duration);
+            futures::pin_mut!(delay);
+
+            tokio::select! {
+                maybe_query = debounce_rx.recv() => {
+                    if let Some(query) = maybe_query {
+                        last_query = Some(query);
+                    } else {
+                        break;
+                    }
+                },
+                _ = delay => {
+                    if let Some(text) = last_query.take() {
+                        let _ = last_tx.send(text).await;
+                    }
+                },
+            }
+        }
+    })
+}
+
+fn copy_to_clipboard(content: &str) -> text::State {
+    match Clipboard::new() {
+        Ok(mut clipboard) => match clipboard.set_text(content) {
+            Ok(_) => text::State {
+                text: "Copied to clipboard".to_string(),
+                style: StyleBuilder::new().fgc(Color::Green).build(),
+            },
+            Err(e) => text::State {
+                text: format!("Failed to copy to clipboard: {}", e),
+                style: StyleBuilder::new().fgc(Color::Red).build(),
+            },
+        },
+        // arboard fails (in the specific environment like linux?) on Clipboard::new()
+        // suppress the errors (but still show them) not to break the prompt
+        // https://github.com/1Password/arboard/issues/153
+        Err(e) => text::State {
+            text: format!("Failed to setup clipboard: {}", e),
+            style: StyleBuilder::new().fgc(Color::Red).build(),
+        },
+    }
+}
+
+enum Focus {
+    Editor,
+    Processor,
+}
+
+#[allow(clippy::too_many_arguments)]
+pub async fn run<T: ViewProvider + SearchProvider>(
+    item: &'static str,
+    spin_duration: Duration,
+    query_debounce_duration: Duration,
+    resize_debounce_duration: Duration,
+    provider: &mut T,
+    text_editor_state: text_editor::State,
+    editor_focus_theme: EditorTheme,
+    editor_defocus_theme: EditorTheme,
+    listbox_state: listbox::State,
+    search_result_chunk_size: usize,
+    search_load_chunk_size: usize,
+    no_hint: bool,
+) -> anyhow::Result<()> {
+    enable_raw_mode()?;
+    execute!(io::stdout(), cursor::Hide)?;
+
+    let size = terminal::size()?;
+
+    let searcher = IncrementalSearcher::new(listbox_state, search_result_chunk_size);
+    let loading_suggestions_task = searcher.spawn_load_task(provider, item, search_load_chunk_size);
+    let editor = Editor::new(
+        text_editor_state,
+        searcher,
+        editor_focus_theme,
+        editor_defocus_theme,
+    );
+
+    let shared_renderer = Arc::new(Mutex::new(Renderer::try_init_draw(
+        [
+            editor.create_editor_pane(size.0, size.1),
+            EMPTY_PANE.to_owned(),
+            EMPTY_PANE.to_owned(),
+            EMPTY_PANE.to_owned(),
+            EMPTY_PANE.to_owned(),
+        ],
+        no_hint,
+    )?));
+
+    let ctx = Arc::new(Mutex::new(Context::new(size)));
+
+    let (last_query_tx, mut last_query_rx) = mpsc::channel(1);
+    let (debounce_query_tx, debounce_query_rx) = mpsc::channel(1);
+    let query_debouncer =
+        spawn_debouncer(debounce_query_rx, last_query_tx, query_debounce_duration);
+
+    let (last_resize_tx, mut last_resize_rx) = mpsc::channel::<(u16, u16)>(1);
+    let (debounce_resize_tx, debounce_resize_rx) = mpsc::channel(1);
+    let resize_debouncer =
+        spawn_debouncer(debounce_resize_rx, last_resize_tx, resize_debounce_duration);
+
+    let spinner_spawner = SpinnerSpawner::new(ctx.clone());
+    let spinning = spinner_spawner.spawn_spin_task(shared_renderer.clone(), spin_duration);
+
+    let mut focus = Focus::Editor;
+    let (editor_event_tx, mut editor_event_rx) = mpsc::channel::<Event>(1);
+    let (processor_event_tx, mut processor_event_rx) = mpsc::channel::<Event>(1);
+
+    let (editor_copy_tx, mut editor_copy_rx) = mpsc::channel::<()>(1);
+    let (processor_copy_tx, mut processor_copy_rx) = mpsc::channel::<()>(1);
+
+    let (editor_focus_tx, mut editor_focus_rx) = mpsc::channel::<bool>(1);
+
+    let mut text_diff = [editor.text(), editor.text()];
+    let shared_editor = Arc::new(RwLock::new(editor));
+    let processor = Processor::new(ctx.clone());
+    let context_monitor = ContextMonitor::new(ctx.clone());
+    let initializer = ViewInitializer::new(ctx.clone());
+    let initializing = initializer.initialize(provider, item, size, shared_renderer.clone());
+
+    let main_task: JoinHandle<anyhow::Result<()>> = {
+        let mut stream = EventStream::new();
+        let shared_renderer = shared_renderer.clone();
+        tokio::spawn(async move {
+            'main: loop {
+                tokio::select! {
+                    Some(Ok(event)) = stream.next() => {
+                        match event {
+                            Event::Resize(width, height) => {
+                                debounce_resize_tx.send((width, height)).await?;
+                            },
+                            Event::Key(KeyEvent {
+                                code: KeyCode::Char('c'),
+                                modifiers: KeyModifiers::CONTROL,
+                                kind: KeyEventKind::Press,
+                                state: KeyEventState::NONE,
+                            }) => {
+                                break 'main
+                            },
+                            Event::Key(KeyEvent {
+                                code: KeyCode::Char('q'),
+                                modifiers: KeyModifiers::CONTROL,
+                                kind: KeyEventKind::Press,
+                                state: KeyEventState::NONE,
+                            }) => {
+                                editor_copy_tx.send(()).await?;
+                            },
+                            Event::Key(KeyEvent {
+                                code: KeyCode::Char('o'),
+                                modifiers: KeyModifiers::CONTROL,
+                                kind: KeyEventKind::Press,
+                                state: KeyEventState::NONE,
+                            }) => {
+                                let mut pane = EMPTY_PANE.to_owned();
+                                if context_monitor.is_idle().await {
+                                    processor_copy_tx.send(()).await?;
+                                } else {
+                                    let size = terminal::size()?;
+                                    pane = text::State {
+                                        text: "Failed to copy while rendering is in progress.".to_string(),
+                                        style: StyleBuilder::new().fgc(Color::Yellow).build(),
+                                    }.create_pane(size.0, size.1);
+                                }
+                                {
+                                    shared_renderer.lock().await.update_and_draw([
+                                        (PaneIndex::Guide, pane),
+                                    ])?;
+                                }
+                            },
+                            Event::Key(KeyEvent {
+                                code: KeyCode::Down,
+                                modifiers: KeyModifiers::SHIFT,
+                                kind: KeyEventKind::Press,
+                                state: KeyEventState::NONE,
+                            }) | Event::Key(KeyEvent {
+                                code: KeyCode::Up,
+                                modifiers: KeyModifiers::SHIFT,
+                                kind: KeyEventKind::Press,
+                                state: KeyEventState::NONE,
+                            }) => {
+                                match focus {
+                                    Focus::Editor => {
+                                        let mut pane = EMPTY_PANE.to_owned();
+                                        if context_monitor.is_idle().await {
+                                            focus = Focus::Processor;
+                                            editor_focus_tx.send(false).await?;
+                                        } else {
+                                            let size = terminal::size()?;
+                                            pane = text::State {
+                                                text: "Failed to switch pane while rendering is in progress.".to_string(),
+                                                style: StyleBuilder::new().fgc(Color::Yellow).build(),
+                                            }.create_pane(size.0, size.1);
+                                        }
+                                        {
+                                            shared_renderer.lock().await.update_and_draw([
+                                                (PaneIndex::Guide, pane),
+                                            ])?;
+                                        }
+                                    },
+                                    Focus::Processor => {
+                                        focus = Focus::Editor;
+                                        editor_focus_tx.send(true).await?;
+                                    },
+                                }
+                            },
+                            event => {
+                                match focus {
+                                    Focus::Editor => {
+                                        editor_event_tx.send(event).await?;
+                                    },
+                                    Focus::Processor => {
+                                        processor_event_tx.send(event).await?;
+                                    },
+                                }
+                            },
+                        }
+                    },
+                    else => {
+                        break 'main;
+                    }
+                }
+            }
+            Ok(())
+        })
+    };
+
+    let editor_task: JoinHandle<anyhow::Result<()>> = {
+        let shared_renderer = shared_renderer.clone();
+        let shared_editor = shared_editor.clone();
+        tokio::spawn(async move {
+            loop {
+                tokio::select! {
+                    Some(focus) = editor_focus_rx.recv() => {
+                        let (editor_pane, guide_pane) = {
+                            let mut editor = shared_editor.write().await;
+                            if focus {
+                                editor.focus();
+                            } else {
+                                editor.defocus();
+                            }
+                            (
+                                editor.create_editor_pane(size.0, size.1),
+                                editor.create_guide_pane(size.0, size.1),
+                            )
+                        };
+                        {
+                            shared_renderer.lock().await.update_and_draw([
+                                (PaneIndex::Editor, editor_pane),
+                                (PaneIndex::Guide, guide_pane),
+                            ])?;
+                        }
+                    }
+                    Some(()) = editor_copy_rx.recv() => {
+                        let text = {
+                            let editor = shared_editor.write().await;
+                            editor.text()
+                        };
+                        let guide = copy_to_clipboard(&text);
+                        let size = terminal::size()?;
+                        let pane = guide.create_pane(size.0, size.1);
+                        {
+                            shared_renderer.lock().await.update_and_draw([
+                                (PaneIndex::Guide, pane),
+                            ])?;
+                        }
+                    }
+                    Some(event) = editor_event_rx.recv() => {
+                        let size = terminal::size()?;
+                        let (editor_pane, guide_pane, searcher_pane) = {
+
+                            let mut editor = shared_editor.write().await;
+                            editor.operate(&event).await?;
+
+                            let current_text = editor.text();
+                            if current_text != text_diff[1] {
+                                debounce_query_tx.send(current_text.clone()).await?;
+                                text_diff[0] = text_diff[1].clone();
+                                text_diff[1] = current_text;
+                            }
+                            (
+                                editor.create_editor_pane(size.0, size.1),
+                                editor.create_guide_pane(size.0, size.1),
+                                editor.create_searcher_pane(size.0, size.1),
+                            )
+                        };
+                        {
+                            shared_renderer.lock().await.update_and_draw([
+                                (PaneIndex::Editor, editor_pane),
+                                (PaneIndex::Guide, guide_pane),
+                                (PaneIndex::Search, searcher_pane),
+                            ])?;
+                        }
+                    }
+                    else => {
+                        break
+                    }
+                }
+            }
+            Ok(())
+        })
+    };
+
+    let processor_task: JoinHandle<anyhow::Result<()>> = {
+        let shared_renderer = shared_renderer.clone();
+        let shared_editor = shared_editor.clone();
+        let visualizer = initializing.await?;
+        let shared_visualizer = Arc::new(Mutex::new(visualizer));
+        tokio::spawn(async move {
+            loop {
+                tokio::select! {
+                    Some(()) = processor_copy_rx.recv() => {
+                        let visualizer = shared_visualizer.lock().await;
+                        let guide = copy_to_clipboard(&visualizer.content_to_copy().await);
+                        let size = terminal::size()?;
+                        let pane = guide.create_pane(size.0, size.1);
+                        {
+                            shared_renderer.lock().await.update_and_draw([
+                                (PaneIndex::Guide, pane),
+                            ])?;
+                        }
+                    }
+                    Some(event) = processor_event_rx.recv() => {
+                        let pane = {
+                            let mut visualizer = shared_visualizer.lock().await;
+                            visualizer.create_pane_from_event((size.0, size.1), &event).await
+                        };
+                        {
+                            shared_renderer.lock().await.update_and_draw([
+                                (PaneIndex::Processor, pane),
+                            ])?;
+                        }
+                    }
+                    Some(query) = last_query_rx.recv() => {
+                        processor.render_result(
+                            shared_visualizer.clone(),
+                            query,
+                            shared_renderer.clone(),
+                        ).await;
+                    }
+                    Some(area) = last_resize_rx.recv() => {
+                        let (editor_pane, guide_pane, searcher_pane) = {
+                            let editor = shared_editor.read().await;
+                            (
+                                editor.create_editor_pane(size.0, size.1),
+                                editor.create_guide_pane(size.0, size.1),
+                                editor.create_searcher_pane(size.0, size.1),
+                            )
+                        };
+                        {
+                            shared_renderer.lock().await.update_and_draw([
+                                (PaneIndex::Editor, editor_pane),
+                                (PaneIndex::Guide, guide_pane),
+                                (PaneIndex::Search, searcher_pane),
+                            ])?;
+                        }
+                        let text = {
+                            let editor = shared_editor.read().await;
+                            editor.text()
+                        };
+                        processor.render_on_resize(
+                            shared_visualizer.clone(),
+                            area,
+                            text,
+                            shared_renderer.clone(),
+                        ).await;
+                    }
+                    else => {
+                        break
+                    }
+                }
+            }
+            Ok(())
+        })
+    };
+
+    main_task.await??;
+
+    loading_suggestions_task.abort();
+    spinning.abort();
+    query_debouncer.abort();
+    resize_debouncer.abort();
+    editor_task.abort();
+    processor_task.abort();
+
+    execute!(io::stdout(), cursor::Show)?;
+    disable_raw_mode()?;
+
+    Ok(())
+}
diff --git a/src/render.rs b/src/render.rs
new file mode 100644
index 0000000..71af51c
--- /dev/null
+++ b/src/render.rs
@@ -0,0 +1,50 @@
+use std::sync::LazyLock;
+
+use crossterm::{self, cursor};
+use promkit::{pane::Pane, terminal::Terminal};
+
+#[derive(Debug, PartialEq)]
+pub enum PaneIndex {
+    Editor = 0,
+    Guide = 1,
+    ProcessorGuide = 2,
+    Search = 3,
+    Processor = 4,
+}
+
+pub static EMPTY_PANE: LazyLock<Pane> = LazyLock::new(|| Pane::new(vec![], 0));
+const PANE_SIZE: usize = PaneIndex::Processor as usize + 1;
+
+pub struct Renderer {
+    no_hint: bool,
+    terminal: Terminal,
+    panes: [Pane; PANE_SIZE],
+}
+
+impl Renderer {
+    pub fn try_init_draw(init_panes: [Pane; PANE_SIZE], no_hint: bool) -> anyhow::Result<Self> {
+        let mut ret = Self {
+            no_hint,
+            terminal: Terminal {
+                position: cursor::position()?,
+            },
+            panes: init_panes,
+        };
+        ret.terminal.draw(&ret.panes)?;
+        Ok(ret)
+    }
+
+    pub fn update_and_draw<I: IntoIterator<Item = (PaneIndex, Pane)>>(
+        &mut self,
+        iter: I,
+    ) -> anyhow::Result<()> {
+        for (index, pane) in iter {
+            if self.no_hint && (index == PaneIndex::Guide || index == PaneIndex::ProcessorGuide) {
+                continue;
+            }
+            self.panes[index as usize] = pane;
+        }
+        self.terminal.draw(&self.panes)?;
+        Ok(())
+    }
+}
diff --git a/src/search.rs b/src/search.rs
new file mode 100644
index 0000000..e9c4779
--- /dev/null
+++ b/src/search.rs
@@ -0,0 +1,170 @@
+use std::{collections::BTreeSet, sync::Arc};
+
+use anyhow::anyhow;
+use async_trait::async_trait;
+use promkit::{
+    listbox::{self, Listbox},
+    pane::Pane,
+    PaneFactory,
+};
+use tokio::{
+    sync::{Mutex, RwLock},
+    task::JoinHandle,
+};
+
+#[async_trait]
+pub trait SearchProvider: Clone + Send + 'static {
+    async fn provide(
+        &mut self,
+        item: &str,
+    ) -> anyhow::Result<Box<dyn Iterator<Item = String> + Send>>;
+}
+
+#[derive(Clone, Default)]
+pub struct LoadState {
+    pub loaded: bool,
+    pub loaded_item_len: usize,
+}
+
+pub struct StartSearchResult {
+    pub head_item: Option<String>,
+    pub load_state: LoadState,
+}
+
+pub struct IncrementalSearcher {
+    shared_set: Arc<Mutex<BTreeSet<String>>>,
+    shared_load_state: Arc<RwLock<LoadState>>,
+    state: listbox::State,
+    search_result_chunk_size: usize,
+    search_chunk_remaining: Vec<String>,
+}
+
+impl IncrementalSearcher {
+    pub fn new(state: listbox::State, search_result_chunk_size: usize) -> Self {
+        Self {
+            shared_set: Default::default(),
+            shared_load_state: Default::default(),
+            state,
+            search_result_chunk_size,
+            search_chunk_remaining: Default::default(),
+        }
+    }
+
+    pub fn spawn_load_task<T: SearchProvider>(
+        &self,
+        provider: &mut T,
+        item: &'static str,
+        chunk_size: usize,
+    ) -> JoinHandle<anyhow::Result<()>> {
+        let shared_set = self.shared_set.clone();
+        let shared_load_state = self.shared_load_state.clone();
+        let mut provider = provider.clone();
+        tokio::spawn(async move {
+            let mut batch = Vec::with_capacity(chunk_size);
+            let iter = provider.provide(item).await?;
+
+            for v in iter {
+                batch.push(v);
+
+                if batch.len() >= chunk_size {
+                    let mut set = shared_set.lock().await;
+                    for item in batch.drain(..) {
+                        set.insert(item);
+                    }
+                    let mut state = shared_load_state.write().await;
+                    state.loaded_item_len += chunk_size;
+                }
+            }
+
+            let remaining = batch.len();
+            if !batch.is_empty() {
+                let mut set = shared_set.lock().await;
+                for item in batch {
+                    set.insert(item);
+                }
+            }
+
+            let mut state = shared_load_state.write().await;
+            state.loaded = true;
+            state.loaded_item_len += remaining;
+            Ok(())
+        })
+    }
+
+    pub fn up(&mut self) {
+        self.state.listbox.backward();
+    }
+
+    pub fn down_with_load(&mut self) {
+        self.state.listbox.forward();
+        if self
+            .state
+            .listbox
+            .len()
+            .saturating_sub(self.state.listbox.position())
+            < self.state.lines.unwrap_or(1)
+        {
+            self.load_more();
+        }
+    }
+
+    pub fn get_current_item(&self) -> String {
+        self.state.listbox.get().to_string()
+    }
+
+    pub fn create_pane(&self, width: u16, height: u16) -> Pane {
+        self.state.create_pane(width, height)
+    }
+
+    pub fn leave_search(&mut self) {
+        self.state.listbox = Listbox::from_displayable(Vec::<String>::new());
+        self.search_chunk_remaining = Vec::<String>::new();
+    }
+
+    pub fn start_search(&mut self, prefix: &str) -> anyhow::Result<StartSearchResult> {
+        match (
+            self.shared_load_state.try_read(),
+            self.shared_set.try_lock(),
+        ) {
+            (Ok(state), Ok(set)) => {
+                let mut items: Vec<_> = set
+                    .iter()
+                    .filter(|p| p.starts_with(prefix))
+                    .cloned()
+                    .collect();
+                if items.is_empty() {
+                    return Ok(StartSearchResult {
+                        head_item: None,
+                        load_state: state.clone(),
+                    });
+                }
+                let used = items
+                    .drain(..self.search_result_chunk_size.min(items.len()))
+                    .collect::<Vec<_>>();
+                self.search_chunk_remaining = items;
+                self.state.listbox = Listbox::from_displayable(used);
+                Ok(StartSearchResult {
+                    head_item: Some(self.state.listbox.get().to_string()),
+                    load_state: state.clone(),
+                })
+            }
+            (Err(_), _) | (_, Err(_)) => Err(anyhow!(
+                "Failed to acquire lock for suggestions. Please try again."
+            )),
+        }
+    }
+
+    fn load_more(&mut self) {
+        if self.search_chunk_remaining.is_empty() {
+            return;
+        }
+        let items = self.search_chunk_remaining.drain(
+            ..self
+                .search_result_chunk_size
+                .min(self.search_chunk_remaining.len()),
+        );
+        for item in items {
+            self.state.listbox.push_string(item);
+        }
+    }
+}
diff --git a/src/trie.rs b/src/trie.rs
deleted file mode 100644
index 560f4d4..0000000
--- a/src/trie.rs
+++ /dev/null
@@ -1,109 +0,0 @@
-use radix_trie::{Trie, TrieCommon};
-
-use promkit::serde_json;
-
-#[derive(Default, Clone)]
-pub struct FilterTrie(Trie<String, Vec<serde_json::Value>>);
-
-impl FilterTrie {
-    pub fn insert(&mut self, query: &str, json_nodes: Vec<serde_json::Value>) {
-        self.0.insert(query.to_string(), json_nodes);
-    }
-
-    pub fn exact_search(&self, query: &str) -> Option<&Vec<serde_json::Value>> {
-        self.0.get(query)
-    }
-
-    pub fn prefix_search(&self, query: &str) -> Option<&Vec<serde_json::Value>> {
-        self.0
-            .get_ancestor(query)
-            .and_then(|subtrie| subtrie.value())
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    mod exact_search {
-        use super::*;
-        use serde_json::json;
-
-        #[test]
-        fn test_exact_match() {
-            let mut trie = FilterTrie::default();
-            trie.insert("apple", vec![json!({"type": "fruit"})]);
-            trie.insert("app", vec![json!({"type": "abbreviation"})]);
-
-            let result = trie.exact_search("app");
-            assert!(result.is_some());
-            let values = result.unwrap();
-            assert_eq!(values.len(), 1);
-            assert_eq!(values[0], json!({"type": "abbreviation"}));
-        }
-
-        #[test]
-        fn test_no_match() {
-            let mut trie = FilterTrie::default();
-            trie.insert("apple", vec![json!({"type": "fruit"})]);
-            trie.insert("app", vec![json!({"type": "abbreviation"})]);
-
-            let result = trie.exact_search("application");
-            assert!(result.is_none());
-        }
-    }
-
-    mod prefix_search {
-        use super::*;
-        use serde_json::json;
-
-        #[test]
-        fn test_with_exact_prefix() {
-            let mut trie = FilterTrie::default();
-            trie.insert("apple", vec![json!({"type": "fruit"})]);
-            trie.insert("app", vec![json!({"type": "abbreviation"})]);
-
-            let result = trie.prefix_search("app");
-            assert!(result.is_some());
-            let values = result.unwrap();
-            assert_eq!(values.len(), 1);
-            assert_eq!(values[0], json!({"type": "abbreviation"}));
-        }
-
-        #[test]
-        fn test_with_longer_query_than_keys() {
-            let mut trie = FilterTrie::default();
-            trie.insert("apple", vec![json!({"type": "fruit"})]);
-            trie.insert("app", vec![json!({"type": "abbreviation"})]);
-
-            let result = trie.prefix_search("application");
-            assert!(result.is_some());
-            let values = result.unwrap();
-            assert_eq!(values.len(), 1);
-            assert_eq!(values[0], json!({"type": "abbreviation"}));
-        }
-
-        #[test]
-        fn test_with_full_key_match() {
-            let mut trie = FilterTrie::default();
-            trie.insert("apple", vec![json!({"type": "fruit"})]);
-            trie.insert("app", vec![json!({"type": "abbreviation"})]);
-
-            let result = trie.prefix_search("apple");
-            assert!(result.is_some());
-            let values = result.unwrap();
-            assert_eq!(values.len(), 1);
-            assert_eq!(values[0], json!({"type": "fruit"}));
-        }
-
-        #[test]
-        fn test_with_shorter_query_than_any_key() {
-            let mut trie = FilterTrie::default();
-            trie.insert("apple", vec![json!({"type": "fruit"})]);
-            trie.insert("app", vec![json!({"type": "abbreviation"})]);
-
-            let result = trie.prefix_search("ap");
-            assert!(result.is_none());
-        }
-    }
-}


Test patch: