Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ and [jiq](https://github.com/fiatjaf/jiq).
- [Object Identifier-Index](https://jqlang.github.io/jq/manual/#object-identifier-index)
- [Array Index](https://jqlang.github.io/jq/manual/#array-index)
- Hint message to evaluate the filter
- Result summary showing the current result's type and length
(e.g. `object · 3 keys`) in the status line

## Installation

Expand Down
5 changes: 5 additions & 0 deletions src/guide.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ pub enum GuideMessage {
NoSuggestionFound(String),
JqReturnedNull(String),
JqFailed(String),
/// Type/length summary of the current jq result (e.g. `object · 3 keys`).
ResultSummary(String),
}

/// Represent an action to be performed on the guide.
Expand Down Expand Up @@ -66,6 +68,9 @@ fn message_to_state(message: GuideMessage) -> status::State {
GuideMessage::JqFailed(e) => {
status::State::new(format!("jq failed: `{e}`"), Severity::Error)
}
GuideMessage::ResultSummary(summary) => {
status::State::new(format!("result: {summary}"), Severity::Success)
}
}
}

Expand Down
72 changes: 72 additions & 0 deletions src/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,75 @@ pub fn run_jaq(

Ok(ret)
}

/// Summarize a jq result stream as a short `type · length` string, mirroring
/// what `| type` and `| length` would report. Used for a status-line hint so
/// users don't have to append those filters manually.
///
/// A single value is described by its type and (where meaningful) its length;
/// a multi-value stream is reported as a count.
pub fn summarize(values: &[serde_json::Value]) -> String {
match values {
[] => "empty (0 results)".to_string(),
[value] => summarize_value(value),
many => format!("stream · {} values", many.len()),
}
}

fn summarize_value(value: &serde_json::Value) -> String {
match value {
Value::Object(map) => format!("object · {} {}", map.len(), pluralize(map.len(), "key")),
Value::Array(items) => {
format!("array · {} {}", items.len(), pluralize(items.len(), "item"))
}
Value::String(s) => {
let count = s.chars().count();
format!("string · {} {}", count, pluralize(count, "char"))
}
Value::Number(_) => "number".to_string(),
Value::Bool(_) => "boolean".to_string(),
Value::Null => "null".to_string(),
}
}

fn pluralize(count: usize, noun: &str) -> String {
if count == 1 {
noun.to_string()
} else {
format!("{noun}s")
}
}

#[cfg(test)]
mod tests {
use super::*;
use promkit_widgets::serde_json::json;

#[test]
fn summarize_object_array_string() {
assert_eq!(summarize(&[json!({"a": 1, "b": 2})]), "object · 2 keys");
assert_eq!(summarize(&[json!({"a": 1})]), "object · 1 key");
assert_eq!(summarize(&[json!([1, 2, 3])]), "array · 3 items");
assert_eq!(summarize(&[json!([1])]), "array · 1 item");
assert_eq!(summarize(&[json!("hello")]), "string · 5 chars");
}

#[test]
fn summarize_scalars() {
assert_eq!(summarize(&[json!(42)]), "number");
assert_eq!(summarize(&[json!(true)]), "boolean");
assert_eq!(summarize(&[json!(null)]), "null");
}

#[test]
fn summarize_stream_and_empty() {
assert_eq!(summarize(&[json!(1), json!(2)]), "stream · 2 values");
assert_eq!(summarize(&[]), "empty (0 results)");
}

#[test]
fn summarize_string_counts_unicode_scalar_values() {
// "café" is 4 chars even though 'é' is multi-byte.
assert_eq!(summarize(&[json!("café")]), "string · 4 chars");
}
}
11 changes: 6 additions & 5 deletions src/json_viewer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,15 @@ impl JsonViewer {
) -> (Option<GuideMessage>, Option<StyledGraphemes>) {
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));

// `all` is vacuously true for an empty result, so this also
// covers the "filter produced nothing" case.
let guide = if ret.iter().all(|val| *val == Value::Null) {
self.state.stream = JsonStream::new(self.json.iter());
Some(GuideMessage::JqReturnedNull(input))
} else {
self.state.stream = JsonStream::new(ret.iter());
}
Some(GuideMessage::ResultSummary(json::summarize(&ret)))
};

(guide, Some(self.state.create_graphemes(area.0, area.1)))
}
Expand Down