diff --git a/Cargo.lock b/Cargo.lock index ae940c6..00253b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,9 +78,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "arboard" @@ -206,7 +206,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -344,7 +344,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn", + "syn 2.0.117", ] [[package]] @@ -453,7 +453,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -504,6 +504,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "futures" version = "0.3.32" @@ -560,7 +566,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -630,7 +636,7 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -728,7 +734,7 @@ version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01dbdbd07b076e8403abac68ce7744d93e2ecd953bbc44bf77bf00e1e81172bc" dependencies = [ - "foldhash", + "foldhash 0.1.5", "hifijson", "indexmap", "jaq-core", @@ -1064,9 +1070,9 @@ dependencies = [ [[package]] name = "promkit-core" -version = "0.3.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26da2c38cb21f2c5470a00868b513916d904f3fd6aef947962a5a06350651c49" +checksum = "6969944b31c2e01c8772aad675549cf8ca03e634978f592c4e278476057188cf" dependencies = [ "anyhow", "crossbeam-skiplist", @@ -1077,9 +1083,9 @@ dependencies = [ [[package]] name = "promkit-widgets" -version = "0.5.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2863f757bfc30b08ba1e0f54c7e1fe3ea62b46ad44fa82cff0849f1293f29188" +checksum = "981305949b8e23913f4a181f870ed0b79dc12b5c6696d53cdc8c707d60925339" dependencies = [ "anyhow", "promkit-core", @@ -1122,9 +1128,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -1208,9 +1214,9 @@ checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -1218,30 +1224,31 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.9", ] [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" dependencies = [ + "foldhash 0.2.0", "indexmap", "itoa", "memchr", @@ -1341,6 +1348,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "termcfg" version = "0.2.0" @@ -1378,7 +1396,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1389,7 +1407,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1408,9 +1426,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.50.0" +version = "1.54.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3" dependencies = [ "bytes", "libc", @@ -1425,13 +1443,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.9", ] [[package]] @@ -1558,7 +1576,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -1690,7 +1708,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1701,7 +1719,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1869,7 +1887,7 @@ checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 39923f2..ce010b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,8 +34,8 @@ jaq-json = { version = "1.1.3", features = ["serde_json"] } jaq-std = "2.1.2" [dependencies.promkit-widgets] -version = "0.5.0" -features = ["jsonstream", "listbox", "serde", "spinner", "status", "texteditor"] +version = "0.10.0" +features = ["json", "listbox", "serde", "spinner", "text", "texteditor"] default-features = false # The profile that 'cargo dist' will build with diff --git a/README.md b/README.md index f89aa96..384f5a7 100644 --- a/README.md +++ b/README.md @@ -301,7 +301,7 @@ inactive_char_style = "attr=dim" # max_streams = # JSON display settings -# Uses promkit_widgets::jsonstream::Config directly +# Uses promkit_widgets::json::Config directly [json.stream] # Number of spaces to use for indentation indent = 2 diff --git a/default.toml b/default.toml index 05f892e..cc4c8aa 100644 --- a/default.toml +++ b/default.toml @@ -65,7 +65,7 @@ inactive_char_style = "attr=dim" # max_streams = # JSON display settings -# Uses promkit_widgets::jsonstream::Config directly +# Uses promkit_widgets::json::Config directly [json.stream] # Number of spaces to use for indentation indent = 2 diff --git a/src/completion.rs b/src/completion.rs index 419f016..e17eba3 100644 --- a/src/completion.rs +++ b/src/completion.rs @@ -1,7 +1,7 @@ use std::{collections::BTreeSet, sync::Arc}; use promkit_widgets::{ - core::{crossterm::event::Event, grapheme::StyledGraphemes, Widget}, + core::{crossterm::event::Event, CreatedGraphemes, Widget}, listbox::{self, Listbox}, }; use tokio::{ @@ -133,8 +133,8 @@ impl CompletionNavigator { } /// Create graphemes for rendering the completion navigator. - pub fn create_graphemes(&self, width: u16, height: u16) -> StyledGraphemes { - self.state.create_graphemes(width, height) + pub fn create_graphemes(&self) -> CreatedGraphemes { + self.state.create_graphemes() } /// Returns true when the cursor is close enough to the visible tail @@ -252,7 +252,6 @@ pub fn start_completion_task( loop { tokio::select! { Some(action) = action_rx.recv() => { - let area = shared_ctx.area().await; let completion_view = { let mut completion = shared_completion.write().await; match action { @@ -296,7 +295,7 @@ pub fn start_completion_task( completion.clear_session_state(); } } - completion.create_graphemes(area.0, area.1) + completion.create_graphemes() }; shared_renderer diff --git a/src/config.rs b/src/config.rs index fb8ee63..d10c618 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,6 @@ use std::collections::HashSet; -use promkit_widgets::{core::crossterm::event::Event, jsonstream, listbox, text_editor}; +use promkit_widgets::{core::crossterm::event::Event, json, listbox, text_editor}; use serde::{Deserialize, Serialize}; use termcfg::crossterm_config::event_set_serde; use tokio::time::Duration; @@ -17,7 +17,7 @@ pub struct EditorConfig { #[derive(Serialize, Deserialize)] pub struct JsonConfig { pub max_streams: Option, - pub stream: jsonstream::Config, + pub stream: json::Config, } #[derive(Serialize, Deserialize)] @@ -140,3 +140,41 @@ impl Config { toml::from_str(content).map_err(Into::into) } } + +#[cfg(test)] +mod tests { + use super::*; + use promkit_widgets::{core::crossterm::style::Color, json::config::OverflowMode}; + + #[test] + fn loads_existing_default_config_with_new_widget_defaults() { + let config = Config::load_from(DEFAULT_CONFIG).unwrap(); + assert_eq!(config.editor.on_focus.prefix, "❯❯ "); + assert!(config.editor.on_focus.continuation_prefix.is_empty()); + assert_eq!(config.json.stream.indent, 2); + assert_eq!(config.json.stream.overflow_mode, OverflowMode::Wrap); + assert!(!config.json.stream.show_line_numbers); + assert_eq!(config.completion.listbox.lines, Some(3)); + } + + #[test] + fn preserves_custom_styles_in_existing_config() { + let custom = DEFAULT_CONFIG + .replace("❯❯ ", "jq ❯ ") + .replace("indent = 2", "indent = 4") + .replace("key_style = \"fg=cyan\"", "key_style = \"fg=#FF79C6\"") + .replace("cursor = \"❯ \"", "cursor = \"→ \""); + let config = Config::load_from(&custom).unwrap(); + assert_eq!(config.editor.on_focus.prefix, "jq ❯ "); + assert_eq!(config.json.stream.indent, 4); + assert_eq!( + config.json.stream.key_style.foreground_color, + Some(Color::Rgb { + r: 255, + g: 121, + b: 198 + }) + ); + assert_eq!(config.completion.listbox.cursor, "→ "); + } +} diff --git a/src/guide.rs b/src/guide.rs index f3f51eb..225bcb7 100644 --- a/src/guide.rs +++ b/src/guide.rs @@ -1,11 +1,15 @@ use arboard::Clipboard; use promkit_widgets::{ - core::{render::SharedRenderer, Widget}, - status::{self, Severity}, + core::{ + crossterm::style::{Color, ContentStyle}, + render::SharedRenderer, + Widget, + }, + text::{self, Text}, }; use tokio::{sync::mpsc, task::JoinHandle}; -use crate::context::{Index, SharedContext}; +use crate::context::Index; /// Represent a message to be shown in the guide. /// This is used to decouple the logic of generating messages from the logic of rendering them. @@ -28,44 +32,48 @@ pub enum GuideAction { Show(GuideMessage), } -fn message_to_state(message: GuideMessage) -> status::State { - match message { - GuideMessage::CopiedToClipboard => { - status::State::new("Copied to clipboard", Severity::Success) - } +fn message_to_state(message: GuideMessage) -> text::State { + let (message, color) = match message { + GuideMessage::CopiedToClipboard => ("Copied to clipboard".to_owned(), Color::Green), GuideMessage::FailedToCopyToClipboard(e) => { - status::State::new(format!("Failed to copy to clipboard: {e}"), Severity::Error) + (format!("Failed to copy to clipboard: {e}"), Color::Red) } GuideMessage::FailedToSetupClipboard(e) => { - status::State::new(format!("Failed to setup clipboard: {e}"), Severity::Error) + (format!("Failed to setup clipboard: {e}"), Color::Red) } - GuideMessage::FailedToCopyWhileRenderingInProgress => status::State::new( - "Failed to copy while rendering is in progress.", - Severity::Warning, - ), - GuideMessage::FailedToSwitchModeWhileRenderingInProgress => status::State::new( - "Failed to switch mode while rendering is in progress.", - Severity::Warning, + GuideMessage::FailedToCopyWhileRenderingInProgress => ( + "Failed to copy while rendering is in progress.".to_owned(), + Color::Yellow, ), - GuideMessage::LoadedAllSuggestions(count) => status::State::new( - format!("Loaded all ({count}) suggestions"), - Severity::Success, + GuideMessage::FailedToSwitchModeWhileRenderingInProgress => ( + "Failed to switch mode while rendering is in progress.".to_owned(), + Color::Yellow, ), - GuideMessage::LoadedPartiallySuggestions(count) => status::State::new( + GuideMessage::LoadedAllSuggestions(count) => { + (format!("Loaded all ({count}) suggestions"), Color::Green) + } + GuideMessage::LoadedPartiallySuggestions(count) => ( format!("Loaded partially ({count}) suggestions"), - Severity::Success, - ), - GuideMessage::NoSuggestionFound(prefix) => status::State::new( - format!("No suggestion found for '{prefix}'"), - Severity::Warning, + Color::Green, ), - GuideMessage::JqReturnedNull(input) => status::State::new( + GuideMessage::NoSuggestionFound(prefix) => { + (format!("No suggestion found for '{prefix}'"), Color::Yellow) + } + GuideMessage::JqReturnedNull(input) => ( format!("jq returned 'null', which may indicate a typo or incorrect filter: `{input}`"), - Severity::Warning, + Color::Yellow, ), - GuideMessage::JqFailed(e) => { - status::State::new(format!("jq failed: `{e}`"), Severity::Error) - } + GuideMessage::JqFailed(e) => (format!("jq failed: `{e}`"), Color::Red), + }; + text::State { + text: Text::from(message), + config: text::Config { + style: Some(ContentStyle { + foreground_color: Some(color), + ..Default::default() + }), + ..Default::default() + }, } } @@ -84,20 +92,18 @@ pub fn copy_to_clipboard_message(content: &str) -> GuideMessage { pub fn start_guide_task( mut action_rx: mpsc::Receiver, shared_renderer: SharedRenderer, - shared_ctx: SharedContext, no_hint: bool, ) -> JoinHandle> { tokio::spawn(async move { loop { tokio::select! { Some(action) = action_rx.recv() => { - let area = shared_ctx.area().await; let view = if no_hint { Default::default() } else { match action { - GuideAction::Clear => status::State::default().create_graphemes(area.0, area.1), - GuideAction::Show(message) => message_to_state(message).create_graphemes(area.0, area.1), + GuideAction::Clear => text::State::default().create_graphemes(), + GuideAction::Show(message) => message_to_state(message).create_graphemes(), } }; shared_renderer.update([(Index::Guide, view)]).render().await?; diff --git a/src/json.rs b/src/json.rs index f12a377..55de367 100644 --- a/src/json.rs +++ b/src/json.rs @@ -5,7 +5,7 @@ use jaq_core::{ use jaq_json::Val; use promkit_widgets::{ - jsonstream::jsonz, + json::jsonz, serde_json::{self, Deserializer, Value}, }; diff --git a/src/json_viewer.rs b/src/json_viewer.rs index be1fcf5..08e423c 100644 --- a/src/json_viewer.rs +++ b/src/json_viewer.rs @@ -1,8 +1,8 @@ use std::sync::Arc; use promkit_widgets::{ - core::{crossterm::event::Event, grapheme::StyledGraphemes, render::SharedRenderer, Widget}, - jsonstream::{self, JsonStream}, + core::{crossterm::event::Event, render::SharedRenderer, CreatedGraphemes, Widget}, + json::{self as json_widget, Document}, serde_json::{self, Value}, }; use tokio::{ @@ -30,7 +30,7 @@ pub enum RenderTrigger { /// JSON viewer that maintains the state of JSON stream /// and handles user interactions and query processing. pub struct JsonViewer { - state: jsonstream::State, + state: json_widget::State, json: Vec, keybinds: JsonViewerKeybinds, } @@ -40,7 +40,7 @@ pub type SharedJsonViewer = Arc>; impl JsonViewer { /// Get the formatted content of current JSON stream. pub fn formatted_content(&self) -> String { - self.state.config.format_raw_json(self.state.stream.rows()) + self.state.render_pretty_json() } /// Handle user event and update the viewer state accordingly. @@ -48,35 +48,35 @@ impl JsonViewer { match event { // Move up. event if self.keybinds.up.contains(event) => { - self.state.stream.up(); + self.state.document.up(); } // Move down. event if self.keybinds.down.contains(event) => { - self.state.stream.down(); + self.state.document.down(); } // Move to head event if self.keybinds.move_to_head.contains(event) => { - self.state.stream.head(); + self.state.document.head(); } // Move to tail event if self.keybinds.move_to_tail.contains(event) => { - self.state.stream.tail(); + self.state.document.tail(); } // Toggle collapse/expand event if self.keybinds.toggle.contains(event) => { - self.state.stream.toggle(); + self.state.document.toggle(); } event if self.keybinds.expand.contains(event) => { - self.state.stream.set_nodes_visibility(false); + self.state.document.set_nodes_visibility(false); } event if self.keybinds.collapse.contains(event) => { - self.state.stream.set_nodes_visibility(true); + self.state.document.set_nodes_visibility(true); } _ => (), @@ -88,26 +88,29 @@ impl JsonViewer { &mut self, area: (u16, u16), input: String, - ) -> (Option, Option) { + ) -> (Option, Option) { match json::run_jaq(&input, &self.json) { Ok(ret) => { let mut guide = None; if ret.iter().all(|val| *val == Value::Null) { guide = Some(GuideMessage::JqReturnedNull(input)); - self.state.stream = JsonStream::new(self.json.iter()); + self.state.document = Document::new(self.json.iter()); } else { - self.state.stream = JsonStream::new(ret.iter()); + self.state.document = Document::new(ret.iter()); } - (guide, Some(self.state.create_graphemes(area.0, area.1))) + ( + guide, + Some(self.state.create_graphemes_in_viewport(area.0, area.1)), + ) } Err(e) => { - self.state.stream = JsonStream::new(self.json.iter()); + self.state.document = Document::new(self.json.iter()); ( Some(GuideMessage::JqFailed(e.to_string())), - Some(self.state.create_graphemes(area.0, area.1)), + Some(self.state.create_graphemes_in_viewport(area.0, area.1)), ) } } @@ -132,9 +135,9 @@ pub async fn initialize( } let input_stream = json::deserialize(input, config.max_streams)?; - let stream = JsonStream::new(input_stream.iter()); - let state = jsonstream::State { - stream, + let document = Document::new(input_stream.iter()); + let state = json_widget::State { + document, config: config.stream, }; @@ -145,13 +148,14 @@ pub async fn initialize( } { - let ctx = shared_ctx.lock().await; - let area = ctx.area; - drop(ctx); + let area = shared_ctx.area().await; // TODO: error handling let _ = shared_renderer - .update([(Index::JsonViewer, state.create_graphemes(area.0, area.1))]) + .update([( + Index::JsonViewer, + state.create_graphemes_in_viewport(area.0, area.1), + )]) .render() .await; } @@ -203,15 +207,12 @@ async fn handle_user_event( shared_ctx: SharedContext, event: Event, ) { - let area = { - let ctx = shared_ctx.lock().await; - ctx.area - }; + let area = shared_ctx.area().await; let graphemes = { let mut viewer = shared_viewer_state.lock().await; viewer.handle_user_event(&event); - viewer.state.create_graphemes(area.0, area.1) + viewer.state.create_graphemes_in_viewport(area.0, area.1) }; // TODO: error handling @@ -302,9 +303,7 @@ fn spawn_query_update_task( } let (maybe_guide, maybe_resp) = { - let ctx = shared_ctx.lock().await; - let area = ctx.area; - drop(ctx); + let area = shared_ctx.area().await; let mut runtime = shared_viewer_state.lock().await; runtime.refresh_view_with_query(area, query).await @@ -324,7 +323,7 @@ fn spawn_query_update_task( let _ = shared_renderer .update([( Index::JsonViewer, - maybe_resp.unwrap_or(StyledGraphemes::default()), + maybe_resp.unwrap_or(CreatedGraphemes::default()), )]) .render() .await; @@ -389,3 +388,124 @@ pub fn start_viewer_task( Ok(()) }) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::{Config, DEFAULT_CONFIG}; + use promkit_widgets::core::{render::RendererLayout, WidthMode}; + + fn viewer(input: &str) -> JsonViewer { + let config = Config::load_from(DEFAULT_CONFIG).unwrap(); + let json = json::deserialize(input, None).unwrap(); + JsonViewer { + state: json_widget::State { + document: Document::new(json.iter()), + config: config.json.stream, + }, + json, + keybinds: config.keybinds.on_json_viewer, + } + } + + #[test] + fn folding_and_navigation_preserve_complete_json_stream_output() { + let mut viewer = viewer("{\"nested\": [1, 2]}\n{\"message\": \"こんにちは\"}"); + let original = viewer.formatted_content(); + assert_eq!( + original, + "{\n \"nested\": [\n 1,\n 2\n ]\n},\n{\n \"message\": \"こんにちは\"\n}" + ); + + let toggle = viewer.keybinds.toggle.iter().next().unwrap().clone(); + viewer.handle_user_event(&toggle); + assert!(viewer + .state + .create_graphemes() + .graphemes + .to_string() + .contains('…')); + assert_eq!(viewer.formatted_content(), original); + + let tail = viewer.keybinds.move_to_tail.iter().next().unwrap().clone(); + viewer.handle_user_event(&tail); + assert!(viewer.state.document.visible_position() > 0); + let collapse = viewer.keybinds.collapse.iter().next().unwrap().clone(); + viewer.handle_user_event(&collapse); + assert_eq!(viewer.state.document.visible_position(), 0); + assert_eq!(viewer.formatted_content(), original); + + let expand = viewer.keybinds.expand.iter().next().unwrap().clone(); + viewer.handle_user_event(&expand); + assert!(!viewer + .state + .create_graphemes() + .graphemes + .to_string() + .contains('…')); + } + + #[tokio::test] + async fn filtered_results_use_the_configured_format_and_layout() { + let mut viewer = viewer("{\"items\": [1, 2, 3]}"); + let (guide, view) = viewer + .refresh_view_with_query((40, 6), ".items[] | . * 2".into()) + .await; + assert!(guide.is_none()); + assert_eq!(viewer.formatted_content(), "2,\n4,\n6"); + assert_eq!(view.unwrap().layout.width_mode, WidthMode::Wrap); + } + + #[tokio::test] + async fn null_and_invalid_filters_restore_the_input_and_show_a_guide() { + let mut viewer = viewer("{\"items\": [1, 2, 3]}"); + viewer + .refresh_view_with_query((40, 6), ".items".into()) + .await; + let (guide, _) = viewer + .refresh_view_with_query((40, 6), ".missing".into()) + .await; + assert!(matches!(guide, Some(GuideMessage::JqReturnedNull(_)))); + assert_eq!( + json::deserialize(&viewer.formatted_content(), None).unwrap(), + viewer.json + ); + let (guide, _) = viewer.refresh_view_with_query((40, 6), ".[".into()).await; + assert!(matches!(guide, Some(GuideMessage::JqFailed(_)))); + assert_eq!( + json::deserialize(&viewer.formatted_content(), None).unwrap(), + viewer.json + ); + } + + #[test] + fn long_documents_keep_the_selected_tail_visible_after_resize() { + let mut viewer = viewer(&format!("{:?}", (0..1000).collect::>())); + viewer.state.document.tail(); + let mut layout = RendererLayout::default(); + for (width, height) in [(40, 10), (20, 5), (80, 24)] { + let json = viewer.state.create_graphemes_in_viewport(width, height); + assert!(json.graphemes.logical_lines().len() <= height as usize); + let prepared = layout + .layout( + [ + ( + Index::QueryEditor, + promkit_widgets::core::grapheme::StyledGraphemes::from("❯❯ .").into(), + ), + (Index::JsonViewer, json), + ], + width, + height, + ) + .unwrap(); + let panes = prepared.panes(); + assert!(panes + .last() + .unwrap() + .iter() + .any(|row| row.to_string().contains(']'))); + assert!(prepared.visible_row_count() <= height as usize); + } + } +} diff --git a/src/main.rs b/src/main.rs index 03b8479..1e7a447 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,8 +10,8 @@ use clap::Parser; use promkit_widgets::{ core::{ crossterm, - grapheme::StyledGraphemes, render::{Renderer, SharedRenderer}, + CreatedGraphemes, }, listbox::{self, Listbox}, spinner::{self, Spinner}, @@ -238,13 +238,10 @@ async fn main() -> anyhow::Result<()> { let renderer = SharedRenderer::new( Renderer::try_new_with_graphemes( [ - ( - Index::QueryEditor, - query_editor.create_graphemes(terminal_size.0, terminal_size.1), - ), - (Index::Guide, StyledGraphemes::default()), - (Index::Completion, StyledGraphemes::default()), - (Index::JsonViewer, StyledGraphemes::default()), + (Index::QueryEditor, query_editor.create_graphemes()), + (Index::Guide, CreatedGraphemes::default()), + (Index::Completion, CreatedGraphemes::default()), + (Index::JsonViewer, CreatedGraphemes::default()), ] .into_iter(), true, @@ -320,12 +317,7 @@ async fn main() -> anyhow::Result<()> { // Spawn the guide task, which will manage the display of hints and guidance // to the user based on their interactions with the interface. - let guide_task = guide::start_guide_task( - guide_action_rx, - renderer.clone(), - ctx.clone(), - config.no_hint, - ); + let guide_task = guide::start_guide_task(guide_action_rx, renderer.clone(), config.no_hint); // Wrap the query editor and completion navigator in Arc> to allow shared mutable access across async tasks. let shared_query_editor = Arc::new(RwLock::new(query_editor)); diff --git a/src/query_editor.rs b/src/query_editor.rs index 54dac54..1435ed3 100644 --- a/src/query_editor.rs +++ b/src/query_editor.rs @@ -3,8 +3,7 @@ use std::sync::Arc; use promkit_widgets::{ core::{ crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers}, - grapheme::StyledGraphemes, - Widget, + CreatedGraphemes, Widget, }, text_editor, }; @@ -60,8 +59,8 @@ impl QueryEditor { } /// Create graphemes for rendering the query editor. - pub fn create_graphemes(&self, width: u16, height: u16) -> StyledGraphemes { - self.state.create_graphemes(width, height) + pub fn create_graphemes(&self) -> CreatedGraphemes { + self.state.create_graphemes() } /// Replace the current text of the query editor with the given text. @@ -169,7 +168,6 @@ pub fn start_query_editor_task( loop { tokio::select! { Some(action) = action_rx.recv() => { - let area = shared_ctx.area().await; let (editor_view, current_text) = { let mut editor = shared_editor.write().await; match action { @@ -194,7 +192,7 @@ pub fn start_query_editor_task( } } let current_text = editor.text(); - (editor.create_graphemes(area.0, area.1), current_text) + (editor.create_graphemes(), current_text) }; // If the text has changed, send it to the debounce channel for processing. diff --git a/src/runtime_tasks.rs b/src/runtime_tasks.rs index 74d4217..489e542 100644 --- a/src/runtime_tasks.rs +++ b/src/runtime_tasks.rs @@ -45,10 +45,7 @@ pub fn spawn_resize_render_task( let (editor_view, completion_view) = { let editor = shared_editor.read().await; let completion = shared_completion.read().await; - ( - editor.create_graphemes(area.0, area.1), - completion.create_graphemes(area.0, area.1), - ) + (editor.create_graphemes(), completion.create_graphemes()) }; shared_renderer .update([