296 lines · 10.9 KB · Rust
RawDownload
//! # JSON Output for REPL//!//! This module provides an interactive JSON viewer for displaying JSON data in the REPL.//! It supports collapsible/expandable tree views for objects and arrays, with syntax//! highlighting for different value types. use std::collections::HashMap;use std::collections::hash_map::DefaultHasher;use std::hash::{Hash, Hasher}; use gpui::{App, ClipboardItem, Context, Entity, Window, div, prelude::*};use language::Buffer;use serde_json::Value;use ui::{Disclosure, prelude::*}; use crate::outputs::OutputContent; pub struct JsonView {    root: Value,    expanded_paths: HashMap<String, bool>,} impl JsonView {    pub fn from_value(value: Value) -> anyhow::Result<Self> {        let mut expanded_paths = HashMap::new();        expanded_paths.insert("root".to_string(), true);         Ok(Self {            root: value,            expanded_paths,        })    }     fn toggle_path(&mut self, path: &str, cx: &mut Context<Self>) {        let current = self.expanded_paths.get(path).copied().unwrap_or(false);        self.expanded_paths.insert(path.to_string(), !current);        cx.notify();    }     fn is_expanded(&self, path: &str) -> bool {        self.expanded_paths.get(path).copied().unwrap_or(false)    }     fn path_hash(path: &str) -> u64 {        let mut hasher = DefaultHasher::new();        path.hash(&mut hasher);        hasher.finish()    }     fn render_value(        &self,        path: String,        key: Option<&str>,        value: &Value,        depth: usize,        window: &mut Window,        cx: &mut Context<Self>,    ) -> AnyElement {        let indent = depth * 12;         match value {            Value::Object(map) if map.is_empty() => {                self.render_line(path, key, "{}", depth, Color::Muted, window, cx)            }            Value::Object(map) => {                let is_expanded = self.is_expanded(&path);                let preview = if is_expanded {                    String::new()                } else {                    format!("{{ {} fields }}", map.len())                };                 v_flex()                    .child(                        h_flex()                            .gap_1()                            .pl(px(indent as f32))                            .cursor_pointer()                            .on_mouse_down(                                gpui::MouseButton::Left,                                cx.listener({                                    let path = path.clone();                                    move |this, _, _, cx| {                                        this.toggle_path(&path, cx);                                    }                                }),                            )                            .child(Disclosure::new(                                ("json-disclosure", Self::path_hash(&path)),                                is_expanded,                            ))                            .child(                                h_flex()                                    .gap_1()                                    .when_some(key, |this, k| {                                        this.child(                                            Label::new(format!("{}: ", k)).color(Color::Accent),                                        )                                    })                                    .when(!is_expanded, |this| {                                        this.child(Label::new("{").color(Color::Muted))                                            .child(                                                Label::new(format!(" {} ", preview))                                                    .color(Color::Muted),                                            )                                            .child(Label::new("}").color(Color::Muted))                                    }),                            ),                    )                    .when(is_expanded, |this| {                        this.children(                            map.iter()                                .map(|(k, v)| {                                    let child_path = format!("{}.{}", path, k);                                    self.render_value(child_path, Some(k), v, depth + 1, window, cx)                                })                                .collect::<Vec<_>>(),                        )                    })                    .into_any_element()            }            Value::Array(arr) if arr.is_empty() => {                self.render_line(path, key, "[]", depth, Color::Muted, window, cx)            }            Value::Array(arr) => {                let is_expanded = self.is_expanded(&path);                let preview = if is_expanded {                    String::new()                } else {                    format!("[ {} items ]", arr.len())                };                 v_flex()                    .child(                        h_flex()                            .gap_1()                            .pl(px(indent as f32))                            .cursor_pointer()                            .on_mouse_down(                                gpui::MouseButton::Left,                                cx.listener({                                    let path = path.clone();                                    move |this, _, _, cx| {                                        this.toggle_path(&path, cx);                                    }                                }),                            )                            .child(Disclosure::new(                                ("json-disclosure", Self::path_hash(&path)),                                is_expanded,                            ))                            .child(                                h_flex()                                    .gap_1()                                    .when_some(key, |this, k| {                                        this.child(                                            Label::new(format!("{}: ", k)).color(Color::Accent),                                        )                                    })                                    .when(!is_expanded, |this| {                                        this.child(Label::new("[").color(Color::Muted))                                            .child(                                                Label::new(format!(" {} ", preview))                                                    .color(Color::Muted),                                            )                                            .child(Label::new("]").color(Color::Muted))                                    }),                            ),                    )                    .when(is_expanded, |this| {                        this.children(                            arr.iter()                                .enumerate()                                .map(|(i, v)| {                                    let child_path = format!("{}[{}]", path, i);                                    self.render_value(child_path, None, v, depth + 1, window, cx)                                })                                .collect::<Vec<_>>(),                        )                    })                    .into_any_element()            }            Value::String(s) => {                let display = format!("\"{}\"", s);                self.render_line(path, key, &display, depth, Color::Success, window, cx)            }            Value::Number(n) => {                let display = n.to_string();                self.render_line(path, key, &display, depth, Color::Modified, window, cx)            }            Value::Bool(b) => {                let display = b.to_string();                self.render_line(path, key, &display, depth, Color::Info, window, cx)            }            Value::Null => self.render_line(path, key, "null", depth, Color::Disabled, window, cx),        }    }     fn render_line(        &self,        _path: String,        key: Option<&str>,        value: &str,        depth: usize,        color: Color,        _window: &mut Window,        _cx: &mut Context<Self>,    ) -> AnyElement {        let indent = depth * 16;         h_flex()            .pl(px(indent as f32))            .gap_1()            .when_some(key, |this, k| {                this.child(Label::new(format!("{}: ", k)).color(Color::Accent))            })            .child(Label::new(value.to_string()).color(color))            .into_any_element()    }} impl Render for JsonView {    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {        let root_clone = self.root.clone();        let root_element = self.render_value("root".to_string(), None, &root_clone, 0, window, cx);        div().w_full().child(root_element)    }} impl OutputContent for JsonView {    fn clipboard_content(&self, _window: &Window, _cx: &App) -> Option<ClipboardItem> {        serde_json::to_string_pretty(&self.root)            .ok()            .map(ClipboardItem::new_string)    }     fn has_clipboard_content(&self, _window: &Window, _cx: &App) -> bool {        true    }     fn has_buffer_content(&self, _window: &Window, _cx: &App) -> bool {        true    }     fn buffer_content(&mut self, _window: &mut Window, cx: &mut App) -> Option<Entity<Buffer>> {        let json_text = serde_json::to_string_pretty(&self.root).ok()?;        let buffer = cx.new(|cx| {            let mut buffer =                Buffer::local(json_text, cx).with_language(language::PLAIN_TEXT.clone(), cx);            buffer.set_capability(language::Capability::ReadOnly, cx);            buffer        });        Some(buffer)    }} #[cfg(test)]mod tests {    use super::*;     #[test]    fn test_json_view_from_value_root_expanded() {        let view = JsonView::from_value(serde_json::json!({"key": "value"})).unwrap();        assert!(            view.is_expanded("root"),            "root should be expanded by default"        );    }     #[test]    fn test_json_view_is_expanded_unknown_path() {        let view = JsonView::from_value(serde_json::json!({"key": "value"})).unwrap();        assert!(            !view.is_expanded("root.key"),            "non-root paths should not be expanded by default"        );        assert!(            !view.is_expanded("nonexistent"),            "unknown paths should not be expanded"        );    }     #[gpui::test]    fn test_json_view_toggle_path(cx: &mut gpui::App) {        let view =            cx.new(|_cx| JsonView::from_value(serde_json::json!({"nested": {"a": 1}})).unwrap());         view.update(cx, |view, cx| {            assert!(!view.is_expanded("root.nested"));            view.toggle_path("root.nested", cx);            assert!(view.is_expanded("root.nested"));            view.toggle_path("root.nested", cx);            assert!(!view.is_expanded("root.nested"));        });    }}