1#![warn(clippy::pedantic)]
23#![forbid(unsafe_code)]
24#![deny(missing_docs)]
25
26use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum Format {
31 Json,
33 Yaml,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum Outcome {
40 Consumed,
42 Close,
44 Save,
46}
47
48#[derive(Clone, PartialEq)]
50enum Val {
51 Null,
52 Bool(bool),
53 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#[derive(Clone, PartialEq)]
68enum Seg {
69 Key(String),
70 Index(usize),
71}
72
73#[derive(Clone, Copy, PartialEq)]
75enum Kind {
76 Scalar,
77 Container(bool),
78}
79
80struct Row {
82 depth: usize,
83 label: String,
84 value: String,
85 kind: Kind,
86 path: Vec<Seg>,
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91enum Mode {
92 Normal,
93 Edit,
94}
95
96const HISTORY_CAP: usize = 200;
98
99pub 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 pub fn from_text(text: &str, format: Format) -> Result<Self, String> {
125 let parsed: serde_norway::Value =
126 serde_norway::from_str(text).map_err(|e| e.to_string())?;
127 let mut tree = Tree {
128 root: from_yaml(&parsed),
129 format,
130 rows: Vec::new(),
131 sel: 0,
132 scroll: 0,
133 dirty: false,
134 mode: Mode::Normal,
135 edit_buf: String::new(),
136 undo: Vec::new(),
137 redo: Vec::new(),
138 };
139 tree.rebuild();
140 Ok(tree)
141 }
142
143 #[must_use]
145 pub fn format(&self) -> Format {
146 self.format
147 }
148
149 #[must_use]
151 pub fn row_count(&self) -> usize {
152 self.rows.len()
153 }
154
155 #[must_use]
157 pub fn sel(&self) -> usize {
158 self.sel
159 }
160
161 #[must_use]
163 pub fn scroll(&self) -> usize {
164 self.scroll
165 }
166
167 #[must_use]
169 pub fn is_dirty(&self) -> bool {
170 self.dirty
171 }
172
173 #[must_use]
175 pub fn is_editing(&self) -> bool {
176 matches!(self.mode, Mode::Edit)
177 }
178
179 #[must_use]
181 pub fn edit_buffer(&self) -> &str {
182 &self.edit_buf
183 }
184
185 #[must_use]
187 pub fn depth(&self, i: usize) -> usize {
188 self.rows.get(i).map_or(0, |r| r.depth)
189 }
190
191 #[must_use]
193 pub fn label(&self, i: usize) -> &str {
194 self.rows.get(i).map_or("", |r| r.label.as_str())
195 }
196
197 #[must_use]
199 pub fn value(&self, i: usize) -> &str {
200 self.rows.get(i).map_or("", |r| r.value.as_str())
201 }
202
203 #[must_use]
205 pub fn is_container(&self, i: usize) -> bool {
206 matches!(self.rows.get(i).map(|r| r.kind), Some(Kind::Container(_)))
207 }
208
209 #[must_use]
211 pub fn is_collapsed(&self, i: usize) -> bool {
212 matches!(
213 self.rows.get(i).map(|r| r.kind),
214 Some(Kind::Container(true))
215 )
216 }
217
218 #[must_use]
220 pub fn to_text(&self) -> String {
221 match self.format {
222 Format::Json => {
223 let mut out = String::new();
224 json_push(&self.root, 0, &mut out);
225 out.push('\n');
226 out
227 }
228 Format::Yaml => serde_norway::to_string(&to_yaml(&self.root)).unwrap_or_default(),
229 }
230 }
231
232 pub fn mark_saved(&mut self) {
234 self.dirty = false;
235 }
236
237 pub fn ensure_visible(&mut self, height: usize) {
239 self.scroll =
240 vix_list_state::ensure_visible(self.sel, self.scroll, height, self.rows.len());
241 }
242
243 pub fn handle_key(&mut self, key: KeyEvent, page: usize) -> Outcome {
245 let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
246 if ctrl && key.code == KeyCode::Char('s') {
247 if self.mode == Mode::Edit {
248 self.commit_edit();
249 }
250 return Outcome::Save;
251 }
252 if self.mode == Mode::Edit {
253 self.edit_key(key);
254 return Outcome::Consumed;
255 }
256 match key.code {
257 KeyCode::Up | KeyCode::Char('k') => self.sel = self.sel.saturating_sub(1),
258 KeyCode::Down | KeyCode::Char('j') => self.step_down(1),
259 KeyCode::PageUp => self.sel = self.sel.saturating_sub(page.max(1)),
260 KeyCode::PageDown => self.step_down(page.max(1)),
261 KeyCode::Home => self.sel = 0,
262 KeyCode::End => self.sel = self.rows.len().saturating_sub(1),
263 KeyCode::Left | KeyCode::Char('h') => self.collapse_or_parent(),
264 KeyCode::Right | KeyCode::Char('l') => self.expand_or_child(),
265 KeyCode::Char(' ') => self.toggle(),
266 KeyCode::Enter | KeyCode::F(2) => self.activate(),
267 KeyCode::Char('u') => self.undo(),
268 KeyCode::Char('r') if ctrl => self.redo(),
269 KeyCode::Esc | KeyCode::Char('q') => return Outcome::Close,
270 _ => {}
271 }
272 Outcome::Consumed
273 }
274
275 fn edit_key(&mut self, key: KeyEvent) {
277 match key.code {
278 KeyCode::Enter => self.commit_edit(),
279 KeyCode::Esc => self.mode = Mode::Normal,
280 KeyCode::Backspace => {
281 self.edit_buf.pop();
282 }
283 KeyCode::Char(c) => self.edit_buf.push(c),
284 _ => {}
285 }
286 }
287
288 fn step_down(&mut self, n: usize) {
290 self.sel = (self.sel + n).min(self.rows.len().saturating_sub(1));
291 }
292
293 fn collapse_or_parent(&mut self) {
295 let Some(row) = self.rows.get(self.sel) else {
296 return;
297 };
298 if matches!(row.kind, Kind::Container(false)) {
299 let path = row.path.clone();
300 self.set_collapsed(&path, true);
301 self.rebuild();
302 self.select_path(&path);
303 } else {
304 let path = row.path.clone();
305 if path.is_empty() {
306 return;
307 }
308 self.select_path(&path[..path.len() - 1]);
309 }
310 }
311
312 fn expand_or_child(&mut self) {
314 let Some(row) = self.rows.get(self.sel) else {
315 return;
316 };
317 match row.kind {
318 Kind::Container(true) => {
319 let path = row.path.clone();
320 self.set_collapsed(&path, false);
321 self.rebuild();
322 self.select_path(&path);
323 }
324 Kind::Container(false) => {
325 if self.sel + 1 < self.rows.len() {
326 self.sel += 1;
327 }
328 }
329 Kind::Scalar => {}
330 }
331 }
332
333 fn toggle(&mut self) {
335 let Some(row) = self.rows.get(self.sel) else {
336 return;
337 };
338 if let Kind::Container(c) = row.kind {
339 let path = row.path.clone();
340 self.set_collapsed(&path, !c);
341 self.rebuild();
342 self.select_path(&path);
343 }
344 }
345
346 fn activate(&mut self) {
348 match self.rows.get(self.sel).map(|r| r.kind) {
349 Some(Kind::Scalar) => {
350 self.edit_buf = self.rows[self.sel].value.clone();
351 self.mode = Mode::Edit;
352 }
353 Some(Kind::Container(_)) => self.toggle(),
354 None => {}
355 }
356 }
357
358 fn commit_edit(&mut self) {
360 self.mode = Mode::Normal;
361 let Some(row) = self.rows.get(self.sel) else {
362 return;
363 };
364 let path = row.path.clone();
365 let new = parse_scalar(&self.edit_buf);
366 if let Some(node) = at_mut(&mut self.root, &path) {
367 if *node == new {
368 return;
369 }
370 let snap = self.root.clone();
373 vix_capped_stack::push_capped(&mut self.undo, snap, HISTORY_CAP);
374 self.redo.clear();
375 if let Some(node) = at_mut(&mut self.root, &path) {
376 *node = new;
377 }
378 self.dirty = true;
379 self.rebuild();
380 self.select_path(&path);
381 }
382 }
383
384 fn set_collapsed(&mut self, path: &[Seg], collapsed: bool) {
386 if let Some(Val::Arr { collapsed: c, .. } | Val::Obj { collapsed: c, .. }) =
387 at_mut(&mut self.root, path)
388 {
389 *c = collapsed;
390 }
391 }
392
393 fn rebuild(&mut self) {
395 self.rows.clear();
396 flatten(&self.root, 0, String::new(), Vec::new(), &mut self.rows);
397 if self.sel >= self.rows.len() {
398 self.sel = self.rows.len().saturating_sub(1);
399 }
400 }
401
402 fn select_path(&mut self, path: &[Seg]) {
404 if let Some(i) = self.rows.iter().position(|r| r.path == path) {
405 self.sel = i;
406 }
407 }
408
409 fn undo(&mut self) {
411 if let Some(prev) = self.undo.pop() {
412 self.redo.push(std::mem::replace(&mut self.root, prev));
413 self.dirty = true;
414 self.rebuild();
415 }
416 }
417
418 fn redo(&mut self) {
420 if let Some(next) = self.redo.pop() {
421 self.undo.push(std::mem::replace(&mut self.root, next));
422 self.dirty = true;
423 self.rebuild();
424 }
425 }
426}
427
428fn at_mut<'a>(mut node: &'a mut Val, path: &[Seg]) -> Option<&'a mut Val> {
430 for seg in path {
431 node = match (node, seg) {
432 (Val::Arr { items, .. }, Seg::Index(i)) => items.get_mut(*i)?,
433 (Val::Obj { entries, .. }, Seg::Key(k)) => {
434 entries.iter_mut().find(|(ek, _)| ek == k).map(|(_, v)| v)?
435 }
436 _ => return None,
437 };
438 }
439 Some(node)
440}
441
442fn scalar_text(val: &Val) -> String {
444 match val {
445 Val::Null => "null".to_string(),
446 Val::Bool(b) => b.to_string(),
447 Val::Num(s) | Val::Str(s) => s.clone(),
448 _ => String::new(),
449 }
450}
451
452fn flatten(val: &Val, depth: usize, label: String, path: Vec<Seg>, out: &mut Vec<Row>) {
455 match val {
456 Val::Arr { collapsed, items } => {
457 out.push(Row {
458 depth,
459 label,
460 value: format!("[{}]", items.len()),
461 kind: Kind::Container(*collapsed),
462 path: path.clone(),
463 });
464 if !collapsed {
465 for (i, item) in items.iter().enumerate() {
466 let mut p = path.clone();
467 p.push(Seg::Index(i));
468 flatten(item, depth + 1, format!("[{i}]"), p, out);
469 }
470 }
471 }
472 Val::Obj { collapsed, entries } => {
473 out.push(Row {
474 depth,
475 label,
476 value: format!("{{{}}}", entries.len()),
477 kind: Kind::Container(*collapsed),
478 path: path.clone(),
479 });
480 if !collapsed {
481 for (k, v) in entries {
482 let mut p = path.clone();
483 p.push(Seg::Key(k.clone()));
484 flatten(v, depth + 1, k.clone(), p, out);
485 }
486 }
487 }
488 scalar => out.push(Row {
489 depth,
490 label,
491 value: scalar_text(scalar),
492 kind: Kind::Scalar,
493 path,
494 }),
495 }
496}
497
498fn parse_scalar(s: &str) -> Val {
501 match s {
502 "true" => Val::Bool(true),
503 "false" => Val::Bool(false),
504 "null" => Val::Null,
505 _ if s.parse::<f64>().is_ok() => Val::Num(s.to_string()),
506 _ => Val::Str(s.to_string()),
507 }
508}
509
510fn from_yaml(v: &serde_norway::Value) -> Val {
512 use serde_norway::Value as Y;
513 match v {
514 Y::Null => Val::Null,
515 Y::Bool(b) => Val::Bool(*b),
516 Y::Number(n) => Val::Num(n.to_string()),
517 Y::String(s) => Val::Str(s.clone()),
518 Y::Sequence(items) => Val::Arr {
519 collapsed: false,
520 items: items.iter().map(from_yaml).collect(),
521 },
522 Y::Mapping(m) => Val::Obj {
523 collapsed: false,
524 entries: m.iter().map(|(k, v)| (yaml_key(k), from_yaml(v))).collect(),
525 },
526 Y::Tagged(t) => from_yaml(&t.value),
527 }
528}
529
530fn yaml_key(k: &serde_norway::Value) -> String {
532 use serde_norway::Value as Y;
533 match k {
534 Y::String(s) => s.clone(),
535 Y::Bool(b) => b.to_string(),
536 Y::Number(n) => n.to_string(),
537 Y::Null => "null".to_string(),
538 _ => "?".to_string(),
539 }
540}
541
542fn to_yaml(val: &Val) -> serde_norway::Value {
544 use serde_norway::Value as Y;
545 match val {
546 Val::Null => Y::Null,
547 Val::Bool(b) => Y::Bool(*b),
548 Val::Num(s) => s
549 .parse::<i64>()
550 .map(Y::from)
551 .or_else(|_| s.parse::<f64>().map(Y::from))
552 .unwrap_or_else(|_| Y::String(s.clone())),
553 Val::Str(s) => Y::String(s.clone()),
554 Val::Arr { items, .. } => Y::Sequence(items.iter().map(to_yaml).collect()),
555 Val::Obj { entries, .. } => {
556 let mut m = serde_norway::Mapping::new();
557 for (k, v) in entries {
558 m.insert(Y::String(k.clone()), to_yaml(v));
559 }
560 Y::Mapping(m)
561 }
562 }
563}
564
565fn indent(out: &mut String, depth: usize) {
567 for _ in 0..depth {
568 out.push_str(" ");
569 }
570}
571
572fn json_quote(s: &str) -> String {
574 serde_json::to_string(s).unwrap_or_else(|_| format!("\"{s}\""))
575}
576
577fn json_push(val: &Val, depth: usize, out: &mut String) {
579 match val {
580 Val::Null => out.push_str("null"),
581 Val::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
582 Val::Num(s) => out.push_str(s),
583 Val::Str(s) => out.push_str(&json_quote(s)),
584 Val::Arr { items, .. } => {
585 if items.is_empty() {
586 out.push_str("[]");
587 return;
588 }
589 out.push_str("[\n");
590 for (i, item) in items.iter().enumerate() {
591 indent(out, depth + 1);
592 json_push(item, depth + 1, out);
593 if i + 1 < items.len() {
594 out.push(',');
595 }
596 out.push('\n');
597 }
598 indent(out, depth);
599 out.push(']');
600 }
601 Val::Obj { entries, .. } => {
602 if entries.is_empty() {
603 out.push_str("{}");
604 return;
605 }
606 out.push_str("{\n");
607 for (i, (k, v)) in entries.iter().enumerate() {
608 indent(out, depth + 1);
609 out.push_str(&json_quote(k));
610 out.push_str(": ");
611 json_push(v, depth + 1, out);
612 if i + 1 < entries.len() {
613 out.push(',');
614 }
615 out.push('\n');
616 }
617 indent(out, depth);
618 out.push('}');
619 }
620 }
621}
622
623#[cfg(test)]
624mod tests {
625 use super::*;
626
627 fn key(c: char) -> KeyEvent {
628 KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE)
629 }
630
631 fn code(code: KeyCode) -> KeyEvent {
632 KeyEvent::new(code, KeyModifiers::NONE)
633 }
634
635 fn ctrl(c: char) -> KeyEvent {
636 KeyEvent::new(KeyCode::Char(c), KeyModifiers::CONTROL)
637 }
638
639 fn json() -> Tree {
640 Tree::from_text(r#"{"a": 1, "b": [10, 20], "c": "hi"}"#, Format::Json).unwrap()
641 }
642
643 #[test]
644 fn parses_json_into_rows_in_order() {
645 let t = json();
646 assert_eq!(t.row_count(), 6);
648 assert_eq!(t.label(1), "a");
649 assert_eq!(t.value(1), "1");
650 assert_eq!(t.label(2), "b");
651 assert!(t.is_container(2));
652 assert_eq!(t.label(5), "c");
653 assert_eq!(t.value(5), "hi");
654 }
655
656 #[test]
657 fn invalid_input_is_an_error() {
658 assert!(Tree::from_text("{ not valid", Format::Json).is_err());
659 }
660
661 #[test]
662 fn parse_error_names_the_real_problem() {
663 let Err(err) = Tree::from_text("a: [1, 2\nb: 3", Format::Yaml) else {
668 panic!("expected a parse error");
669 };
670 assert!(
671 err.to_lowercase().contains("line"),
672 "the parser's own diagnostic names a location: {err:?}"
673 );
674 }
675
676 #[test]
677 fn collapsing_hides_children() {
678 let mut t = json();
679 t.handle_key(code(KeyCode::Down), 8); t.handle_key(code(KeyCode::Down), 8); assert!(t.is_container(t.sel()));
682 t.handle_key(code(KeyCode::Left), 8); assert!(t.is_collapsed(t.sel()));
684 assert_eq!(t.row_count(), 4);
686 }
687
688 #[test]
689 fn edits_a_scalar_value() {
690 let mut t = json();
691 t.handle_key(code(KeyCode::Down), 8); t.handle_key(code(KeyCode::Enter), 8); assert!(t.is_editing());
694 t.handle_key(code(KeyCode::Backspace), 8);
695 t.handle_key(key('9'), 8);
696 t.handle_key(code(KeyCode::Enter), 8); assert_eq!(t.value(1), "9");
698 assert!(t.is_dirty());
699 assert!(t.to_text().contains("\"a\": 9"));
700 }
701
702 #[test]
703 fn undo_and_redo_a_value_edit() {
704 let mut t = json();
705 t.handle_key(code(KeyCode::Down), 8); t.handle_key(code(KeyCode::Enter), 8);
707 t.handle_key(code(KeyCode::Backspace), 8);
708 t.handle_key(key('7'), 8);
709 t.handle_key(code(KeyCode::Enter), 8);
710 assert_eq!(t.value(1), "7");
711 t.handle_key(key('u'), 8);
712 assert_eq!(t.value(1), "1", "undo restores");
713 t.handle_key(ctrl('r'), 8);
714 assert_eq!(t.value(1), "7", "redo reapplies");
715 }
716
717 #[test]
718 fn json_round_trips_and_preserves_key_order() {
719 let t = Tree::from_text(r#"{"b": 1, "a": 2}"#, Format::Json).unwrap();
720 assert_eq!(
721 t.to_text(),
722 "{\n \"b\": 1,\n \"a\": 2\n}\n",
723 "keys stay in source order"
724 );
725 }
726
727 #[test]
728 fn yaml_parses_and_serializes() {
729 let t = Tree::from_text("name: vix\ntags:\n - tui\n - editor\n", Format::Yaml).unwrap();
730 assert_eq!(t.label(1), "name");
731 assert_eq!(t.value(1), "vix");
732 let out = t.to_text();
733 assert!(out.contains("name: vix"), "got: {out:?}");
734 assert!(out.contains("- tui"), "got: {out:?}");
735 }
736
737 #[test]
738 fn save_and_close_outcomes() {
739 let mut t = json();
740 assert_eq!(t.handle_key(ctrl('s'), 8), Outcome::Save);
741 assert_eq!(t.handle_key(key('q'), 8), Outcome::Close);
742 assert_eq!(t.handle_key(code(KeyCode::Esc), 8), Outcome::Close);
743 }
744
745 proptest::proptest! {
746 #[test]
748 fn from_text_never_panics(s in ".*") {
749 let _ = Tree::from_text(&s, Format::Json);
750 let _ = Tree::from_text(&s, Format::Yaml);
751 }
752 }
753
754 #[test]
755 fn deeply_nested_value_does_not_overflow() {
756 let deep = format!("{}{}", "[".repeat(100_000), "]".repeat(100_000));
759 assert!(Tree::from_text(&deep, Format::Json).is_err());
760 }
761}