json.rs

  1//! # JSON Output for REPL
  2//!
  3//! This module provides an interactive JSON viewer for displaying JSON data in the REPL.
  4//! It supports collapsible/expandable tree views for objects and arrays, with syntax
  5//! highlighting for different value types.
  6
  7use std::collections::HashMap;
  8use std::collections::hash_map::DefaultHasher;
  9use std::hash::{Hash, Hasher};
 10
 11use gpui::{App, ClipboardItem, Context, Entity, Window, div, prelude::*};
 12use language::Buffer;
 13use serde_json::Value;
 14use ui::{Disclosure, prelude::*};
 15
 16use crate::outputs::OutputContent;
 17
 18pub struct JsonView {
 19    root: Value,
 20    expanded_paths: HashMap<String, bool>,
 21}
 22
 23impl JsonView {
 24    pub fn from_value(value: Value) -> anyhow::Result<Self> {
 25        let mut expanded_paths = HashMap::new();
 26        expanded_paths.insert("root".to_string(), true);
 27
 28        Ok(Self {
 29            root: value,
 30            expanded_paths,
 31        })
 32    }
 33
 34    fn toggle_path(&mut self, path: &str, cx: &mut Context<Self>) {
 35        let current = self.expanded_paths.get(path).copied().unwrap_or(false);
 36        self.expanded_paths.insert(path.to_string(), !current);
 37        cx.notify();
 38    }
 39
 40    fn is_expanded(&self, path: &str) -> bool {
 41        self.expanded_paths.get(path).copied().unwrap_or(false)
 42    }
 43
 44    fn path_hash(path: &str) -> u64 {
 45        let mut hasher = DefaultHasher::new();
 46        path.hash(&mut hasher);
 47        hasher.finish()
 48    }
 49
 50    fn render_value(
 51        &self,
 52        path: String,
 53        key: Option<&str>,
 54        value: &Value,
 55        depth: usize,
 56        window: &mut Window,
 57        cx: &mut Context<Self>,
 58    ) -> AnyElement {
 59        let indent = depth * 12;
 60
 61        match value {
 62            Value::Object(map) if map.is_empty() => {
 63                self.render_line(path, key, "{}", depth, Color::Muted, window, cx)
 64            }
 65            Value::Object(map) => {
 66                let is_expanded = self.is_expanded(&path);
 67                let preview = if is_expanded {
 68                    String::new()
 69                } else {
 70                    format!("{{ {} fields }}", map.len())
 71                };
 72
 73                v_flex()
 74                    .child(
 75                        h_flex()
 76                            .gap_1()
 77                            .pl(px(indent as f32))
 78                            .cursor_pointer()
 79                            .on_mouse_down(
 80                                gpui::MouseButton::Left,
 81                                cx.listener({
 82                                    let path = path.clone();
 83                                    move |this, _, _, cx| {
 84                                        this.toggle_path(&path, cx);
 85                                    }
 86                                }),
 87                            )
 88                            .child(Disclosure::new(
 89                                ("json-disclosure", Self::path_hash(&path)),
 90                                is_expanded,
 91                            ))
 92                            .child(
 93                                h_flex()
 94                                    .gap_1()
 95                                    .when_some(key, |this, k| {
 96                                        this.child(
 97                                            Label::new(format!("{}: ", k)).color(Color::Accent),
 98                                        )
 99                                    })
100                                    .when(!is_expanded, |this| {
101                                        this.child(Label::new("{").color(Color::Muted))
102                                            .child(
103                                                Label::new(format!(" {} ", preview))
104                                                    .color(Color::Muted),
105                                            )
106                                            .child(Label::new("}").color(Color::Muted))
107                                    }),
108                            ),
109                    )
110                    .when(is_expanded, |this| {
111                        this.children(
112                            map.iter()
113                                .map(|(k, v)| {
114                                    let child_path = format!("{}.{}", path, k);
115                                    self.render_value(child_path, Some(k), v, depth + 1, window, cx)
116                                })
117                                .collect::<Vec<_>>(),
118                        )
119                    })
120                    .into_any_element()
121            }
122            Value::Array(arr) if arr.is_empty() => {
123                self.render_line(path, key, "[]", depth, Color::Muted, window, cx)
124            }
125            Value::Array(arr) => {
126                let is_expanded = self.is_expanded(&path);
127                let preview = if is_expanded {
128                    String::new()
129                } else {
130                    format!("[ {} items ]", arr.len())
131                };
132
133                v_flex()
134                    .child(
135                        h_flex()
136                            .gap_1()
137                            .pl(px(indent as f32))
138                            .cursor_pointer()
139                            .on_mouse_down(
140                                gpui::MouseButton::Left,
141                                cx.listener({
142                                    let path = path.clone();
143                                    move |this, _, _, cx| {
144                                        this.toggle_path(&path, cx);
145                                    }
146                                }),
147                            )
148                            .child(Disclosure::new(
149                                ("json-disclosure", Self::path_hash(&path)),
150                                is_expanded,
151                            ))
152                            .child(
153                                h_flex()
154                                    .gap_1()
155                                    .when_some(key, |this, k| {
156                                        this.child(
157                                            Label::new(format!("{}: ", k)).color(Color::Accent),
158                                        )
159                                    })
160                                    .when(!is_expanded, |this| {
161                                        this.child(Label::new("[").color(Color::Muted))
162                                            .child(
163                                                Label::new(format!(" {} ", preview))
164                                                    .color(Color::Muted),
165                                            )
166                                            .child(Label::new("]").color(Color::Muted))
167                                    }),
168                            ),
169                    )
170                    .when(is_expanded, |this| {
171                        this.children(
172                            arr.iter()
173                                .enumerate()
174                                .map(|(i, v)| {
175                                    let child_path = format!("{}[{}]", path, i);
176                                    self.render_value(child_path, None, v, depth + 1, window, cx)
177                                })
178                                .collect::<Vec<_>>(),
179                        )
180                    })
181                    .into_any_element()
182            }
183            Value::String(s) => {
184                let display = format!("\"{}\"", s);
185                self.render_line(path, key, &display, depth, Color::Success, window, cx)
186            }
187            Value::Number(n) => {
188                let display = n.to_string();
189                self.render_line(path, key, &display, depth, Color::Modified, window, cx)
190            }
191            Value::Bool(b) => {
192                let display = b.to_string();
193                self.render_line(path, key, &display, depth, Color::Info, window, cx)
194            }
195            Value::Null => self.render_line(path, key, "null", depth, Color::Disabled, window, cx),
196        }
197    }
198
199    fn render_line(
200        &self,
201        _path: String,
202        key: Option<&str>,
203        value: &str,
204        depth: usize,
205        color: Color,
206        _window: &mut Window,
207        _cx: &mut Context<Self>,
208    ) -> AnyElement {
209        let indent = depth * 16;
210
211        h_flex()
212            .pl(px(indent as f32))
213            .gap_1()
214            .when_some(key, |this, k| {
215                this.child(Label::new(format!("{}: ", k)).color(Color::Accent))
216            })
217            .child(Label::new(value.to_string()).color(color))
218            .into_any_element()
219    }
220}
221
222impl Render for JsonView {
223    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
224        let root_clone = self.root.clone();
225        let root_element = self.render_value("root".to_string(), None, &root_clone, 0, window, cx);
226        div().w_full().child(root_element)
227    }
228}
229
230impl OutputContent for JsonView {
231    fn clipboard_content(&self, _window: &Window, _cx: &App) -> Option<ClipboardItem> {
232        serde_json::to_string_pretty(&self.root)
233            .ok()
234            .map(ClipboardItem::new_string)
235    }
236
237    fn has_clipboard_content(&self, _window: &Window, _cx: &App) -> bool {
238        true
239    }
240
241    fn has_buffer_content(&self, _window: &Window, _cx: &App) -> bool {
242        true
243    }
244
245    fn buffer_content(&mut self, _window: &mut Window, cx: &mut App) -> Option<Entity<Buffer>> {
246        let json_text = serde_json::to_string_pretty(&self.root).ok()?;
247        let buffer = cx.new(|cx| {
248            let mut buffer =
249                Buffer::local(json_text, cx).with_language(language::PLAIN_TEXT.clone(), cx);
250            buffer.set_capability(language::Capability::ReadOnly, cx);
251            buffer
252        });
253        Some(buffer)
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    #[test]
262    fn test_json_view_from_value_root_expanded() {
263        let view = JsonView::from_value(serde_json::json!({"key": "value"})).unwrap();
264        assert!(
265            view.is_expanded("root"),
266            "root should be expanded by default"
267        );
268    }
269
270    #[test]
271    fn test_json_view_is_expanded_unknown_path() {
272        let view = JsonView::from_value(serde_json::json!({"key": "value"})).unwrap();
273        assert!(
274            !view.is_expanded("root.key"),
275            "non-root paths should not be expanded by default"
276        );
277        assert!(
278            !view.is_expanded("nonexistent"),
279            "unknown paths should not be expanded"
280        );
281    }
282
283    #[gpui::test]
284    fn test_json_view_toggle_path(cx: &mut gpui::App) {
285        let view =
286            cx.new(|_cx| JsonView::from_value(serde_json::json!({"nested": {"a": 1}})).unwrap());
287
288        view.update(cx, |view, cx| {
289            assert!(!view.is_expanded("root.nested"));
290            view.toggle_path("root.nested", cx);
291            assert!(view.is_expanded("root.nested"));
292            view.toggle_path("root.nested", cx);
293            assert!(!view.is_expanded("root.nested"));
294        });
295    }
296}