1use std::collections::HashSet;
10
11use anyhow::Result;
12use fig::Value;
13
14use crate::backend::{Backend, EditOp};
15use crate::schema::{FieldRule, Schema};
16use fig_schema::{Issue, SegPat, Validation};
17use crate::tree::{self, Row, Seg};
18
19pub enum Mode {
21 Normal,
22 Editing { buffer: String },
23}
24
25pub struct Model<B> {
26 backend: B,
27
28 value: Value,
30 pub rows: Vec<Row>,
31 collapsed: HashSet<Vec<Seg>>,
32 hidden: HashSet<String>,
36 derived: HashSet<String>,
46 schema: Option<Schema>,
51
52 pub selected: usize,
53 pub mode: Mode,
54 pub status: String,
55 pub dirty: bool,
56}
57
58impl<B: Backend> Model<B> {
59 pub fn new(backend: B) -> Result<Self> {
61 Self::with_hidden(backend, Vec::new())
62 }
63
64 pub fn with_hidden(backend: B, hidden: Vec<String>) -> Result<Self> {
69 Self::with_managed(backend, hidden, Vec::new())
70 }
71
72 pub fn with_managed(backend: B, hidden: Vec<String>, derived: Vec<String>) -> Result<Self> {
79 Self::with_collapsed(backend, hidden, derived, Vec::new())
80 }
81
82 pub fn with_collapsed(
98 backend: B,
99 hidden: Vec<String>,
100 derived: Vec<String>,
101 collapsed: Vec<Vec<Seg>>,
102 ) -> Result<Self> {
103 let schema = backend.schema();
106 let mut model = Model {
107 backend,
108 value: Value::Null,
109 rows: Vec::new(),
110 collapsed: collapsed.into_iter().collect(),
111 hidden: hidden.into_iter().collect(),
112 derived: derived.into_iter().collect(),
113 schema,
114 selected: 0,
115 mode: Mode::Normal,
116 status: "opened".to_string(),
117 dirty: false,
118 };
119 model.reload()?;
120 Ok(model)
121 }
122
123 pub fn set_schema(&mut self, schema: Schema) {
128 self.schema = Some(schema);
129 }
130
131 pub fn schema(&self) -> Option<&Schema> {
133 self.schema.as_ref()
134 }
135
136 pub fn rule_at(&self, path: &[Seg]) -> Option<&FieldRule> {
139 self.schema.as_ref().and_then(|s| s.rule_for(path))
140 }
141
142 pub fn root_kind(&self) -> &'static str {
145 match self.value {
146 Value::Map(_) => "map",
147 Value::Seq(_) => "seq",
148 _ => "scalar",
149 }
150 }
151
152 pub fn hidden_present(&self) -> usize {
155 match &self.value {
156 Value::Map(entries) => entries
157 .iter()
158 .filter(|(k, _)| matches!(k, Value::Str(s) if self.hidden.contains(s)))
159 .count(),
160 _ => 0,
161 }
162 }
163
164 pub fn is_derived(&self, path: &[Seg]) -> bool {
168 matches!(path.first(), Some(Seg::Key(k)) if self.derived.contains(k))
169 }
170
171 pub fn addable_fields(&self) -> Vec<&FieldRule> {
188 let Some(schema) = &self.schema else {
189 return Vec::new();
190 };
191 let Value::Map(entries) = &self.value else {
193 return Vec::new();
194 };
195 let present: HashSet<&str> = entries
196 .iter()
197 .filter_map(|(k, _)| match k {
198 Value::Str(s) => Some(s.as_str()),
199 _ => None,
200 })
201 .collect();
202 let mut seen = HashSet::new();
203 schema
204 .rules()
205 .iter()
206 .filter(|rule| {
207 let [SegPat::Key(name)] = rule.at.0.as_slice() else {
208 return false;
209 };
210 !present.contains(name.as_str())
211 && !self.hidden.contains(name)
212 && seen.insert(name.as_str())
213 })
214 .collect()
215 }
216
217 pub fn source_snapshot(&self) -> String {
219 self.backend.source().unwrap_or_default()
220 }
221
222 pub fn backend(&self) -> &B {
224 &self.backend
225 }
226
227 pub fn backend_mut(&mut self) -> &mut B {
232 &mut self.backend
233 }
234
235 pub fn set_status(&mut self, s: impl Into<String>) {
236 self.status = s.into();
237 }
238
239 pub fn mark_saved(&mut self) {
241 self.dirty = false;
242 }
243
244 fn reload(&mut self) -> Result<()> {
248 self.value = self
249 .backend
250 .to_value()
251 .map_err(|e| anyhow::anyhow!("reading value tree: {e}"))?;
252 self.rebuild_rows();
253 Ok(())
254 }
255
256 fn rebuild_rows(&mut self) {
257 self.rows = tree::build_rows(&self.value, &self.collapsed, &self.hidden);
258 if self.selected >= self.rows.len() {
259 self.selected = self.rows.len().saturating_sub(1);
260 }
261 }
262
263 fn selected_row(&self) -> Option<&Row> {
264 self.rows.get(self.selected)
265 }
266
267 fn select_path(&mut self, path: &[Seg]) {
269 if let Some(i) = self.rows.iter().position(|r| r.path == path) {
270 self.selected = i;
271 } else if self.selected >= self.rows.len() {
272 self.selected = self.rows.len().saturating_sub(1);
273 }
274 }
275
276 pub fn move_down(&mut self) {
279 if self.selected + 1 < self.rows.len() {
280 self.selected += 1;
281 }
282 }
283
284 pub fn move_up(&mut self) {
285 self.selected = self.selected.saturating_sub(1);
286 }
287
288 pub fn expand_or_enter(&mut self) {
290 let Some(row) = self.selected_row() else {
291 return;
292 };
293 if row.is_container() {
294 if !row.expanded {
295 let path = row.path.clone();
296 self.collapsed.remove(&path);
297 self.rebuild_rows();
298 self.select_path(&path);
299 } else if self.selected + 1 < self.rows.len()
300 && self.rows[self.selected + 1].depth > row.depth
301 {
302 self.selected += 1;
303 }
304 }
305 }
306
307 pub fn collapse_or_leave(&mut self) {
309 let Some(row) = self.selected_row() else {
310 return;
311 };
312 if row.is_container() && row.expanded {
313 let path = row.path.clone();
314 self.collapsed.insert(path.clone());
315 self.rebuild_rows();
316 self.select_path(&path);
317 return;
318 }
319 let depth = row.depth;
321 if depth == 0 {
322 return;
323 }
324 for i in (0..self.selected).rev() {
325 if self.rows[i].depth < depth {
326 self.selected = i;
327 return;
328 }
329 }
330 }
331
332 pub fn is_collapsed(&self, path: &[Seg]) -> bool {
336 self.collapsed.contains(path)
337 }
338
339 pub fn set_collapsed(&mut self, path: &[Seg], collapsed: bool) {
351 let changed = if collapsed {
352 self.collapsed.insert(path.to_vec())
353 } else {
354 self.collapsed.remove(path)
355 };
356 if !changed {
357 return;
358 }
359 let was = self.selected_row().map(|r| r.path.clone());
360 self.rebuild_rows();
361 if let Some(was) = was {
362 if collapsed && was.len() > path.len() && was.starts_with(path) {
365 self.select_path(path);
366 } else {
367 self.select_path(&was);
368 }
369 }
370 }
371
372 pub fn activate(&mut self) {
374 let Some(row) = self.selected_row() else {
375 return;
376 };
377 if row.is_container() {
378 let path = row.path.clone();
379 if row.expanded {
380 self.collapsed.insert(path.clone());
381 } else {
382 self.collapsed.remove(&path);
383 }
384 self.rebuild_rows();
385 self.select_path(&path);
386 } else {
387 self.begin_edit();
388 }
389 }
390
391 pub fn begin_edit(&mut self) {
394 let Some(row) = self.selected_row() else {
395 return;
396 };
397 if !row.is_scalar() {
398 self.status = "can only edit scalar values".to_string();
399 return;
400 }
401 let seed = self
402 .value_at(&row.path)
403 .map(tree::edit_seed)
404 .unwrap_or_default();
405 self.mode = Mode::Editing { buffer: seed };
406 }
407
408 pub fn edit_push(&mut self, c: char) {
409 if let Mode::Editing { buffer } = &mut self.mode {
410 buffer.push(c);
411 }
412 }
413
414 pub fn edit_backspace(&mut self) {
415 if let Mode::Editing { buffer } = &mut self.mode {
416 buffer.pop();
417 }
418 }
419
420 pub fn edit_cancel(&mut self) {
421 self.mode = Mode::Normal;
422 self.status = "edit cancelled".to_string();
423 }
424
425 pub fn edit_commit(&mut self) {
426 let Mode::Editing { buffer } = &mut self.mode else {
427 return;
428 };
429 let buffer = std::mem::take(buffer);
430 self.mode = Mode::Normal;
431
432 let Some(row) = self.selected_row() else {
433 return;
434 };
435 let path = row.path.clone();
436 let value = self.coerce_text(&path, &buffer);
437 self.commit(
438 EditOp::ReplaceValue {
439 path: path.clone(),
440 value,
441 },
442 path,
443 "value updated",
444 );
445 }
446
447 pub fn set_value_at(&mut self, path: &[Seg], value: Value) {
452 self.commit(
453 EditOp::ReplaceValue {
454 path: path.to_vec(),
455 value,
456 },
457 path.to_vec(),
458 "value updated",
459 );
460 }
461
462 pub fn set_scalar_text(&mut self, path: &[Seg], text: &str) {
468 let value = self.coerce_text(path, text);
469 self.set_value_at(path, value);
470 }
471
472 fn coerce_text(&self, path: &[Seg], text: &str) -> Value {
483 match self.rule_at(path).and_then(|r| r.ty) {
484 Some(ty) => ty.coerce(text),
485 None => tree::parse_scalar(text),
486 }
487 }
488
489 pub fn rename_key(&mut self, path: &[Seg], new_key: &str) {
494 match path.last() {
495 Some(Seg::Key(_)) => {
496 let mut anchor = path[..path.len() - 1].to_vec();
497 anchor.push(Seg::Key(new_key.to_string()));
498 self.commit(
499 EditOp::RenameKey {
500 path: path.to_vec(),
501 new_key: new_key.to_string(),
502 },
503 anchor,
504 "renamed",
505 );
506 }
507 _ => self.status = "only mapping keys can be renamed".to_string(),
508 }
509 }
510
511 pub fn insert_key(&mut self, map_path: &[Seg], key: &str, value: Value) {
515 let mut anchor = map_path.to_vec();
516 anchor.push(Seg::Key(key.to_string()));
517 self.commit(
518 EditOp::InsertKey {
519 map_path: map_path.to_vec(),
520 key: key.to_string(),
521 value,
522 },
523 anchor,
524 "inserted",
525 );
526 }
527
528 pub fn insert_key_text(&mut self, map_path: &[Seg], key: &str, text: &str) {
538 let mut target = map_path.to_vec();
539 target.push(Seg::Key(key.to_string()));
540 let value = self.coerce_text(&target, text);
541 self.insert_key(map_path, key, value);
542 }
543
544 pub fn append_item(&mut self, seq_path: &[Seg], value: Value) {
546 let idx = self.seq_len(seq_path);
547 let mut anchor = seq_path.to_vec();
548 anchor.push(Seg::Index(idx));
549 self.commit(
550 EditOp::AppendItem {
551 seq_path: seq_path.to_vec(),
552 value,
553 },
554 anchor,
555 "appended",
556 );
557 }
558
559 pub fn append_item_text(&mut self, seq_path: &[Seg], text: &str) {
568 let mut target = seq_path.to_vec();
569 target.push(Seg::Index(self.seq_len(seq_path)));
570 let value = self.coerce_text(&target, text);
571 self.append_item(seq_path, value);
572 }
573
574 pub fn move_selected_up(&mut self) {
577 self.reorder_selected(-1);
578 }
579
580 pub fn move_selected_down(&mut self) {
582 self.reorder_selected(1);
583 }
584
585 fn reorder_selected(&mut self, delta: isize) {
589 let Some(row) = self.selected_row() else {
590 return;
591 };
592 let path = row.path.clone();
593 let Some(last) = path.last().cloned() else {
594 self.status = "cannot move the document root".to_string();
595 return;
596 };
597 let parent = path[..path.len() - 1].to_vec();
598 match last {
599 Seg::Index(i) => {
600 let len = self.seq_len(&parent);
601 let to = i as isize + delta;
602 if to < 0 || to as usize >= len {
603 self.status = "already at the edge".to_string();
604 return;
605 }
606 let to = to as usize;
607 let mut anchor = parent.clone();
608 anchor.push(Seg::Index(to));
609 self.commit(
610 EditOp::MoveItem {
611 seq_path: parent,
612 from: i,
613 to,
614 },
615 anchor,
616 "moved",
617 );
618 }
619 Seg::Key(k) => {
620 let keys = self.map_keys(&parent);
621 let Some(pos) = keys.iter().position(|x| *x == k) else {
622 return;
623 };
624 let target = pos as isize + delta;
625 if target < 0 || target as usize >= keys.len() {
626 self.status = "already at the edge".to_string();
627 return;
628 }
629 let mut order = keys;
630 order.swap(pos, target as usize);
631 self.commit(
632 EditOp::ReorderKeys {
633 map_path: parent,
634 keys: order,
635 },
636 path,
637 "moved",
638 );
639 }
640 }
641 }
642
643 pub fn value_at(&self, path: &[Seg]) -> Option<&Value> {
647 tree::value_at(&self.value, path)
648 }
649
650 fn map_keys(&self, path: &[Seg]) -> Vec<String> {
652 tree::map_keys(&self.value, path).unwrap_or_default()
653 }
654
655 pub fn seq_len(&self, path: &[Seg]) -> usize {
658 tree::seq_len(&self.value, path).unwrap_or(0)
659 }
660
661 pub fn delete_selected(&mut self) {
663 let Some(row) = self.selected_row() else {
664 return;
665 };
666 let path = row.path.clone();
667 let (op, anchor) = match path.last() {
668 Some(Seg::Index(i)) => {
669 let seq_path = path[..path.len() - 1].to_vec();
670 (
671 EditOp::RemoveItem {
672 seq_path: seq_path.clone(),
673 index: *i,
674 },
675 seq_path,
676 )
677 }
678 Some(Seg::Key(_)) => (
679 EditOp::DeleteKey { path: path.clone() },
680 path[..path.len() - 1].to_vec(),
681 ),
682 None => {
683 self.status = "cannot delete the document root".to_string();
684 return;
685 }
686 };
687 self.commit(op, anchor, "deleted");
688 }
689
690 fn commit(&mut self, op: EditOp, anchor: Vec<Seg>, msg: &str) {
696 if let Some(key) = op_root_key(&op)
700 && self.derived.contains(key)
701 {
702 self.status = format!("rejected: `{key}` is maintained by the workspace");
703 return;
704 }
705 let mut warn: Option<Issue> = None;
706 if let Some((path, value)) = op_target(&op)
707 && let Some(rule) = self.rule_at(&path)
708 {
709 match rule.validate(value) {
710 Validation::Reject(why) => {
711 self.status = format!("rejected: {why}");
712 return;
713 }
714 Validation::Warn(why) => warn = Some(why),
715 Validation::Ok => {}
716 }
717 }
718 match self.backend.apply(op) {
719 Ok(()) => {
720 self.after_edit(&anchor, msg);
721 if let Some(why) = warn {
723 self.status = why.to_string();
724 }
725 }
726 Err(e) => self.status = format!("rejected: {e}"),
728 }
729 }
730
731 fn after_edit(&mut self, anchor: &[Seg], msg: &str) {
734 if let Err(e) = self.reload() {
735 self.status = format!("view refresh failed: {e}");
736 return;
737 }
738 self.select_path(anchor);
739 self.dirty = true;
740 self.status = msg.to_string();
741 }
742}
743
744fn op_root_key(op: &EditOp) -> Option<&str> {
754 fn first_key(path: &[Seg]) -> Option<&str> {
755 match path.first() {
756 Some(Seg::Key(k)) => Some(k.as_str()),
757 _ => None,
758 }
759 }
760 match op {
761 EditOp::ReplaceValue { path, .. }
762 | EditOp::DeleteKey { path }
763 | EditOp::RenameKey { path, .. } => first_key(path),
764 EditOp::RemoveItem { seq_path, .. }
765 | EditOp::AppendItem { seq_path, .. }
766 | EditOp::MoveItem { seq_path, .. } => first_key(seq_path),
767 EditOp::InsertKey { map_path, key, .. } => match map_path.first() {
770 None => Some(key.as_str()),
771 _ => first_key(map_path),
772 },
773 EditOp::ReorderKeys { map_path, .. } => first_key(map_path),
775 }
776}
777
778fn op_target(op: &EditOp) -> Option<(Vec<Seg>, &Value)> {
779 match op {
780 EditOp::ReplaceValue { path, value } => Some((path.clone(), value)),
781 EditOp::InsertKey {
782 map_path,
783 key,
784 value,
785 } => {
786 let mut p = map_path.clone();
787 p.push(Seg::Key(key.clone()));
788 Some((p, value))
789 }
790 EditOp::AppendItem { seq_path, value } => {
791 let mut p = seq_path.clone();
792 p.push(Seg::Index(0));
793 Some((p, value))
794 }
795 _ => None,
796 }
797}
798
799#[cfg(test)]
800mod tests {
801 use super::*;
802 use crate::backend::FigBackend;
803 use fig::Format;
804
805 const SAMPLE: &str = "\
806# flower sample config — comments and formatting below should survive edits
807title = \"flower\"
808version = 1
809enabled = true
810
811# the server block
812[server]
813host = \"localhost\"
814port = 8080
815tags = [\"alpha\", \"beta\"]
816
817[server.limits]
818max_connections = 100
819timeout = 30.5
820";
821
822 fn sample_model() -> Model<FigBackend> {
823 let backend = FigBackend::open(SAMPLE.as_bytes(), Format::Toml).expect("open backend");
824 Model::new(backend).expect("build model")
825 }
826
827 fn select(model: &mut Model<FigBackend>, path: &[Seg]) {
828 model.selected = model
829 .rows
830 .iter()
831 .position(|r| r.path == path)
832 .unwrap_or_else(|| panic!("no row for {path:?}"));
833 }
834
835 fn type_value(model: &mut Model<FigBackend>, text: &str) {
836 if let Mode::Editing { buffer } = &mut model.mode {
837 buffer.clear();
838 }
839 for c in text.chars() {
840 model.edit_push(c);
841 }
842 model.edit_commit();
843 }
844
845 #[test]
846 fn edits_a_scalar_losslessly() {
847 let mut model = sample_model();
848
849 select(&mut model, &[Seg::Key("version".into())]);
850 model.begin_edit();
851 type_value(&mut model, "2");
852
853 let src = model.source_snapshot();
854 assert!(src.contains("version = 2"), "value changed:\n{src}");
855 assert!(src.contains("# the server block"), "comment preserved:\n{src}");
856 assert!(
857 src.contains("# flower sample config"),
858 "header preserved:\n{src}"
859 );
860 assert!(model.dirty);
861 }
862
863 #[test]
864 fn edits_a_nested_string() {
865 let mut model = sample_model();
866
867 select(
868 &mut model,
869 &[Seg::Key("server".into()), Seg::Key("host".into())],
870 );
871 model.begin_edit();
872 type_value(&mut model, "example.com");
873
874 let src = model.source_snapshot();
875 assert!(src.contains("host = \"example.com\""), "nested edit:\n{src}");
876 assert!(src.contains("port = 8080"), "sibling untouched:\n{src}");
877 }
878
879 #[test]
880 fn deletes_a_key() {
881 let mut model = sample_model();
882
883 select(&mut model, &[Seg::Key("enabled".into())]);
884 model.delete_selected();
885
886 let src = model.source_snapshot();
887 assert!(!src.contains("enabled = true"), "key removed:\n{src}");
888 assert!(src.contains("title = \"flower\""), "siblings kept:\n{src}");
889 }
890
891 #[test]
892 fn appends_a_sequence_item() {
893 let mut model = sample_model();
894 let tags = vec![Seg::Key("server".into()), Seg::Key("tags".into())];
895 model.append_item(&tags, Value::Str("gamma".into()));
896
897 let src = model.source_snapshot();
898 assert!(src.contains("gamma"), "item appended:\n{src}");
899 assert!(src.contains("alpha") && src.contains("beta"), "siblings kept");
900 assert!(model.dirty);
901 }
902
903 #[test]
904 fn inserts_a_mapping_key() {
905 let mut model = sample_model();
906 let server = vec![Seg::Key("server".into())];
907 model.insert_key(&server, "scheme", Value::Str("https".into()));
908
909 let src = model.source_snapshot();
910 assert!(
912 src.contains("scheme") && src.contains("= \"https\""),
913 "key inserted:\n{src}"
914 );
915 assert!(src.contains("host = \"localhost\""), "siblings kept");
916 }
917
918 #[test]
919 fn moves_a_sequence_item_and_reorders_keys() {
920 let mut model = sample_model();
921
922 select(
924 &mut model,
925 &[
926 Seg::Key("server".into()),
927 Seg::Key("tags".into()),
928 Seg::Index(1),
929 ],
930 );
931 model.move_selected_up();
932 let src = model.source_snapshot();
933 let a = src.find("alpha").unwrap();
934 let b = src.find("beta").unwrap();
935 assert!(b < a, "beta now precedes alpha:\n{src}");
936
937 select(&mut model, &[Seg::Key("title".into())]);
939 model.move_selected_down();
940 let src = model.source_snapshot();
941 assert!(
942 src.find("version").unwrap() < src.find("title").unwrap(),
943 "version now precedes title:\n{src}"
944 );
945 }
946
947 #[test]
948 fn hidden_top_level_keys_are_projected_out_but_kept_lossless() {
949 let backend = FigBackend::open(SAMPLE.as_bytes(), Format::Toml).expect("open");
950 let mut model =
951 Model::with_hidden(backend, vec!["title".into(), "enabled".into()]).expect("model");
952
953 assert!(!model.rows.iter().any(|r| r.path == [Seg::Key("title".into())]));
955 assert!(!model.rows.iter().any(|r| r.path == [Seg::Key("enabled".into())]));
956 assert!(model.rows.iter().any(|r| r.path == [Seg::Key("version".into())]));
958 assert!(model.source_snapshot().contains("title = \"flower\""));
960 assert!(model.source_snapshot().contains("enabled = true"));
961
962 select(&mut model, &[Seg::Key("version".into())]);
964 model.begin_edit();
965 type_value(&mut model, "9");
966 let src = model.source_snapshot();
967 assert!(src.contains("version = 9"));
968 assert!(src.contains("title = \"flower\"") && src.contains("enabled = true"));
969 }
970
971 #[test]
972 fn reorder_leaves_hidden_keys_in_place() {
973 let backend = FigBackend::open(SAMPLE.as_bytes(), Format::Toml).expect("open");
974 let mut model = Model::with_hidden(backend, vec!["title".into()]).expect("model");
975
976 select(&mut model, &[Seg::Key("enabled".into())]);
978 model.move_selected_up(); let src = model.source_snapshot();
980 let title = src.find("title").unwrap();
982 let version = src.find("version").unwrap();
983 let enabled = src.find("enabled").unwrap();
984 assert!(title < version && title < enabled, "title stayed put:\n{src}");
985 assert!(enabled < version, "enabled moved above version:\n{src}");
986 }
987
988 #[test]
989 fn inserts_a_root_level_key() {
990 let mut model = sample_model();
991 model.insert_key(&[], "root_flag", Value::Bool(true));
992 let src = model.source_snapshot();
993 assert!(src.contains("root_flag"), "root key inserted:\n{src}");
994 assert!(src.contains("title = \"flower\""), "existing kept");
995 }
996
997 #[test]
998 fn renames_a_key_losslessly() {
999 let mut model = sample_model();
1000 select(&mut model, &[Seg::Key("version".into())]);
1001 model.rename_key(&[Seg::Key("version".into())], "revision");
1002 let src = model.source_snapshot();
1003 assert!(
1005 src.contains("revision") && src.contains("= 1"),
1006 "renamed with value kept:\n{src}"
1007 );
1008 assert!(!src.contains("version = 1"), "old key gone");
1009 assert_eq!(model.rows[model.selected].path, [Seg::Key("revision".into())]);
1011 }
1012
1013 #[test]
1014 fn rename_rejects_a_sequence_item() {
1015 let mut model = sample_model();
1016 model.rename_key(
1017 &[
1018 Seg::Key("server".into()),
1019 Seg::Key("tags".into()),
1020 Seg::Index(0),
1021 ],
1022 "nope",
1023 );
1024 assert!(model.status.contains("mapping keys"));
1025 }
1026
1027 #[test]
1028 fn schema_closed_vocabulary_rejects_an_unknown_edit() {
1029 use crate::schema::{Constraint, FieldRule};
1030 use fig_schema::{FieldType, PathPat, Presentation, Term};
1031 let src = "audience = [\"public\"]\ntitle = \"note\"\n";
1032 let backend = FigBackend::open(src.as_bytes(), Format::Toml).expect("open");
1033 let mut model = Model::new(backend).expect("model");
1034 model.set_schema(crate::schema::Schema::new(vec![FieldRule {
1035 at: PathPat::each_item_of("audience"),
1036 ty: Some(FieldType::Str),
1037 constraint: Some(Constraint::Enum {
1038 values: vec![Term::value("public"), Term::value("private")],
1039 closed: true,
1040 }),
1041 present: Presentation::default(),
1042 }]));
1043
1044 select(&mut model, &[Seg::Key("audience".into()), Seg::Index(0)]);
1047 model.begin_edit();
1048 type_value(&mut model, "familly");
1049 assert!(model.status.contains("rejected"), "status: {}", model.status);
1050 assert!(
1051 model.source_snapshot().contains("public"),
1052 "document unchanged:\n{}",
1053 model.source_snapshot()
1054 );
1055
1056 model.begin_edit();
1058 type_value(&mut model, "private");
1059 let out = model.source_snapshot();
1060 assert!(out.contains("private"), "known value applied:\n{out}");
1061 assert!(!out.contains("public"), "old value replaced:\n{out}");
1062 }
1063
1064 #[test]
1068 fn addable_fields_are_the_declared_keys_the_document_lacks() {
1069 use crate::schema::{Constraint, FieldRule};
1070 use fig_schema::{FieldType, PathPat, Presentation, Term};
1071 let src = "audience = [\"public\"]\ntitle = \"note\"\n";
1072 let backend = FigBackend::open(src.as_bytes(), Format::Toml).expect("open");
1073 let mut model =
1074 Model::with_hidden(backend, vec!["title".into(), "updated".into()]).expect("model");
1075 model.set_schema(crate::schema::Schema::new(vec![
1076 FieldRule {
1078 at: PathPat::key("audience"),
1079 ty: Some(FieldType::Str),
1080 constraint: None,
1081 present: Presentation::default(),
1082 },
1083 FieldRule {
1085 at: PathPat::each_item_of("audience"),
1086 ty: Some(FieldType::Str),
1087 constraint: Some(Constraint::Enum {
1088 values: vec![Term::value("public")],
1089 closed: true,
1090 }),
1091 present: Presentation::default(),
1092 },
1093 FieldRule {
1095 at: PathPat::key("created"),
1096 ty: Some(FieldType::Str),
1097 constraint: None,
1098 present: Presentation::default(),
1099 },
1100 FieldRule {
1102 at: PathPat::key("updated"),
1103 ty: Some(FieldType::Str),
1104 constraint: None,
1105 present: Presentation::default(),
1106 },
1107 ]));
1108
1109 let offered: Vec<_> = model
1110 .addable_fields()
1111 .iter()
1112 .map(|r| match r.at.0.as_slice() {
1113 [SegPat::Key(k)] => k.clone(),
1114 _ => unreachable!("only single-key rules are offered"),
1115 })
1116 .collect();
1117 assert_eq!(offered, vec!["created".to_string()]);
1118
1119 model.insert_key(&[], "created", Value::Str("2026-07-24".into()));
1121 assert!(model.addable_fields().is_empty());
1122 }
1123
1124 #[test]
1127 fn a_derived_field_is_visible_but_declines_edits() {
1128 let src = "title = \"note\"\nupdated = \"2026-07-01\"\ncreated = \"2026-06-01\"\n";
1129 let backend = FigBackend::open(src.as_bytes(), Format::Toml).expect("open");
1130 let mut model =
1131 Model::with_managed(backend, vec!["title".into()], vec!["updated".into()])
1132 .expect("model");
1133
1134 let labels: Vec<&str> = model.rows.iter().map(|r| r.label.as_str()).collect();
1136 assert_eq!(labels, vec!["updated", "created"]);
1137 assert!(model.is_derived(&[Seg::Key("updated".into())]));
1138 assert!(!model.is_derived(&[Seg::Key("created".into())]));
1139
1140 model.set_scalar_text(&[Seg::Key("updated".into())], "2026-01-01");
1142 assert!(model.status.contains("maintained by the workspace"));
1143 model.rename_key(&[Seg::Key("updated".into())], "modified");
1144 assert!(model.status.contains("maintained by the workspace"));
1145 model.selected = 0;
1146 model.delete_selected();
1147 assert!(model.status.contains("maintained by the workspace"));
1148 let out = model.source_snapshot();
1149 assert!(out.contains("updated = \"2026-07-01\""), "unchanged:\n{out}");
1150
1151 model.set_scalar_text(&[Seg::Key("created".into())], "2026-06-15");
1153 assert!(model.source_snapshot().contains("2026-06-15"));
1154 }
1155
1156 #[test]
1159 fn addable_fields_are_empty_without_a_schema() {
1160 let src = "title = \"note\"\n";
1161 let backend = FigBackend::open(src.as_bytes(), Format::Toml).expect("open");
1162 let model = Model::new(backend).expect("model");
1163 assert!(model.addable_fields().is_empty());
1164 }
1165
1166 #[test]
1167 fn schema_typed_field_keeps_a_numeric_string_as_text() {
1168 use crate::schema::FieldRule;
1169 use fig_schema::{FieldType, PathPat, Presentation};
1170 let src = "code = \"x\"\n";
1171 let backend = FigBackend::open(src.as_bytes(), Format::Toml).expect("open");
1172 let mut model = Model::new(backend).expect("model");
1173 model.set_schema(crate::schema::Schema::new(vec![FieldRule {
1174 at: PathPat::key("code"),
1175 ty: Some(FieldType::Str),
1176 constraint: None,
1177 present: Presentation::default(),
1178 }]));
1179
1180 select(&mut model, &[Seg::Key("code".into())]);
1181 model.begin_edit();
1182 type_value(&mut model, "123");
1183 let out = model.source_snapshot();
1186 assert!(out.contains("code = \"123\""), "kept as string:\n{out}");
1187 }
1188
1189 #[test]
1192 fn containers_can_arrive_collapsed() {
1193 let backend = FigBackend::open(SAMPLE.as_bytes(), Format::Toml).expect("open");
1194 let model = Model::with_collapsed(
1195 backend,
1196 Vec::new(),
1197 Vec::new(),
1198 vec![
1199 vec![Seg::Key("server".into())],
1200 vec![Seg::Key("title".into())],
1203 ],
1204 )
1205 .expect("model");
1206
1207 let server = model
1208 .rows
1209 .iter()
1210 .find(|r| r.path == [Seg::Key("server".into())])
1211 .expect("server row");
1212 assert!(!server.expanded, "collapsed before the first frame");
1213 assert!(
1214 !model.rows.iter().any(|r| r.path.len() > 1),
1215 "no descendant rows: {:?}",
1216 model.rows.iter().map(|r| &r.label).collect::<Vec<_>>()
1217 );
1218 assert!(
1220 model
1221 .rows
1222 .iter()
1223 .any(|r| r.path == [Seg::Key("title".into())])
1224 );
1225 assert_eq!(model.selected, 0, "selection untouched");
1226 }
1227
1228 #[test]
1231 fn set_collapsed_folds_by_path_without_moving_the_selection() {
1232 let mut model = sample_model();
1233 select(&mut model, &[Seg::Key("title".into())]);
1234
1235 model.set_collapsed(&[Seg::Key("server".into())], true);
1236 assert!(model.is_collapsed(&[Seg::Key("server".into())]));
1237 assert!(
1238 !model.rows.iter().any(|r| r.path.len() > 1),
1239 "children hidden"
1240 );
1241 assert_eq!(
1242 model.rows[model.selected].path,
1243 [Seg::Key("title".into())],
1244 "selection stayed on title"
1245 );
1246
1247 model.set_collapsed(&[Seg::Key("server".into())], false);
1248 assert!(!model.is_collapsed(&[Seg::Key("server".into())]));
1249 assert!(
1250 model
1251 .rows
1252 .iter()
1253 .any(|r| r.path == [Seg::Key("server".into()), Seg::Key("host".into())])
1254 );
1255 assert_eq!(model.rows[model.selected].path, [Seg::Key("title".into())]);
1256 }
1257
1258 #[test]
1260 fn set_collapsed_reanchors_a_selection_it_swallowed() {
1261 let mut model = sample_model();
1262 select(
1263 &mut model,
1264 &[Seg::Key("server".into()), Seg::Key("host".into())],
1265 );
1266 model.set_collapsed(&[Seg::Key("server".into())], true);
1267 assert_eq!(
1268 model.rows[model.selected].path,
1269 [Seg::Key("server".into())],
1270 "landed on the container that swallowed it"
1271 );
1272 }
1273
1274 #[test]
1277 fn insert_and_append_are_type_directed_by_the_schema() {
1278 use crate::schema::FieldRule;
1279 use fig_schema::{FieldType, PathPat, Presentation};
1280 let src = "tags = [\"alpha\"]\n\n[meta]\nk = \"v\"\n";
1281 let backend = FigBackend::open(src.as_bytes(), Format::Toml).expect("open");
1282 let mut model = Model::new(backend).expect("model");
1283 model.set_schema(crate::schema::Schema::new(vec![
1284 FieldRule {
1286 at: PathPat::each_item_of("tags"),
1287 ty: Some(FieldType::Str),
1288 constraint: None,
1289 present: Presentation::default(),
1290 },
1291 FieldRule {
1292 at: PathPat::key("year"),
1293 ty: Some(FieldType::Str),
1294 constraint: None,
1295 present: Presentation::default(),
1296 },
1297 FieldRule {
1298 at: PathPat(vec![
1299 fig_schema::SegPat::Key("meta".into()),
1300 fig_schema::SegPat::Key("code".into()),
1301 ]),
1302 ty: Some(FieldType::Str),
1303 constraint: None,
1304 present: Presentation::default(),
1305 },
1306 ]));
1307
1308 model.append_item_text(&[Seg::Key("tags".into())], "2026");
1309 model.insert_key_text(&[], "year", "2026");
1310 model.insert_key_text(&[Seg::Key("meta".into())], "code", "2026");
1312
1313 let out = model.source_snapshot();
1314 assert!(
1315 !out.contains("2026,") && !out.contains("[2026]") && !out.contains("= 2026"),
1316 "no bare integers survived the schema:\n{out}"
1317 );
1318 assert_eq!(
1319 model.value_at(&[Seg::Key("tags".into()), Seg::Index(1)]),
1320 Some(&Value::Str("2026".into())),
1321 "list item took the each-item type:\n{out}"
1322 );
1323 assert_eq!(
1324 model.value_at(&[Seg::Key("year".into())]),
1325 Some(&Value::Str("2026".into()))
1326 );
1327 assert_eq!(
1328 model.value_at(&[Seg::Key("meta".into()), Seg::Key("code".into())]),
1329 Some(&Value::Str("2026".into()))
1330 );
1331 }
1332
1333 #[test]
1337 fn insert_and_append_text_shape_guess_without_a_schema() {
1338 let mut model = sample_model();
1339 model.append_item_text(&[Seg::Key("server".into()), Seg::Key("tags".into())], "42");
1340 model.insert_key_text(&[], "count", "7");
1341 assert_eq!(
1342 model.value_at(&[
1343 Seg::Key("server".into()),
1344 Seg::Key("tags".into()),
1345 Seg::Index(2)
1346 ]),
1347 Some(&Value::Int(42))
1348 );
1349 assert_eq!(
1350 model.value_at(&[Seg::Key("count".into())]),
1351 Some(&Value::Int(7))
1352 );
1353 }
1354
1355 #[test]
1357 fn tree_walkers_resolve_paths_and_reject_mismatches() {
1358 let model = sample_model();
1359 let root = model.value_at(&[]).expect("root");
1360
1361 assert_eq!(
1362 tree::value_at(root, &[Seg::Key("server".into()), Seg::Key("port".into())]),
1363 Some(&Value::Int(8080))
1364 );
1365 assert_eq!(
1366 tree::seq_len(root, &[Seg::Key("server".into()), Seg::Key("tags".into())]),
1367 Some(2)
1368 );
1369 assert_eq!(tree::seq_len(root, &[Seg::Key("title".into())]), None);
1372 assert_eq!(tree::seq_len(root, &[Seg::Key("absent".into())]), None);
1373 assert_eq!(
1374 tree::map_keys(root, &[Seg::Key("server".into())]),
1375 Some(vec![
1376 "host".to_string(),
1377 "port".to_string(),
1378 "tags".to_string(),
1379 "limits".to_string()
1380 ])
1381 );
1382 assert_eq!(tree::map_keys(root, &[Seg::Key("title".into())]), None);
1383 assert_eq!(
1385 tree::value_at(
1386 root,
1387 &[
1388 Seg::Key("server".into()),
1389 Seg::Key("tags".into()),
1390 Seg::Key("0".into())
1391 ]
1392 ),
1393 None
1394 );
1395 }
1396
1397 #[test]
1398 fn navigation_folds_and_reanchors() {
1399 let mut model = sample_model();
1400
1401 select(&mut model, &[Seg::Key("server".into())]);
1402 model.collapse_or_leave();
1403 assert!(
1404 !model
1405 .rows
1406 .iter()
1407 .any(|r| r.path == [Seg::Key("server".into()), Seg::Key("host".into())]),
1408 "collapsed children hidden"
1409 );
1410 assert_eq!(model.rows[model.selected].path, [Seg::Key("server".into())]);
1411 }
1412}