Repository: ynqa/jnv (language: rust)

Issue:
Feature Request: Support JSON stream
It would be nice if jnv supported sequences of json objects separate by newlines the way jq does:

``` json
{ "a": 123 }
{ "a": 123 }
```


Note: [fx](https://github.com/antonmedv/fx) also supports this.


Discussion / hints:
This would be really useful for inspecting Caddy logs.
Ditto useful for our use cases. 

We're talking about JSON Lines, yes? https://jsonlines.org

@robpottify The objects are delimited by newlines, but `jq` allows a single object to span multiple lines:

``` json
{
  "a": 123
}
{
  "a": 321
}
```

**edit:** `jq` doesn't even require the newline:

``` json
{
  "a": 123
} {
  "a": 321
}
```
Ah ok, so subtly different as JSON Lines prescribes newlines between records.

Should I raise a separate gh issue ticket for JSON Lines, or are they similar enough to be prioritised as one feature?
@robpottify JSON lines is a subset of JSON streams.

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 980f815..6c14d02 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -249,9 +249,9 @@ dependencies = [
 
 [[package]]
 name = "fastrand"
-version = "2.0.1"
+version = "2.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
+checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984"
 
 [[package]]
 name = "filedescriptor"
@@ -303,9 +303,9 @@ dependencies = [
 
 [[package]]
 name = "indexmap"
-version = "2.2.5"
+version = "2.2.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4"
+checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
 dependencies = [
  "equivalent",
  "hashbrown",
@@ -328,9 +328,9 @@ checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
 
 [[package]]
 name = "j9"
-version = "0.1.2"
+version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "072f88a49bd789ab58efb20864a55e840a3629241988df3aad349b051052fa1a"
+checksum = "6c2876f6d536ef88276de82d5c4c76a463b9f7ebaee288b49284aeacfca7b699"
 dependencies = [
  "j9-sys",
  "thiserror",
@@ -338,9 +338,9 @@ dependencies = [
 
 [[package]]
 name = "j9-sys"
-version = "0.1.2"
+version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e8657c612fdec9ce223e88a462c1119367c9640951e2801b7715b4513ba4001"
+checksum = "fe8caa9c5f8d1b56e4191614bed237d2f9081d933ac3884cafab1100f37d0afd"
 dependencies = [
  "anyhow",
  "autotools",
@@ -350,7 +350,7 @@ dependencies = [
 
 [[package]]
 name = "jnv"
-version = "0.1.3"
+version = "0.2.0"
 dependencies = [
  "anyhow",
  "clap",
@@ -484,9 +484,9 @@ dependencies = [
 
 [[package]]
 name = "prettyplease"
-version = "0.2.16"
+version = "0.2.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5"
+checksum = "8d3928fb5db768cb86f891ff014f0144589297e3c6a1aba6ed7cecfdace270c7"
 dependencies = [
  "proc-macro2",
  "syn",
@@ -503,10 +503,11 @@ dependencies = [
 
 [[package]]
 name = "promkit"
-version = "0.3.1"
+version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53726058b89f75dcae42ddc26e3f155c86e585b7e41fde6f720a25cbed38581e"
+checksum = "dfa31233c1a91cf9b5e8753a57b59a3bcca559f45f38da508857fb866635ab92"
 dependencies = [
+ "anyhow",
  "crossterm",
  "indexmap",
  "radix_trie",
@@ -546,9 +547,9 @@ dependencies = [
 
 [[package]]
 name = "regex"
-version = "1.10.3"
+version = "1.10.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15"
+checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
 dependencies = [
  "aho-corasick",
  "memchr",
@@ -695,9 +696,9 @@ checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01"
 
 [[package]]
 name = "syn"
-version = "2.0.53"
+version = "2.0.55"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7383cd0e49fff4b6b90ca5670bfd3e9d6a733b3f90c686605aa7eec8c4996032"
+checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0"
 dependencies = [
  "proc-macro2",
  "quote",
diff --git a/Cargo.toml b/Cargo.toml
index 3439fde..4139a9e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "jnv"
-version = "0.1.3"
+version = "0.2.0"
 authors = ["ynqa <un.pensiero.vano@gmail.com>"]
 edition = "2021"
 description = "JSON navigator and interactive filter leveraging jq"
@@ -12,8 +12,8 @@ readme = "README.md"
 anyhow = "1.0.80"
 clap = { version = "4.5.1", features = ["derive"] }
 gag = "1.0.0"
-j9 = "0.1.2"
-promkit = "0.3.1"
+j9 = "0.1.3"
+promkit = "0.3.2"
 radix_trie = "0.2.1"
 
 # The profile that 'cargo dist' will build with
diff --git a/README.md b/README.md
index 3d6f9b1..143c6bb 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,12 @@ and [jiq](https://github.com/fiatjaf/jiq).
 
 - Interactive JSON viewer and `jq` filter editor
   - Syntax highlighting for JSON
-- Accept JSON from stdin, file, URL
+- Capable of accommodating various format
+  - Input: File, Stdin
+  - Data: A JSON or multiple JSON structures
+    that can be deserialized with 
+    [StreamDeserializer](https://docs.rs/serde_json/latest/serde_json/struct.StreamDeserializer.html),
+    such as [JSON Lines](https://jsonlines.org/)
 - Auto-completion for the filter
   - Only supports:
     - [Identity](https://jqlang.github.io/jq/manual/#identity)
diff --git a/src/jnv.rs b/src/jnv.rs
index 739e745..19a4497 100644
--- a/src/jnv.rs
+++ b/src/jnv.rs
@@ -1,16 +1,17 @@
-use std::cell::RefCell;
+use std::{cell::RefCell, rc::Rc};
 
-use anyhow::{anyhow, Result};
+use anyhow::Result;
 use gag::Gag;
 
 use promkit::{
     crossterm::{
         event::Event,
-        style::{Attribute, Attributes, Color},
+        style::{Attribute, Attributes, Color, ContentStyle},
     },
-    json::{self, JsonBundle, JsonNode, JsonPathSegment},
+    json::{self, JsonNode, JsonPathSegment, JsonStream},
     keymap::KeymapManager,
-    listbox, serde_json,
+    listbox,
+    serde_json::{self, Deserializer},
     snapshot::Snapshot,
     style::StyleBuilder,
     suggest::Suggest,
@@ -23,7 +24,7 @@ mod trie;
 use trie::QueryTrie;
 
 pub struct Jnv {
-    input_json: String,
+    input_json_stream: Vec<serde_json::Value>,
     expand_depth: Option<usize>,
     no_hint: bool,
 
@@ -31,42 +32,46 @@ pub struct Jnv {
     hint_message_renderer: text::Renderer,
     suggest: Suggest,
     suggest_renderer: listbox::Renderer,
-    json_bundle_renderer: json::bundle::Renderer,
+    json_renderer: json::Renderer,
     keymap: KeymapManager<self::render::Renderer>,
 }
 
 impl Jnv {
     pub fn try_new(
-        input_json: String,
+        input_json_str: String,
         expand_depth: Option<usize>,
         no_hint: bool,
         edit_mode: text_editor::Mode,
         indent: usize,
         suggestion_list_length: usize,
     ) -> Result<Self> {
-        let kinds = JsonNode::try_new(input_json.clone(), None)?.flatten_visibles();
-        let full = kinds.iter().filter_map(|kind| kind.path()).map(|segments| {
-            if segments.is_empty() {
-                ".".to_string()
-            } else {
-                segments
-                    .iter()
-                    .map(|segment| match segment {
-                        JsonPathSegment::Key(key) => {
-                            if key.contains('.') || key.contains('-') || key.contains('@') {
-                                format!(".\"{}\"", key)
-                            } else {
-                                format!(".{}", key)
+        let stream = deserialize_json(&input_json_str)?;
+        let all_kinds = JsonStream::new(stream.clone(), None).flatten_kinds();
+        let suggestions = all_kinds
+            .iter()
+            .filter_map(|kind| kind.path())
+            .map(|segments| {
+                if segments.is_empty() {
+                    ".".to_string()
+                } else {
+                    segments
+                        .iter()
+                        .map(|segment| match segment {
+                            JsonPathSegment::Key(key) => {
+                                if key.contains('.') || key.contains('-') || key.contains('@') {
+                                    format!(".\"{}\"", key)
+                                } else {
+                                    format!(".{}", key)
+                                }
                             }
-                        }
-                        JsonPathSegment::Index(index) => format!("[{}]", index),
-                    })
-                    .collect::<String>()
-            }
-        });
+                            JsonPathSegment::Index(index) => format!("[{}]", index),
+                        })
+                        .collect::<String>()
+                }
+            });
 
         Ok(Self {
-            input_json: input_json.clone(),
+            input_json_stream: stream.clone(),
             expand_depth,
             no_hint,
             query_editor_renderer: text_editor::Renderer {
@@ -87,7 +92,7 @@ impl Jnv {
                     .attrs(Attributes::from(Attribute::Bold))
                     .build(),
             },
-            suggest: Suggest::from_iter(full),
+            suggest: Suggest::from_iter(suggestions),
             suggest_renderer: listbox::Renderer {
                 listbox: listbox::Listbox::from_iter(Vec::<String>::new()),
                 cursor: String::from("❯ "),
@@ -100,20 +105,8 @@ impl Jnv {
             },
             keymap: KeymapManager::new("default", self::keymap::default)
                 .register("on_suggest", self::keymap::on_suggest),
-            json_bundle_renderer: json::bundle::Renderer {
-                bundle: json::JsonBundle::new([JsonNode::try_new(
-                    j9::run(".", &input_json)
-                        .map_err(|_| {
-                            anyhow!(format!(
-                                "jq error with program: '.', input: {}",
-                                &input_json
-                            ))
-                        })?
-                        .first()
-                        .map(|s| s.as_str())
-                        .ok_or_else(|| anyhow!("No data found"))?,
-                    expand_depth,
-                )?]),
+            json_renderer: json::Renderer {
+                stream: JsonStream::new(stream, expand_depth),
                 theme: json::Theme {
                     curly_brackets_style: StyleBuilder::new()
                         .attrs(Attributes::from(Attribute::Bold))
@@ -135,149 +128,187 @@ impl Jnv {
         })
     }
 
+    fn update_hint_message(
+        &self,
+        renderer: &mut self::render::Renderer,
+        text: String,
+        style: ContentStyle,
+    ) {
+        if !self.no_hint {
+            renderer
+                .hint_message_snapshot
+                .after_mut()
+                .replace(text::Renderer { text, style })
+        }
+    }
+
+    fn evaluate(
+        &self,
+        event: &Event,
+        renderer: &mut Box<dyn Renderer + 'static>,
+        trie: RefCell<QueryTrie>,
+    ) -> promkit::Result<PromptSignal> {
+        let renderer = self::render::Renderer::cast_mut(renderer.as_mut())?;
+        let signal = match renderer.keymap.get() {
+            Some(f) => f(event, renderer),
+            None => Ok(PromptSignal::Quit),
+        }?;
+        let completed = renderer
+            .query_editor_snapshot
+            .after()
+            .texteditor
+            .text_without_cursor()
+            .to_string();
+
+        if completed
+            != renderer
+                .query_editor_snapshot
+                .borrow_before()
+                .texteditor
+                .text_without_cursor()
+                .to_string()
+        {
+            renderer.hint_message_snapshot.reset_after_to_init();
+
+            // libjq writes to the console when an internal error occurs.
+            //
+            // e.g.
+            // ```
+            // let _ = j9::run(". | select(.number == invalid_no_quote)", "{}");
+            // jq: error: invalid_no_quote/0 is not defined at <top-level>, line 1:
+            //     . | select(.number == invalid_no_quote)
+            // ```
+            //
+            // While errors themselves are not an issue,
+            // they interfere with the console output handling mechanism
+            // in promkit and qjq (e.g., causing line numbers to shift).
+            // Therefore, we'll ignore console output produced inside j9::run.
+            //
+            // It's possible that this could be handled
+            // within github.com/ynqa/j9, but for now,
+            // we'll proceed with this workaround.
+            //
+            // For reference, the functionality of a quiet mode in libjq is
+            // also being discussed at https://github.com/jqlang/jq/issues/1225.
+            let ignore_err = Gag::stderr().unwrap();
+
+            let mut flatten_ret = Vec::<String>::new();
+            for v in &self.input_json_stream {
+                let inner_ret: Vec<String> = match j9::run(&completed, &v.to_string()) {
+                    Ok(ret) => ret,
+                    Err(_e) => {
+                        self.update_hint_message(
+                            renderer,
+                            format!("Failed to execute jq query '{}'", &completed),
+                            StyleBuilder::new()
+                                .fgc(Color::Red)
+                                .attrs(Attributes::from(Attribute::Bold))
+                                .build(),
+                        );
+                        if let Some(searched) = trie.borrow().prefix_search_value(&completed) {
+                            renderer.json_snapshot.after_mut().stream =
+                                JsonStream::new(searched.clone(), self.expand_depth);
+                        }
+                        return Ok(signal);
+                    }
+                };
+                flatten_ret.extend(inner_ret);
+            }
+            drop(ignore_err);
+
+            if flatten_ret.is_empty() {
+                self.update_hint_message(
+                    renderer,
+                    format!(
+                        "JSON query ('{}') was executed, but no results were returned.",
+                        &completed
+                    ),
+                    StyleBuilder::new()
+                        .fgc(Color::Red)
+                        .attrs(Attributes::from(Attribute::Bold))
+                        .build(),
+                );
+                if let Some(searched) = trie.borrow().prefix_search_value(&completed) {
+                    renderer.json_snapshot.after_mut().stream =
+                        JsonStream::new(searched.clone(), self.expand_depth);
+                }
+            } else {
+                match deserialize_json(&flatten_ret.join("\n")) {
+                    Ok(jsonl) => {
+                        let stream = JsonStream::new(jsonl.clone(), self.expand_depth);
+
+                        let is_null = stream
+                            .roots()
+                            .iter()
+                            .all(|node| node == &JsonNode::Leaf(serde_json::Value::Null));
+                        if is_null {
+                            self.update_hint_message(
+                                renderer,
+                                format!("JSON query resulted in 'null', which may indicate a typo or incorrect query: '{}'", &completed),
+                                StyleBuilder::new()
+                                    .fgc(Color::Yellow)
+                                    .attrs(Attributes::from(Attribute::Bold))
+                                    .build(),
+                            );
+                            if let Some(searched) = trie.borrow().prefix_search_value(&completed) {
+                                renderer.json_snapshot.after_mut().stream =
+                                    JsonStream::new(searched.clone(), self.expand_depth);
+                            }
+                        } else {
+                            // SUCCESS!
+                            trie.borrow_mut().insert(&completed, jsonl);
+                            renderer.json_snapshot.after_mut().stream = stream;
+                        }
+                    }
+                    Err(e) => {
+                        self.update_hint_message(
+                            renderer,
+                            format!("Failed to parse query result for viewing: {}", e),
+                            StyleBuilder::new()
+                                .fgc(Color::Red)
+                                .attrs(Attributes::from(Attribute::Bold))
+                                .build(),
+                        );
+                        if let Some(searched) = trie.borrow().prefix_search_value(&completed) {
+                            renderer.json_snapshot.after_mut().stream =
+                                JsonStream::new(searched.clone(), self.expand_depth);
+                        }
+                    }
+                }
+                // flatten_ret.is_empty()
+            }
+            // before != completed
+        }
+        Ok(signal)
+    }
+
     pub fn prompt(self) -> Result<Prompt<String>> {
-        let trie = RefCell::new(QueryTrie::default());
+        let rc_self = Rc::new(RefCell::new(self));
+        let rc_self_clone = rc_self.clone();
+
+        let keymap_clone = rc_self_clone.borrow().keymap.clone();
+        let query_editor_renderer_clone = rc_self_clone.borrow().query_editor_renderer.clone();
+        let hint_message_renderer_clone = rc_self_clone.borrow().hint_message_renderer.clone();
+        let suggest_clone = rc_self_clone.borrow().suggest.clone();
+        let suggest_renderer_clone = rc_self_clone.borrow().suggest_renderer.clone();
+        let json_renderer_clone = rc_self_clone.borrow().json_renderer.clone();
         Ok(Prompt::try_new(
             Box::new(self::render::Renderer {
-                keymap: self.keymap,
+                keymap: keymap_clone,
                 query_editor_snapshot: Snapshot::<text_editor::Renderer>::new(
-                    self.query_editor_renderer,
-                ),
-                hint_message_snapshot: Snapshot::<text::Renderer>::new(self.hint_message_renderer),
-                suggest: self.suggest,
-                suggest_snapshot: Snapshot::<listbox::Renderer>::new(self.suggest_renderer),
-                json_bundle_snapshot: Snapshot::<json::bundle::Renderer>::new(
-                    self.json_bundle_renderer,
+                    query_editor_renderer_clone,
                 ),
+                hint_message_snapshot: Snapshot::<text::Renderer>::new(hint_message_renderer_clone),
+                suggest: suggest_clone,
+                suggest_snapshot: Snapshot::<listbox::Renderer>::new(suggest_renderer_clone),
+                json_snapshot: Snapshot::<json::Renderer>::new(json_renderer_clone),
             }),
             Box::new(
                 move |event: &Event,
                       renderer: &mut Box<dyn Renderer + 'static>|
                       -> promkit::Result<PromptSignal> {
-                    let renderer = self::render::Renderer::cast_mut(renderer.as_mut())?;
-                    let signal = match renderer.keymap.get() {
-                        Some(f) => f(event, renderer),
-                        None => Ok(PromptSignal::Quit),
-                    }?;
-                    let completed = renderer
-                        .query_editor_snapshot
-                        .after()
-                        .texteditor
-                        .text_without_cursor()
-                        .to_string();
-
-                    if completed
-                        != renderer
-                            .query_editor_snapshot
-                            .borrow_before()
-                            .texteditor
-                            .text_without_cursor()
-                            .to_string()
-                    {
-                        renderer.hint_message_snapshot.reset_after_to_init();
-
-                        // libjq writes to the console when an internal error occurs.
-                        //
-                        // e.g.
-                        // ```
-                        // let _ = j9::run(". | select(.number == invalid_no_quote)", "{}");
-                        // jq: error: invalid_no_quote/0 is not defined at <top-level>, line 1:
-                        //     . | select(.number == invalid_no_quote)
-                        // ```
-                        //
-                        // While errors themselves are not an issue,
-                        // they interfere with the console output handling mechanism
-                        // in promkit and qjq (e.g., causing line numbers to shift).
-                        // Therefore, we'll ignore console output produced inside j9::run.
-                        //
-                        // It's possible that this could be handled
-                        // within github.com/ynqa/j9, but for now,
-                        // we'll proceed with this workaround.
-                        //
-                        // For reference, the functionality of a quiet mode in libjq is
-                        // also being discussed at https://github.com/jqlang/jq/issues/1225.
-                        let ignore_err = Gag::stderr().unwrap();
-                        let ret = j9::run(&completed, &self.input_json);
-                        drop(ignore_err);
-
-                        ret
-                        .map(|ret| {
-                            if ret.is_empty() {
-                                if !self.no_hint {
-                                    renderer.hint_message_snapshot.after_mut().replace(text::Renderer {
-                                        text: format!("JSON query ('{}') was executed, but no results were returned.", &completed),
-                                        style: StyleBuilder::new()
-                                            .fgc(Color::Red)
-                                            .attrs(Attributes::from(Attribute::Bold))
-                                            .build(),
-                                    });
-                                }
-                                if let Some(searched) = trie.borrow().prefix_search_value(&completed) {
-                                    renderer.json_bundle_snapshot.after_mut().bundle = JsonBundle::new(searched.clone());
-                                }
-                            } else {
-                                ret.iter().map(|string| {
-                                    JsonNode::try_new(string.as_str(), self.expand_depth)
-                                }).collect::<Result<Vec<JsonNode>, _>>()
-                                .map(|nodes| {
-                                    if nodes.len() == 1 && nodes.first().unwrap() == &JsonNode::Leaf(serde_json::Value::Null) {
-                                        if !self.no_hint {
-                                            renderer.hint_message_snapshot.after_mut().replace(text::Renderer {
-                                                text: format!(
-                                                    "JSON query resulted in 'null', which may indicate a typo or incorrect query: '{}'",
-                                                    &completed,
-                                                ),
-                                                style: StyleBuilder::new()
-                                                    .fgc(Color::Yellow)
-                                                    .attrs(Attributes::from(Attribute::Bold))
-                                                    .build(),
-                                            });
-                                        }
-                                        if let Some(searched) = trie.borrow().prefix_search_value(&completed) {
-                                            renderer.json_bundle_snapshot.after_mut().bundle = JsonBundle::new(searched.clone());
-                                        }
-                                    } else {
-                                        // SUCCESS!
-                                        trie.borrow_mut().insert(&completed, nodes.clone());
-                                        renderer.json_bundle_snapshot.after_mut().bundle = JsonBundle::new(nodes);
-                                    }
-                                })
-                                .unwrap_or_else(|e| {
-                                    if !self.no_hint {
-                                        renderer.hint_message_snapshot.after_mut().replace(text::Renderer{
-                                            text: format!(
-                                                "Failed to parse query result for viewing: {}",
-                                                e
-                                            ),
-                                            style: StyleBuilder::new()
-                                                .fgc(Color::Red)
-                                                .attrs(Attributes::from(Attribute::Bold))
-                                                .build(),
-                                        })
-                                    }
-                                    if let Some(searched) = trie.borrow().prefix_search_value(&completed) {
-                                        renderer.json_bundle_snapshot.after_mut().bundle = JsonBundle::new(searched.clone());
-                                    }
-                                });
-                            }
-                        })
-                        .unwrap_or_else(|_| {
-                            if !self.no_hint {
-                                renderer.hint_message_snapshot.after_mut().replace(text::Renderer {
-                                    text: format!("Failed to execute jq query '{}'", &completed),
-                                    style: StyleBuilder::new()
-                                        .fgc(Color::Red)
-                                        .attrs(Attributes::from(Attribute::Bold))
-                                        .build(),
-                                    },
-                                );
-                            }
-                            if let Some(searched) = trie.borrow().prefix_search_value(&completed) {
-                                renderer.json_bundle_snapshot.after_mut().bundle = JsonBundle::new(searched.clone());
-                            }
-                        });
-                    }
-                    Ok(signal)
+                    let trie = RefCell::new(QueryTrie::default());
+                    rc_self_clone.borrow().evaluate(event, renderer, trie)
                 },
             ),
             |renderer: &(dyn Renderer + '_)| -> promkit::Result<String> {
@@ -291,3 +322,10 @@ impl Jnv {
         )?)
     }
 }
+
+fn deserialize_json(json_str: &str) -> Result<Vec<serde_json::Value>> {
+    Deserializer::from_str(json_str)
+        .into_iter::<serde_json::Value>()
+        .map(|res| res.map_err(anyhow::Error::new))
+        .collect::<Result<Vec<serde_json::Value>>>()
+}
diff --git a/src/jnv/keymap.rs b/src/jnv/keymap.rs
index bdcb0c5..cc48bab 100644
--- a/src/jnv/keymap.rs
+++ b/src/jnv/keymap.rs
@@ -7,7 +7,7 @@ use promkit::{
 pub fn default(event: &Event, renderer: &mut crate::jnv::render::Renderer) -> Result<PromptSignal> {
     let query_editor_after_mut = renderer.query_editor_snapshot.after_mut();
     let suggest_after_mut = renderer.suggest_snapshot.after_mut();
-    let json_bundle_after_mut = renderer.json_bundle_snapshot.after_mut();
+    let json_after_mut = renderer.json_snapshot.after_mut();
 
     match event {
         Event::Key(KeyEvent {
@@ -96,7 +96,7 @@ pub fn default(event: &Event, renderer: &mut crate::jnv::render::Renderer) -> Re
             kind: KeyEventKind::Press,
             state: KeyEventState::NONE,
         }) => {
-            json_bundle_after_mut.bundle.backward();
+            json_after_mut.stream.backward();
         }
 
         // Move down.
@@ -112,7 +112,7 @@ pub fn default(event: &Event, renderer: &mut crate::jnv::render::Renderer) -> Re
             kind: KeyEventKind::Press,
             state: KeyEventState::NONE,
         }) => {
-            json_bundle_after_mut.bundle.forward();
+            json_after_mut.stream.forward();
         }
 
         // Move to tail
@@ -122,7 +122,7 @@ pub fn default(event: &Event, renderer: &mut crate::jnv::render::Renderer) -> Re
             kind: KeyEventKind::Press,
             state: KeyEventState::NONE,
         }) => {
-            json_bundle_after_mut.bundle.move_to_tail();
+            json_after_mut.stream.move_to_tail();
         }
 
         // Move to head
@@ -132,7 +132,7 @@ pub fn default(event: &Event, renderer: &mut crate::jnv::render::Renderer) -> Re
             kind: KeyEventKind::Press,
             state: KeyEventState::NONE,
         }) => {
-            json_bundle_after_mut.bundle.move_to_head();
+            json_after_mut.stream.move_to_head();
         }
 
         // Toggle collapse/expand
@@ -142,7 +142,7 @@ pub fn default(event: &Event, renderer: &mut crate::jnv::render::Renderer) -> Re
             kind: KeyEventKind::Press,
             state: KeyEventState::NONE,
         }) => {
-            json_bundle_after_mut.bundle.toggle();
+            json_after_mut.stream.toggle();
         }
 
         Event::Key(KeyEvent {
@@ -151,7 +151,7 @@ pub fn default(event: &Event, renderer: &mut crate::jnv::render::Renderer) -> Re
             kind: KeyEventKind::Press,
             state: KeyEventState::NONE,
         }) => {
-            json_bundle_after_mut.bundle.expand_all();
+            json_after_mut.stream.expand_all();
         }
 
         Event::Key(KeyEvent {
@@ -160,7 +160,7 @@ pub fn default(event: &Event, renderer: &mut crate::jnv::render::Renderer) -> Re
             kind: KeyEventKind::Press,
             state: KeyEventState::NONE,
         }) => {
-            json_bundle_after_mut.bundle.collapse_all();
+            json_after_mut.stream.collapse_all();
         }
 
         // Input char.
diff --git a/src/jnv/render.rs b/src/jnv/render.rs
index af81c7b..6a275c4 100644
--- a/src/jnv/render.rs
+++ b/src/jnv/render.rs
@@ -3,13 +3,14 @@ use promkit::{
     suggest::Suggest, text, text_editor,
 };
 
+#[derive(Clone)]
 pub struct Renderer {
     pub keymap: KeymapManager<Self>,
     pub query_editor_snapshot: Snapshot<text_editor::Renderer>,
     pub hint_message_snapshot: Snapshot<text::Renderer>,
     pub suggest: Suggest,
     pub suggest_snapshot: Snapshot<listbox::Renderer>,
-    pub json_bundle_snapshot: Snapshot<json::bundle::Renderer>,
+    pub json_snapshot: Snapshot<json::Renderer>,
 }
 
 impl_as_any!(Renderer);
@@ -21,7 +22,7 @@ impl promkit::Renderer for Renderer {
         panes.extend(self.query_editor_snapshot.create_panes(width));
         panes.extend(self.hint_message_snapshot.create_panes(width));
         panes.extend(self.suggest_snapshot.create_panes(width));
-        panes.extend(self.json_bundle_snapshot.create_panes(width));
+        panes.extend(self.json_snapshot.create_panes(width));
         panes
     }
 }
diff --git a/src/jnv/trie.rs b/src/jnv/trie.rs
index 3c435d9..e1267c8 100644
--- a/src/jnv/trie.rs
+++ b/src/jnv/trie.rs
@@ -1,22 +1,22 @@
 use radix_trie::{Trie, TrieCommon};
 
-use promkit::json::JsonNode;
+use promkit::serde_json;
 
 #[derive(Default)]
-pub struct QueryTrie(Trie<String, Vec<JsonNode>>);
+pub struct QueryTrie(Trie<String, Vec<serde_json::Value>>);
 
 impl QueryTrie {
-    pub fn insert(&mut self, query: &str, json_nodes: Vec<JsonNode>) {
+    pub fn insert(&mut self, query: &str, json_nodes: Vec<serde_json::Value>) {
         self.0.insert(query.to_string(), json_nodes);
     }
 
-    pub fn prefix_search(&self, query: &str) -> Option<(&String, &Vec<JsonNode>)> {
+    pub fn prefix_search(&self, query: &str) -> Option<(&String, &Vec<serde_json::Value>)> {
         self.0
             .get_ancestor(query)
             .and_then(|subtrie| Some((subtrie.key()?, subtrie.value()?)))
     }
 
-    pub fn prefix_search_value(&self, query: &str) -> Option<&Vec<JsonNode>> {
+    pub fn prefix_search_value(&self, query: &str) -> Option<&Vec<serde_json::Value>> {
         self.prefix_search(query).map(|tup| tup.1)
     }
 }


Test patch: