From dd37c602857927776f02f8df155f675a555cce9c Mon Sep 17 00:00:00 2001 From: ynqa Date: Sun, 24 Mar 2024 13:08:11 +0900 Subject: [PATCH 1/7] use promkit v0.3.2: jsonstream instead of jsonbundle --- src/jnv/keymap.rs | 16 ++++++++-------- src/jnv/render.rs | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) 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 { 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..2be0617 100644 --- a/src/jnv/render.rs +++ b/src/jnv/render.rs @@ -9,7 +9,7 @@ pub struct Renderer { pub hint_message_snapshot: Snapshot, pub suggest: Suggest, pub suggest_snapshot: Snapshot, - pub json_bundle_snapshot: Snapshot, + pub json_snapshot: Snapshot, } impl_as_any!(Renderer); @@ -21,7 +21,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 } } From 7530dbb5d17656556c25a21ab702b5f04189bda1 Mon Sep 17 00:00:00 2001 From: ynqa Date: Sun, 24 Mar 2024 19:41:11 +0900 Subject: [PATCH 2/7] error: initialize --- Cargo.lock | 9 +- Cargo.toml | 3 +- src/jnv.rs | 374 ++++++++++++++++++++++++++---------------------- src/jnv/trie.rs | 10 +- 4 files changed, 215 insertions(+), 181 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 980f815..ca05981 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", @@ -503,10 +503,9 @@ dependencies = [ [[package]] name = "promkit" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53726058b89f75dcae42ddc26e3f155c86e585b7e41fde6f720a25cbed38581e" +version = "0.3.2" dependencies = [ + "anyhow", "crossterm", "indexmap", "radix_trie", diff --git a/Cargo.toml b/Cargo.toml index 3439fde..5042119 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,8 @@ anyhow = "1.0.80" clap = { version = "4.5.1", features = ["derive"] } gag = "1.0.0" j9 = "0.1.2" -promkit = "0.3.1" +# promkit = "0.3.1" +promkit = { path = "../promkit" } radix_trie = "0.2.1" # The profile that 'cargo dist' will build with diff --git a/src/jnv.rs b/src/jnv.rs index 739e745..f5eefac 100644 --- a/src/jnv.rs +++ b/src/jnv.rs @@ -1,4 +1,7 @@ -use std::cell::RefCell; +use std::{ + cell::{Ref, RefCell}, + rc::Rc, +}; use anyhow::{anyhow, Result}; use gag::Gag; @@ -6,11 +9,12 @@ 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 +27,7 @@ mod trie; use trie::QueryTrie; pub struct Jnv { - input_json: String, + input_json_stream: Vec, expand_depth: Option, no_hint: bool, @@ -31,42 +35,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, } impl Jnv { pub fn try_new( - input_json: String, + input_json_str: String, expand_depth: Option, no_hint: bool, edit_mode: text_editor::Mode, indent: usize, suggestion_list_length: usize, ) -> Result { - 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::() - } - }); + JsonPathSegment::Index(index) => format!("[{}]", index), + }) + .collect::() + } + }); Ok(Self { - input_json: input_json.clone(), + input_json_stream: stream.clone(), expand_depth, no_hint, query_editor_renderer: text_editor::Renderer { @@ -87,7 +95,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::::new()), cursor: String::from("❯ "), @@ -100,20 +108,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,8 +131,23 @@ 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 }) + } + } + pub fn prompt(self) -> Result> { - let trie = RefCell::new(QueryTrie::default()); + let rc_self = Rc::new(RefCell::new(self)); + let rc_self_clone = rc_self.clone(); Ok(Prompt::try_new( Box::new(self::render::Renderer { keymap: self.keymap, @@ -146,138 +157,14 @@ impl Jnv { hint_message_snapshot: Snapshot::::new(self.hint_message_renderer), suggest: self.suggest, suggest_snapshot: Snapshot::::new(self.suggest_renderer), - json_bundle_snapshot: Snapshot::::new( - self.json_bundle_renderer, - ), + json_snapshot: Snapshot::::new(self.json_renderer), }), Box::new( move |event: &Event, renderer: &mut Box| -> promkit::Result { - 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 , 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::, _>>() - .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()); + evaluate(rc_self_clone.borrow(), event, renderer, trie) }, ), |renderer: &(dyn Renderer + '_)| -> promkit::Result { @@ -291,3 +178,150 @@ impl Jnv { )?) } } + +fn deserialize_json(json_str: &str) -> Result> { + Deserializer::from_str(json_str) + .into_iter::() + .map(|res| res.map_err(anyhow::Error::new)) + .collect::>>() +} + +fn evaluate( + jnv: Ref, + event: &Event, + renderer: &mut Box, + trie: RefCell, +) -> promkit::Result { + 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 , 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::::new(); + for v in &jnv.input_json_stream { + let inner_ret: Vec = match j9::run(&completed, &v.to_string()) { + Ok(ret) => ret, + Err(e) => { + jnv.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(), jnv.expand_depth); + } + return Ok(signal); + } + }; + flatten_ret.extend(inner_ret); + } + drop(ignore_err); + + if flatten_ret.is_empty() { + jnv.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(), jnv.expand_depth); + } + } else { + match deserialize_json(&flatten_ret.join("\n")) { + Ok(jsonl) => { + let stream = JsonStream::new(jsonl.clone(), jnv.expand_depth); + + let is_null = stream + .roots() + .iter() + .all(|node| node == &JsonNode::Leaf(serde_json::Value::Null)); + if is_null { + jnv.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(), jnv.expand_depth); + } + } else { + // SUCCESS! + trie.borrow_mut().insert(&completed, jsonl); + renderer.json_snapshot.after_mut().stream = stream; + } + } + Err(e) => { + jnv.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(), jnv.expand_depth); + } + } + } + // flatten_ret.is_empty() + } + // before != completed + } + Ok(signal) +} 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>); +pub struct QueryTrie(Trie>); impl QueryTrie { - pub fn insert(&mut self, query: &str, json_nodes: Vec) { + pub fn insert(&mut self, query: &str, json_nodes: Vec) { self.0.insert(query.to_string(), json_nodes); } - pub fn prefix_search(&self, query: &str) -> Option<(&String, &Vec)> { + pub fn prefix_search(&self, query: &str) -> Option<(&String, &Vec)> { self.0 .get_ancestor(query) .and_then(|subtrie| Some((subtrie.key()?, subtrie.value()?))) } - pub fn prefix_search_value(&self, query: &str) -> Option<&Vec> { + pub fn prefix_search_value(&self, query: &str) -> Option<&Vec> { self.prefix_search(query).map(|tup| tup.1) } } From 06babebe33f5dcf3cb29bc168bef2d6fbc50b4c9 Mon Sep 17 00:00:00 2001 From: ynqa Date: Sun, 24 Mar 2024 22:47:53 +0900 Subject: [PATCH 3/7] accept jsonl like `echo '{ "a": 123 }{ "b": 456 }' | cargo run` --- src/jnv.rs | 308 +++++++++++++++++++++++----------------------- src/jnv/render.rs | 1 + 2 files changed, 157 insertions(+), 152 deletions(-) diff --git a/src/jnv.rs b/src/jnv.rs index f5eefac..19a4497 100644 --- a/src/jnv.rs +++ b/src/jnv.rs @@ -1,9 +1,6 @@ -use std::{ - cell::{Ref, RefCell}, - rc::Rc, -}; +use std::{cell::RefCell, rc::Rc}; -use anyhow::{anyhow, Result}; +use anyhow::Result; use gag::Gag; use promkit::{ @@ -145,26 +142,173 @@ impl Jnv { } } + fn evaluate( + &self, + event: &Event, + renderer: &mut Box, + trie: RefCell, + ) -> promkit::Result { + 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 , 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::::new(); + for v in &self.input_json_stream { + let inner_ret: Vec = 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> { 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::::new( - self.query_editor_renderer, + query_editor_renderer_clone, ), - hint_message_snapshot: Snapshot::::new(self.hint_message_renderer), - suggest: self.suggest, - suggest_snapshot: Snapshot::::new(self.suggest_renderer), - json_snapshot: Snapshot::::new(self.json_renderer), + hint_message_snapshot: Snapshot::::new(hint_message_renderer_clone), + suggest: suggest_clone, + suggest_snapshot: Snapshot::::new(suggest_renderer_clone), + json_snapshot: Snapshot::::new(json_renderer_clone), }), Box::new( move |event: &Event, renderer: &mut Box| -> promkit::Result { let trie = RefCell::new(QueryTrie::default()); - evaluate(rc_self_clone.borrow(), event, renderer, trie) + rc_self_clone.borrow().evaluate(event, renderer, trie) }, ), |renderer: &(dyn Renderer + '_)| -> promkit::Result { @@ -185,143 +329,3 @@ fn deserialize_json(json_str: &str) -> Result> { .map(|res| res.map_err(anyhow::Error::new)) .collect::>>() } - -fn evaluate( - jnv: Ref, - event: &Event, - renderer: &mut Box, - trie: RefCell, -) -> promkit::Result { - 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 , 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::::new(); - for v in &jnv.input_json_stream { - let inner_ret: Vec = match j9::run(&completed, &v.to_string()) { - Ok(ret) => ret, - Err(e) => { - jnv.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(), jnv.expand_depth); - } - return Ok(signal); - } - }; - flatten_ret.extend(inner_ret); - } - drop(ignore_err); - - if flatten_ret.is_empty() { - jnv.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(), jnv.expand_depth); - } - } else { - match deserialize_json(&flatten_ret.join("\n")) { - Ok(jsonl) => { - let stream = JsonStream::new(jsonl.clone(), jnv.expand_depth); - - let is_null = stream - .roots() - .iter() - .all(|node| node == &JsonNode::Leaf(serde_json::Value::Null)); - if is_null { - jnv.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(), jnv.expand_depth); - } - } else { - // SUCCESS! - trie.borrow_mut().insert(&completed, jsonl); - renderer.json_snapshot.after_mut().stream = stream; - } - } - Err(e) => { - jnv.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(), jnv.expand_depth); - } - } - } - // flatten_ret.is_empty() - } - // before != completed - } - Ok(signal) -} diff --git a/src/jnv/render.rs b/src/jnv/render.rs index 2be0617..6a275c4 100644 --- a/src/jnv/render.rs +++ b/src/jnv/render.rs @@ -3,6 +3,7 @@ use promkit::{ suggest::Suggest, text, text_editor, }; +#[derive(Clone)] pub struct Renderer { pub keymap: KeymapManager, pub query_editor_snapshot: Snapshot, From 35d275f54a32b750d1c0227badcf715059111b62 Mon Sep 17 00:00:00 2001 From: ynqa Date: Sun, 24 Mar 2024 23:40:21 +0900 Subject: [PATCH 4/7] (temporary) use dev-0.3.2/main branch for promkit --- Cargo.lock | 1 + Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index ca05981..c5fae66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -504,6 +504,7 @@ dependencies = [ [[package]] name = "promkit" version = "0.3.2" +source = "git+https://github.com/ynqa/promkit?branch=dev-0.3.2/main#94c1e139f20b4ffb51649a288c41d63327b04400" dependencies = [ "anyhow", "crossterm", diff --git a/Cargo.toml b/Cargo.toml index 5042119..3f05d67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ clap = { version = "4.5.1", features = ["derive"] } gag = "1.0.0" j9 = "0.1.2" # promkit = "0.3.1" -promkit = { path = "../promkit" } +promkit = { git = "https://github.com/ynqa/promkit", branch = "dev-0.3.2/main" } radix_trie = "0.2.1" # The profile that 'cargo dist' will build with From 0618fa9647ccba8510469e1de01025b6509281af Mon Sep 17 00:00:00 2001 From: ynqa Date: Mon, 25 Mar 2024 18:23:18 +0900 Subject: [PATCH 5/7] use 0.3.2 for promkit --- Cargo.lock | 3 ++- Cargo.toml | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c5fae66..3072f12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -504,7 +504,8 @@ dependencies = [ [[package]] name = "promkit" version = "0.3.2" -source = "git+https://github.com/ynqa/promkit?branch=dev-0.3.2/main#94c1e139f20b4ffb51649a288c41d63327b04400" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa31233c1a91cf9b5e8753a57b59a3bcca559f45f38da508857fb866635ab92" dependencies = [ "anyhow", "crossterm", diff --git a/Cargo.toml b/Cargo.toml index 3f05d67..69339de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,8 +13,7 @@ anyhow = "1.0.80" clap = { version = "4.5.1", features = ["derive"] } gag = "1.0.0" j9 = "0.1.2" -# promkit = "0.3.1" -promkit = { git = "https://github.com/ynqa/promkit", branch = "dev-0.3.2/main" } +promkit = "0.3.2" radix_trie = "0.2.1" # The profile that 'cargo dist' will build with From 324ec8c094c90fbe0e497f3c13a621ffa2c96f68 Mon Sep 17 00:00:00 2001 From: ynqa Date: Mon, 25 Mar 2024 19:23:58 +0900 Subject: [PATCH 6/7] update readme: accept multiple json structures --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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) From ce744aa98484e90d0ebaf04df7f8bffd9c0c9265 Mon Sep 17 00:00:00 2001 From: ynqa Date: Mon, 25 Mar 2024 19:52:20 +0900 Subject: [PATCH 7/7] bump up version of jnv to v0.2.0 (j9 to v0.1.3) --- Cargo.lock | 26 +++++++++++++------------- Cargo.toml | 4 ++-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3072f12..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" @@ -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", @@ -547,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", @@ -696,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 69339de..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 "] edition = "2021" description = "JSON navigator and interactive filter leveraging jq" @@ -12,7 +12,7 @@ readme = "README.md" anyhow = "1.0.80" clap = { version = "4.5.1", features = ["derive"] } gag = "1.0.0" -j9 = "0.1.2" +j9 = "0.1.3" promkit = "0.3.2" radix_trie = "0.2.1"