1use std::borrow::Cow;
4use std::collections::{HashMap, HashSet};
5
6use rich::cells::{cell_len, char_cell_width};
7use rich::measure::Measurement;
8use rich::{Console, ConsoleOptions, Justify, Overflow, Renderable, Segment, Style, Table, Text};
9
10use super::table::{cell_text, TableOptions, TableView};
11use super::{quote_str, scalar_text, style, summary, Node, Path, Value, XmlKind};
12use crate::event::flatten as join_lines;
13
14const SPACE: &str = " ";
16const CONTINUE: &str = "│ ";
17const FORK: &str = "├── ";
18const END: &str = "└── ";
19
20#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
22pub enum View {
23 #[default]
25 Tree,
26 Table,
29}
30
31#[derive(Clone, Debug)]
49pub struct Explorer<'a> {
50 node: Cow<'a, Node>,
51 max_depth: Option<usize>,
52 max_length: Option<usize>,
53 max_string: Option<usize>,
54 show_paths: bool,
55 show_types: bool,
56 folded: HashSet<Path>,
57 highlighted: HashMap<Path, Style>,
58 root_label: Option<String>,
59 view: View,
60}
61
62enum Key<'n> {
63 Root,
64 Name(&'n str),
65 Index(usize),
66}
67
68enum Item<'n> {
69 Node {
70 key: Key<'n>,
71 node: &'n Node,
72 path: Path,
73 depth: usize,
74 },
75 More(usize),
76}
77
78struct Line {
80 prefix: String,
81 label: Text,
82 minimum: usize,
83}
84
85impl<'a> Explorer<'a> {
86 pub fn new(node: impl Into<Cow<'a, Node>>) -> Self {
88 Explorer {
89 node: node.into(),
90 max_depth: None,
91 max_length: None,
92 max_string: None,
93 show_paths: false,
94 show_types: false,
95 folded: HashSet::new(),
96 highlighted: HashMap::new(),
97 root_label: None,
98 view: View::Tree,
99 }
100 }
101
102 pub fn max_depth(mut self, depth: usize) -> Self {
105 self.max_depth = Some(depth);
106 self
107 }
108
109 pub fn max_length(mut self, length: usize) -> Self {
111 self.max_length = Some(length);
112 self
113 }
114
115 pub fn max_string(mut self, length: usize) -> Self {
118 self.max_string = Some(length);
119 self
120 }
121
122 pub fn show_paths(mut self, show: bool) -> Self {
124 self.show_paths = show;
125 self
126 }
127
128 pub fn show_types(mut self, show: bool) -> Self {
130 self.show_types = show;
131 self
132 }
133
134 pub fn fold(mut self, path: Path) -> Self {
136 self.folded.insert(path);
137 self
138 }
139
140 pub fn highlight(mut self, path: Path, style: Style) -> Self {
143 self.highlighted.insert(path, style);
144 self
145 }
146
147 pub fn root_label(mut self, label: impl Into<String>) -> Self {
149 self.root_label = Some(label.into());
150 self
151 }
152
153 pub fn view(mut self, view: View) -> Self {
155 self.view = view;
156 self
157 }
158
159 pub fn node(&self) -> &Node {
161 &self.node
162 }
163
164 fn is_folded(&self, path: &Path, depth: usize) -> bool {
165 self.max_depth.is_some_and(|max| depth >= max) || self.folded.contains(path)
166 }
167
168 fn expanded(&self, node: &Node, path: &Path, depth: usize) -> bool {
169 node.is_container() && !node.is_empty() && !self.is_folded(path, depth)
170 }
171
172 fn label(
175 &self,
176 console: &Console,
177 key: &Key<'_>,
178 node: &Node,
179 path: &Path,
180 expanded: bool,
181 available: usize,
182 ) -> (Text, usize) {
183 let mut head = Text::new("");
184 match key {
185 Key::Root => {
186 if let Some(label) = &self.root_label {
187 head.append(&super::escape_controls(label), None);
188 }
189 }
190 Key::Name(name) => {
191 let key_style = match node.meta.xml {
192 Some(XmlKind::Attribute) => "data.attribute",
193 Some(XmlKind::Text) => "data.comment",
194 _ => "json.key",
195 };
196 head.append(
197 &super::escape_controls(name),
198 Some(style(console, key_style).into()),
199 );
200 }
201 Key::Index(index) => {
202 head.append(
203 &format!("[{index}]"),
204 Some(style(console, "data.index").into()),
205 );
206 }
207 }
208
209 let root_summary = matches!(key, Key::Root) && self.root_label.is_none();
212 let value: Option<(String, &'static str, bool)> = if node.is_container() {
213 (!expanded || root_summary).then(|| (summary(node), "data.summary", false))
214 } else {
215 let is_string = matches!(node.value, Value::String(_));
216 let text = match &node.value {
217 Value::String(s) => fit_quoted(s, self.max_string, None),
218 other => scalar_text(other, true).0,
219 };
220 Some((text, scalar_text(&node.value, true).1, is_string))
221 };
222 let separator = if value.is_some() && !head.is_empty() {
223 ": "
224 } else {
225 ""
226 };
227
228 let mut badges = Text::new("");
229 if let Some(anchor) = &node.meta.anchor {
230 badges.append(
231 &format!(" &{}", super::escape_controls(anchor)),
232 Some(style(console, "data.anchor").into()),
233 );
234 }
235 if let Some(alias) = &node.meta.alias {
236 badges.append(
237 &format!(" *{}", super::escape_controls(alias)),
238 Some(style(console, "data.alias").into()),
239 );
240 }
241 if self.show_types {
242 badges.append(
243 &format!(" ({})", node.type_name()),
244 Some(style(console, "data.type").into()),
245 );
246 }
247
248 let mut extras = Text::new("");
249 if let Some(comment) = &node.meta.comment {
250 let comment = super::escape_controls(&comment.replace('\n', " "));
251 extras.append(
252 &format!(" # {comment}"),
253 Some(style(console, "data.comment").into()),
254 );
255 }
256 if self.show_paths && !expanded && !path.is_root() {
257 extras.append(
258 &format!(" {path}"),
259 Some(style(console, "data.path").into()),
260 );
261 }
262
263 let fixed = head.cell_len() + cell_len(separator) + badges.cell_len();
264 let minimum = head.cell_len() + 1;
265 let mut text = head;
266 text.append(separator, None);
267 if let Some((mut value, value_style, is_string)) = value {
268 if is_string && fixed + cell_len(&value) > available {
269 if let (Some(budget), Value::String(s)) =
270 (available.checked_sub(fixed), &node.value)
271 {
272 value = fit_quoted(s, self.max_string, Some(budget));
273 }
274 }
275 text.append(&value, Some(style(console, value_style).into()));
276 }
277 let text = text.append_text(&badges).append_text(&extras);
278 let mut text = text;
279 text.truncate(available, Some(Overflow::Ellipsis), false);
280 if let Some(style) = self.highlighted.get(path) {
281 let end = text.plain().len();
282 text.stylize(style.clone(), 0, end);
283 }
284 (text, minimum)
285 }
286
287 fn tree_lines(&self, console: &Console, width: usize) -> Vec<Line> {
288 let mut lines = Vec::new();
289 let mut stack: Vec<(Item<'_>, String, String)> = vec![(
290 Item::Node {
291 key: Key::Root,
292 node: &self.node,
293 path: Path::root(),
294 depth: 0,
295 },
296 String::new(),
297 String::new(),
298 )];
299 while let Some((item, first, rest)) = stack.pop() {
300 let available = width.saturating_sub(cell_len(&first));
301 let (key, node, path, depth) = match item {
302 Item::More(count) => {
303 let mut label =
304 Text::styled(format!("… {count} more"), style(console, "data.summary"));
305 label.truncate(available, Some(Overflow::Ellipsis), false);
306 lines.push(Line {
307 prefix: first,
308 label,
309 minimum: 1,
310 });
311 continue;
312 }
313 Item::Node {
314 key,
315 node,
316 path,
317 depth,
318 } => (key, node, path, depth),
319 };
320 let expanded = self.expanded(node, &path, depth);
321 let (label, minimum) = self.label(console, &key, node, &path, expanded, available);
322 lines.push(Line {
323 prefix: first,
324 label,
325 minimum,
326 });
327 if !expanded {
328 continue;
329 }
330 let limit = self.max_length.unwrap_or(usize::MAX);
331 let mut children: Vec<Item<'_>> = match &node.value {
332 Value::Seq(items) => items
333 .iter()
334 .enumerate()
335 .take(limit)
336 .map(|(i, child)| Item::Node {
337 key: Key::Index(i),
338 node: child,
339 path: path.child_index(i),
340 depth: depth + 1,
341 })
342 .collect(),
343 Value::Map(entries) => entries
344 .iter()
345 .take(limit)
346 .map(|(k, child)| Item::Node {
347 key: Key::Name(k),
348 node: child,
349 path: path.child_key(k),
350 depth: depth + 1,
351 })
352 .collect(),
353 _ => Vec::new(),
354 };
355 if node.len() > limit {
356 children.push(Item::More(node.len() - limit));
357 }
358 let last = children.len().saturating_sub(1);
359 for (index, child) in children.into_iter().enumerate().rev() {
360 let (fork, space) = if index == last {
361 (END, SPACE)
362 } else {
363 (FORK, CONTINUE)
364 };
365 stack.push((child, format!("{rest}{fork}"), format!("{rest}{space}")));
366 }
367 }
368 lines
369 }
370
371 fn table_view(&self, console: &Console) -> Table {
372 let records = matches!(&self.node.value, Value::Seq(items)
373 if !items.is_empty() && items.iter().all(|i| matches!(i.value, Value::Map(_))));
374 if records {
375 let options = TableOptions {
376 max_rows: self.max_length,
377 max_string: self.max_string,
378 ..TableOptions::default()
379 };
380 return TableView::new(&*self.node)
381 .options(options)
382 .to_table(console);
383 }
384 let mut table = Table::new();
385 table.add_column_text(Text::new("path"), Justify::Left);
386 table.add_column_text(Text::new("value"), Justify::Left);
387 if self.show_types {
388 table.add_column_text(Text::new("type"), Justify::Left);
389 }
390 let dim = |text: String| Text::styled(text, style(console, "data.path"));
391 let mut stack: Vec<(&Node, Path, usize)> = vec![(&self.node, Path::root(), 0)];
392 while let Some((node, path, depth)) = stack.pop() {
393 if depth != usize::MAX && self.expanded(node, &path, depth) {
394 let limit = self.max_length.unwrap_or(usize::MAX);
395 if node.len() > limit {
396 stack.push((&self.node, path.clone(), usize::MAX));
398 }
399 match &node.value {
400 Value::Seq(items) => {
401 for (i, item) in items.iter().enumerate().take(limit).rev() {
402 stack.push((item, path.child_index(i), depth + 1));
403 }
404 }
405 Value::Map(entries) => {
406 for (k, v) in entries.iter().take(limit).rev() {
407 stack.push((v, path.child_key(k), depth + 1));
408 }
409 }
410 _ => {}
411 }
412 continue;
413 }
414 let shown_path = if path.is_root() {
415 "(root)".to_string()
416 } else {
417 path.to_string()
418 };
419 let mut row = vec![Text::new(super::escape_controls(&shown_path))];
420 if depth == usize::MAX {
421 let container = self.node.at(&path).map_or(0, Node::len);
423 let limit = self.max_length.unwrap_or(usize::MAX);
424 row.push(dim(format!("… {} more", container.saturating_sub(limit))));
425 } else if node.is_container() {
426 row.push(Text::styled(summary(node), style(console, "data.summary")));
427 } else {
428 row.push(cell_text(console, node, self.max_string));
429 }
430 if self.show_types {
431 row.push(dim(if depth == usize::MAX {
432 String::new()
433 } else {
434 node.type_name().to_string()
435 }));
436 }
437 table.add_row_text(row);
438 }
439 table
440 }
441}
442
443pub(crate) fn fit_quoted(raw: &str, max_chars: Option<usize>, budget: Option<usize>) -> String {
446 let (content, cut) = match max_chars.and_then(|n| raw.char_indices().nth(n)) {
447 Some((i, _)) => (&raw[..i], true),
448 None => (raw, false),
449 };
450 let quoted = quote_str(content);
451 let escaped = "ed[1..quoted.len() - 1];
452 let whole = cell_len(escaped) + 2 + usize::from(cut);
453 match budget {
454 Some(budget) if whole > budget => {
455 let allowed = budget.saturating_sub(3);
456 let mut kept = String::new();
457 let mut used = 0;
458 for c in escaped.chars() {
459 let w = char_cell_width(c);
460 if used + w > allowed {
461 break;
462 }
463 used += w;
464 kept.push(c);
465 }
466 let trailing = kept.chars().rev().take_while(|c| *c == '\\').count();
468 if trailing % 2 == 1 {
469 kept.pop();
470 }
471 format!("\"{kept}…\"")
472 }
473 _ if cut => format!("\"{escaped}…\""),
474 _ => quoted,
475 }
476}
477
478impl Renderable for Explorer<'_> {
479 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
480 if options.max_width == 0 {
481 return Vec::new();
482 }
483 if self.view == View::Table {
484 return self.table_view(console).rich_render(console, options);
485 }
486 let guide = style(console, "tree.line");
487 let rows = self
488 .tree_lines(console, options.max_width)
489 .into_iter()
490 .map(|line| {
491 let mut row = Vec::new();
492 if !line.prefix.is_empty() {
493 row.push(Segment::new(line.prefix, Some(guide.clone())));
494 }
495 row.extend(line.label.render(console.theme(), &Style::new()));
496 row
497 })
498 .collect();
499 join_lines(rows)
500 }
501
502 fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
503 if self.view == View::Table {
504 return Measurement::new(options.max_width, options.max_width);
505 }
506 let lines = self.tree_lines(console, usize::MAX / 2);
507 let (mut minimum, mut maximum) = (0, 0);
508 for line in &lines {
509 let prefix = cell_len(&line.prefix);
510 minimum = minimum.max(prefix + line.minimum);
511 maximum = maximum.max(prefix + line.label.cell_len());
512 }
513 Measurement::new(minimum.min(maximum), maximum).with_maximum(options.max_width)
514 }
515}