Skip to main content

vix_edit_value/
lib.rs

1//! The structured-value editor: a foldable tree for JSON and YAML.
2//!
3//! Vix's Tools menu offers *Edit JSON* and *Edit YAML* commands that parse the
4//! active buffer into a tree of objects, arrays, and scalars, then let the user
5//! fold/unfold containers, navigate, and edit scalar values — code folding, but
6//! for structured data. Saving serializes the tree back to JSON or YAML.
7//!
8//! JSON and YAML share one model: both parse (via `serde_yaml`, which also reads
9//! JSON) into [`Val`], and only the serializer differs (a key-order-preserving
10//! JSON pretty-printer, or `serde_yaml` for YAML). The [`Format`] chosen at open
11//! time decides the title and the serializer.
12//!
13//! Like the other editors, this module owns the data, cursor, fold state, and an
14//! undo history, and interprets keys itself, returning an [`Outcome`] telling the
15//! host when to close or save.
16//!
17//! Keys: **↑/↓** (or `k`/`j`) move; **←/→** (or `h`/`l`) collapse/expand (or step
18//! to parent/first child); **Enter**/**F2** edit the selected scalar (or toggle a
19//! container); **Space** toggles fold; **u**/**Ctrl+R** undo/redo; **Ctrl+S**
20//! save; **Esc**/**q** close.
21
22#![warn(clippy::pedantic)]
23#![forbid(unsafe_code)]
24#![deny(missing_docs)]
25
26use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
27
28/// Which serialization the buffer uses.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum Format {
31    /// JSON (pretty-printed on save, preserving key order).
32    Json,
33    /// YAML.
34    Yaml,
35}
36
37/// What the host should do after the tree handled a key.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum Outcome {
40    /// Handled internally; nothing further for the host to do.
41    Consumed,
42    /// The user asked to close the editor (Esc/`q`).
43    Close,
44    /// The user asked to save (Ctrl+S); the host should persist the tree.
45    Save,
46}
47
48/// A structured value: scalar, array, or object (object key order preserved).
49#[derive(Clone, PartialEq)]
50enum Val {
51    Null,
52    Bool(bool),
53    /// A number kept in its textual form so it round-trips unchanged.
54    Num(String),
55    Str(String),
56    Arr {
57        collapsed: bool,
58        items: Vec<Val>,
59    },
60    Obj {
61        collapsed: bool,
62        entries: Vec<(String, Val)>,
63    },
64}
65
66/// One step of a path locating a node within the tree.
67#[derive(Clone, PartialEq)]
68enum Seg {
69    Key(String),
70    Index(usize),
71}
72
73/// Whether a display row is a scalar leaf or a container (with collapse state).
74#[derive(Clone, Copy, PartialEq)]
75enum Kind {
76    Scalar,
77    Container(bool),
78}
79
80/// A flattened display row computed from the tree and its fold state.
81struct Row {
82    depth: usize,
83    label: String,
84    value: String,
85    kind: Kind,
86    path: Vec<Seg>,
87}
88
89/// Current interaction mode.
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91enum Mode {
92    Normal,
93    Edit,
94}
95
96/// Maximum number of undo steps retained.
97const HISTORY_CAP: usize = 200;
98
99/// A foldable, editable JSON/YAML value tree with a cursor and undo history.
100pub struct Tree {
101    root: Val,
102    format: Format,
103    rows: Vec<Row>,
104    sel: usize,
105    scroll: usize,
106    dirty: bool,
107    mode: Mode,
108    edit_buf: String,
109    undo: Vec<Val>,
110    redo: Vec<Val>,
111}
112
113impl Tree {
114    /// Parse `text` as JSON or YAML into a value tree. Returns `None` when the
115    /// text does not parse (the host can warn). YAML's parser also accepts JSON.
116    #[must_use]
117    pub fn from_text(text: &str, format: Format) -> Option<Self> {
118        let parsed: serde_yaml::Value = serde_yaml::from_str(text).ok()?;
119        let mut tree = Tree {
120            root: from_yaml(&parsed),
121            format,
122            rows: Vec::new(),
123            sel: 0,
124            scroll: 0,
125            dirty: false,
126            mode: Mode::Normal,
127            edit_buf: String::new(),
128            undo: Vec::new(),
129            redo: Vec::new(),
130        };
131        tree.rebuild();
132        Some(tree)
133    }
134
135    /// The chosen format.
136    #[must_use]
137    pub fn format(&self) -> Format {
138        self.format
139    }
140
141    /// Number of visible rows.
142    #[must_use]
143    pub fn row_count(&self) -> usize {
144        self.rows.len()
145    }
146
147    /// The selected row index.
148    #[must_use]
149    pub fn sel(&self) -> usize {
150        self.sel
151    }
152
153    /// The first visible row (scroll offset).
154    #[must_use]
155    pub fn scroll(&self) -> usize {
156        self.scroll
157    }
158
159    /// Whether there are unsaved edits.
160    #[must_use]
161    pub fn is_dirty(&self) -> bool {
162        self.dirty
163    }
164
165    /// Whether a scalar is currently being edited.
166    #[must_use]
167    pub fn is_editing(&self) -> bool {
168        matches!(self.mode, Mode::Edit)
169    }
170
171    /// The in-progress edit text (valid while [`Tree::is_editing`]).
172    #[must_use]
173    pub fn edit_buffer(&self) -> &str {
174        &self.edit_buf
175    }
176
177    /// Indentation depth of visible row `i`.
178    #[must_use]
179    pub fn depth(&self, i: usize) -> usize {
180        self.rows.get(i).map_or(0, |r| r.depth)
181    }
182
183    /// The label (key or index) of visible row `i`.
184    #[must_use]
185    pub fn label(&self, i: usize) -> &str {
186        self.rows.get(i).map_or("", |r| r.label.as_str())
187    }
188
189    /// The value/summary text of visible row `i`.
190    #[must_use]
191    pub fn value(&self, i: usize) -> &str {
192        self.rows.get(i).map_or("", |r| r.value.as_str())
193    }
194
195    /// Whether visible row `i` is a container (object/array).
196    #[must_use]
197    pub fn is_container(&self, i: usize) -> bool {
198        matches!(self.rows.get(i).map(|r| r.kind), Some(Kind::Container(_)))
199    }
200
201    /// Whether visible row `i` is a collapsed container.
202    #[must_use]
203    pub fn is_collapsed(&self, i: usize) -> bool {
204        matches!(
205            self.rows.get(i).map(|r| r.kind),
206            Some(Kind::Container(true))
207        )
208    }
209
210    /// Serialize the tree back to text in its format.
211    #[must_use]
212    pub fn to_text(&self) -> String {
213        match self.format {
214            Format::Json => {
215                let mut out = String::new();
216                json_push(&self.root, 0, &mut out);
217                out.push('\n');
218                out
219            }
220            Format::Yaml => serde_yaml::to_string(&to_yaml(&self.root)).unwrap_or_default(),
221        }
222    }
223
224    /// Mark the tree as saved (called by the host after a successful write).
225    pub fn mark_saved(&mut self) {
226        self.dirty = false;
227    }
228
229    /// Adjust the scroll so the selected row stays within a `height`-row window.
230    pub fn ensure_visible(&mut self, height: usize) {
231        let height = height.max(1);
232        if self.sel < self.scroll {
233            self.scroll = self.sel;
234        } else if self.sel >= self.scroll + height {
235            self.scroll = self.sel + 1 - height;
236        }
237        let max = self.rows.len().saturating_sub(height);
238        self.scroll = self.scroll.min(max);
239    }
240
241    /// Interpret a key event and report what the host should do next.
242    pub fn handle_key(&mut self, key: KeyEvent, page: usize) -> Outcome {
243        let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
244        if ctrl && key.code == KeyCode::Char('s') {
245            if self.mode == Mode::Edit {
246                self.commit_edit();
247            }
248            return Outcome::Save;
249        }
250        if self.mode == Mode::Edit {
251            self.edit_key(key);
252            return Outcome::Consumed;
253        }
254        match key.code {
255            KeyCode::Up | KeyCode::Char('k') => self.sel = self.sel.saturating_sub(1),
256            KeyCode::Down | KeyCode::Char('j') => self.step_down(1),
257            KeyCode::PageUp => self.sel = self.sel.saturating_sub(page.max(1)),
258            KeyCode::PageDown => self.step_down(page.max(1)),
259            KeyCode::Home => self.sel = 0,
260            KeyCode::End => self.sel = self.rows.len().saturating_sub(1),
261            KeyCode::Left | KeyCode::Char('h') => self.collapse_or_parent(),
262            KeyCode::Right | KeyCode::Char('l') => self.expand_or_child(),
263            KeyCode::Char(' ') => self.toggle(),
264            KeyCode::Enter | KeyCode::F(2) => self.activate(),
265            KeyCode::Char('u') => self.undo(),
266            KeyCode::Char('r') if ctrl => self.redo(),
267            KeyCode::Esc | KeyCode::Char('q') => return Outcome::Close,
268            _ => {}
269        }
270        Outcome::Consumed
271    }
272
273    /// Handle a key while editing a scalar.
274    fn edit_key(&mut self, key: KeyEvent) {
275        match key.code {
276            KeyCode::Enter => self.commit_edit(),
277            KeyCode::Esc => self.mode = Mode::Normal,
278            KeyCode::Backspace => {
279                self.edit_buf.pop();
280            }
281            KeyCode::Char(c) => self.edit_buf.push(c),
282            _ => {}
283        }
284    }
285
286    /// Move the selection down by `n`, clamped.
287    fn step_down(&mut self, n: usize) {
288        self.sel = (self.sel + n).min(self.rows.len().saturating_sub(1));
289    }
290
291    /// Collapse the selected container, or jump to its parent.
292    fn collapse_or_parent(&mut self) {
293        let Some(row) = self.rows.get(self.sel) else {
294            return;
295        };
296        if matches!(row.kind, Kind::Container(false)) {
297            let path = row.path.clone();
298            self.set_collapsed(&path, true);
299            self.rebuild();
300            self.select_path(&path);
301        } else {
302            let path = row.path.clone();
303            if path.is_empty() {
304                return;
305            }
306            self.select_path(&path[..path.len() - 1]);
307        }
308    }
309
310    /// Expand the selected container, or step into its first child.
311    fn expand_or_child(&mut self) {
312        let Some(row) = self.rows.get(self.sel) else {
313            return;
314        };
315        match row.kind {
316            Kind::Container(true) => {
317                let path = row.path.clone();
318                self.set_collapsed(&path, false);
319                self.rebuild();
320                self.select_path(&path);
321            }
322            Kind::Container(false) => {
323                if self.sel + 1 < self.rows.len() {
324                    self.sel += 1;
325                }
326            }
327            Kind::Scalar => {}
328        }
329    }
330
331    /// Toggle the selected container's fold state.
332    fn toggle(&mut self) {
333        let Some(row) = self.rows.get(self.sel) else {
334            return;
335        };
336        if let Kind::Container(c) = row.kind {
337            let path = row.path.clone();
338            self.set_collapsed(&path, !c);
339            self.rebuild();
340            self.select_path(&path);
341        }
342    }
343
344    /// Enter: edit a scalar, or toggle a container.
345    fn activate(&mut self) {
346        match self.rows.get(self.sel).map(|r| r.kind) {
347            Some(Kind::Scalar) => {
348                self.edit_buf = self.rows[self.sel].value.clone();
349                self.mode = Mode::Edit;
350            }
351            Some(Kind::Container(_)) => self.toggle(),
352            None => {}
353        }
354    }
355
356    /// Commit the edit buffer into the selected scalar as one undo step.
357    fn commit_edit(&mut self) {
358        self.mode = Mode::Normal;
359        let Some(row) = self.rows.get(self.sel) else {
360            return;
361        };
362        let path = row.path.clone();
363        let new = parse_scalar(&self.edit_buf);
364        if let Some(node) = at_mut(&mut self.root, &path) {
365            if *node == new {
366                return;
367            }
368            self.undo.push(self.root.clone());
369            if self.undo.len() > HISTORY_CAP {
370                self.undo.remove(0);
371            }
372            self.redo.clear();
373            if let Some(node) = at_mut(&mut self.root, &path) {
374                *node = new;
375            }
376            self.dirty = true;
377            self.rebuild();
378            self.select_path(&path);
379        }
380    }
381
382    /// Set the collapse flag of the container at `path`, if any.
383    fn set_collapsed(&mut self, path: &[Seg], collapsed: bool) {
384        if let Some(Val::Arr { collapsed: c, .. } | Val::Obj { collapsed: c, .. }) =
385            at_mut(&mut self.root, path)
386        {
387            *c = collapsed;
388        }
389    }
390
391    /// Recompute the visible rows from the tree, clamping the selection.
392    fn rebuild(&mut self) {
393        self.rows.clear();
394        flatten(&self.root, 0, String::new(), Vec::new(), &mut self.rows);
395        if self.sel >= self.rows.len() {
396            self.sel = self.rows.len().saturating_sub(1);
397        }
398    }
399
400    /// Select the row whose path equals `path` (else leave the selection clamped).
401    fn select_path(&mut self, path: &[Seg]) {
402        if let Some(i) = self.rows.iter().position(|r| r.path == path) {
403            self.sel = i;
404        }
405    }
406
407    /// Undo the most recent value edit.
408    fn undo(&mut self) {
409        if let Some(prev) = self.undo.pop() {
410            self.redo.push(std::mem::replace(&mut self.root, prev));
411            self.dirty = true;
412            self.rebuild();
413        }
414    }
415
416    /// Redo the most recently undone edit.
417    fn redo(&mut self) {
418        if let Some(next) = self.redo.pop() {
419            self.undo.push(std::mem::replace(&mut self.root, next));
420            self.dirty = true;
421            self.rebuild();
422        }
423    }
424}
425
426/// Locate a mutable node by path.
427fn at_mut<'a>(mut node: &'a mut Val, path: &[Seg]) -> Option<&'a mut Val> {
428    for seg in path {
429        node = match (node, seg) {
430            (Val::Arr { items, .. }, Seg::Index(i)) => items.get_mut(*i)?,
431            (Val::Obj { entries, .. }, Seg::Key(k)) => {
432                entries.iter_mut().find(|(ek, _)| ek == k).map(|(_, v)| v)?
433            }
434            _ => return None,
435        };
436    }
437    Some(node)
438}
439
440/// The display text of a scalar (`""` for containers).
441fn scalar_text(val: &Val) -> String {
442    match val {
443        Val::Null => "null".to_string(),
444        Val::Bool(b) => b.to_string(),
445        Val::Num(s) | Val::Str(s) => s.clone(),
446        _ => String::new(),
447    }
448}
449
450/// Flatten `val` (named `label`, located at `path`) into display rows; recurses
451/// into expanded containers only.
452fn flatten(val: &Val, depth: usize, label: String, path: Vec<Seg>, out: &mut Vec<Row>) {
453    match val {
454        Val::Arr { collapsed, items } => {
455            out.push(Row {
456                depth,
457                label,
458                value: format!("[{}]", items.len()),
459                kind: Kind::Container(*collapsed),
460                path: path.clone(),
461            });
462            if !collapsed {
463                for (i, item) in items.iter().enumerate() {
464                    let mut p = path.clone();
465                    p.push(Seg::Index(i));
466                    flatten(item, depth + 1, format!("[{i}]"), p, out);
467                }
468            }
469        }
470        Val::Obj { collapsed, entries } => {
471            out.push(Row {
472                depth,
473                label,
474                value: format!("{{{}}}", entries.len()),
475                kind: Kind::Container(*collapsed),
476                path: path.clone(),
477            });
478            if !collapsed {
479                for (k, v) in entries {
480                    let mut p = path.clone();
481                    p.push(Seg::Key(k.clone()));
482                    flatten(v, depth + 1, k.clone(), p, out);
483                }
484            }
485        }
486        scalar => out.push(Row {
487            depth,
488            label,
489            value: scalar_text(scalar),
490            kind: Kind::Scalar,
491            path,
492        }),
493    }
494}
495
496/// Parse edited text into a scalar: `true`/`false`/`null`, else a number when it
497/// parses as one, else a string.
498fn parse_scalar(s: &str) -> Val {
499    match s {
500        "true" => Val::Bool(true),
501        "false" => Val::Bool(false),
502        "null" => Val::Null,
503        _ if s.parse::<f64>().is_ok() => Val::Num(s.to_string()),
504        _ => Val::Str(s.to_string()),
505    }
506}
507
508/// Convert a parsed `serde_yaml` value into [`Val`] (preserving map order).
509fn from_yaml(v: &serde_yaml::Value) -> Val {
510    use serde_yaml::Value as Y;
511    match v {
512        Y::Null => Val::Null,
513        Y::Bool(b) => Val::Bool(*b),
514        Y::Number(n) => Val::Num(n.to_string()),
515        Y::String(s) => Val::Str(s.clone()),
516        Y::Sequence(items) => Val::Arr {
517            collapsed: false,
518            items: items.iter().map(from_yaml).collect(),
519        },
520        Y::Mapping(m) => Val::Obj {
521            collapsed: false,
522            entries: m.iter().map(|(k, v)| (yaml_key(k), from_yaml(v))).collect(),
523        },
524        Y::Tagged(t) => from_yaml(&t.value),
525    }
526}
527
528/// Render a mapping key as a string.
529fn yaml_key(k: &serde_yaml::Value) -> String {
530    use serde_yaml::Value as Y;
531    match k {
532        Y::String(s) => s.clone(),
533        Y::Bool(b) => b.to_string(),
534        Y::Number(n) => n.to_string(),
535        Y::Null => "null".to_string(),
536        _ => "?".to_string(),
537    }
538}
539
540/// Convert [`Val`] into a `serde_yaml` value for YAML serialization.
541fn to_yaml(val: &Val) -> serde_yaml::Value {
542    use serde_yaml::Value as Y;
543    match val {
544        Val::Null => Y::Null,
545        Val::Bool(b) => Y::Bool(*b),
546        Val::Num(s) => s
547            .parse::<i64>()
548            .map(Y::from)
549            .or_else(|_| s.parse::<f64>().map(Y::from))
550            .unwrap_or_else(|_| Y::String(s.clone())),
551        Val::Str(s) => Y::String(s.clone()),
552        Val::Arr { items, .. } => Y::Sequence(items.iter().map(to_yaml).collect()),
553        Val::Obj { entries, .. } => {
554            let mut m = serde_yaml::Mapping::new();
555            for (k, v) in entries {
556                m.insert(Y::String(k.clone()), to_yaml(v));
557            }
558            Y::Mapping(m)
559        }
560    }
561}
562
563/// Append two spaces per `depth` to `out`.
564fn indent(out: &mut String, depth: usize) {
565    for _ in 0..depth {
566        out.push_str("  ");
567    }
568}
569
570/// A JSON string literal with proper escaping.
571fn json_quote(s: &str) -> String {
572    serde_json::to_string(s).unwrap_or_else(|_| format!("\"{s}\""))
573}
574
575/// Pretty-print `val` as JSON into `out`, preserving object key order.
576fn json_push(val: &Val, depth: usize, out: &mut String) {
577    match val {
578        Val::Null => out.push_str("null"),
579        Val::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
580        Val::Num(s) => out.push_str(s),
581        Val::Str(s) => out.push_str(&json_quote(s)),
582        Val::Arr { items, .. } => {
583            if items.is_empty() {
584                out.push_str("[]");
585                return;
586            }
587            out.push_str("[\n");
588            for (i, item) in items.iter().enumerate() {
589                indent(out, depth + 1);
590                json_push(item, depth + 1, out);
591                if i + 1 < items.len() {
592                    out.push(',');
593                }
594                out.push('\n');
595            }
596            indent(out, depth);
597            out.push(']');
598        }
599        Val::Obj { entries, .. } => {
600            if entries.is_empty() {
601                out.push_str("{}");
602                return;
603            }
604            out.push_str("{\n");
605            for (i, (k, v)) in entries.iter().enumerate() {
606                indent(out, depth + 1);
607                out.push_str(&json_quote(k));
608                out.push_str(": ");
609                json_push(v, depth + 1, out);
610                if i + 1 < entries.len() {
611                    out.push(',');
612                }
613                out.push('\n');
614            }
615            indent(out, depth);
616            out.push('}');
617        }
618    }
619}
620
621#[cfg(test)]
622mod tests {
623    use super::*;
624
625    fn key(c: char) -> KeyEvent {
626        KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE)
627    }
628
629    fn code(code: KeyCode) -> KeyEvent {
630        KeyEvent::new(code, KeyModifiers::NONE)
631    }
632
633    fn ctrl(c: char) -> KeyEvent {
634        KeyEvent::new(KeyCode::Char(c), KeyModifiers::CONTROL)
635    }
636
637    fn json() -> Tree {
638        Tree::from_text(r#"{"a": 1, "b": [10, 20], "c": "hi"}"#, Format::Json).unwrap()
639    }
640
641    #[test]
642    fn parses_json_into_rows_in_order() {
643        let t = json();
644        // root + a + b + b[0] + b[1] + c = 6 rows
645        assert_eq!(t.row_count(), 6);
646        assert_eq!(t.label(1), "a");
647        assert_eq!(t.value(1), "1");
648        assert_eq!(t.label(2), "b");
649        assert!(t.is_container(2));
650        assert_eq!(t.label(5), "c");
651        assert_eq!(t.value(5), "hi");
652    }
653
654    #[test]
655    fn invalid_input_is_none() {
656        assert!(Tree::from_text("{ not valid", Format::Json).is_none());
657    }
658
659    #[test]
660    fn collapsing_hides_children() {
661        let mut t = json();
662        t.handle_key(code(KeyCode::Down), 8); // a
663        t.handle_key(code(KeyCode::Down), 8); // b (array)
664        assert!(t.is_container(t.sel()));
665        t.handle_key(code(KeyCode::Left), 8); // collapse b
666        assert!(t.is_collapsed(t.sel()));
667        // root, a, b(collapsed), c = 4 rows
668        assert_eq!(t.row_count(), 4);
669    }
670
671    #[test]
672    fn edits_a_scalar_value() {
673        let mut t = json();
674        t.handle_key(code(KeyCode::Down), 8); // a = 1
675        t.handle_key(code(KeyCode::Enter), 8); // edit
676        assert!(t.is_editing());
677        t.handle_key(code(KeyCode::Backspace), 8);
678        t.handle_key(key('9'), 8);
679        t.handle_key(code(KeyCode::Enter), 8); // commit
680        assert_eq!(t.value(1), "9");
681        assert!(t.is_dirty());
682        assert!(t.to_text().contains("\"a\": 9"));
683    }
684
685    #[test]
686    fn undo_and_redo_a_value_edit() {
687        let mut t = json();
688        t.handle_key(code(KeyCode::Down), 8); // a
689        t.handle_key(code(KeyCode::Enter), 8);
690        t.handle_key(code(KeyCode::Backspace), 8);
691        t.handle_key(key('7'), 8);
692        t.handle_key(code(KeyCode::Enter), 8);
693        assert_eq!(t.value(1), "7");
694        t.handle_key(key('u'), 8);
695        assert_eq!(t.value(1), "1", "undo restores");
696        t.handle_key(ctrl('r'), 8);
697        assert_eq!(t.value(1), "7", "redo reapplies");
698    }
699
700    #[test]
701    fn json_round_trips_and_preserves_key_order() {
702        let t = Tree::from_text(r#"{"b": 1, "a": 2}"#, Format::Json).unwrap();
703        assert_eq!(
704            t.to_text(),
705            "{\n  \"b\": 1,\n  \"a\": 2\n}\n",
706            "keys stay in source order"
707        );
708    }
709
710    #[test]
711    fn yaml_parses_and_serializes() {
712        let t = Tree::from_text("name: vix\ntags:\n  - tui\n  - editor\n", Format::Yaml).unwrap();
713        assert_eq!(t.label(1), "name");
714        assert_eq!(t.value(1), "vix");
715        let out = t.to_text();
716        assert!(out.contains("name: vix"), "got: {out:?}");
717        assert!(out.contains("- tui"), "got: {out:?}");
718    }
719
720    #[test]
721    fn save_and_close_outcomes() {
722        let mut t = json();
723        assert_eq!(t.handle_key(ctrl('s'), 8), Outcome::Save);
724        assert_eq!(t.handle_key(key('q'), 8), Outcome::Close);
725        assert_eq!(t.handle_key(code(KeyCode::Esc), 8), Outcome::Close);
726    }
727
728    proptest::proptest! {
729        // Parsing arbitrary text as JSON or YAML never panics (invalid → None).
730        #[test]
731        fn from_text_never_panics(s in ".*") {
732            let _ = Tree::from_text(&s, Format::Json);
733            let _ = Tree::from_text(&s, Format::Yaml);
734        }
735    }
736
737    #[test]
738    fn deeply_nested_value_does_not_overflow() {
739        // The recursive `from_yaml`/`rebuild` walk must not stack-overflow on
740        // pathologically nested input; serde's depth limit rejects it as None.
741        let deep = format!("{}{}", "[".repeat(100_000), "]".repeat(100_000));
742        assert!(Tree::from_text(&deep, Format::Json).is_none());
743    }
744}