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}