flower_core/tree.rs
1//! The navigable view of a config document: a flat list of `Row`s derived from
2//! fig's `Value` tree.
3//!
4//! Each row carries its **fig path** — a `Vec<Seg>` of mapping keys and sequence
5//! indices from the document root. That path is exactly what `fig::Editor`'s ops
6//! take (`&[fig::Segment]`), so navigation and editing speak the same language:
7//! move the selection to a row, then hand its path straight to
8//! `replace_value` / `delete` / `remove_item` / … .
9//!
10//! Flattening (rather than bough's recursive path-arithmetic over a nested tree)
11//! keeps j/k navigation a single index step and naturally handles the fact that
12//! a config path interleaves keys and indices.
13
14use std::collections::HashSet;
15
16use fig::Value;
17pub use fig_schema::Seg;
18
19/// Borrow an owned path as fig's `Segment` slice for an editor call.
20pub fn to_fig(path: &[Seg]) -> Vec<fig::Segment<'_>> {
21 path.iter()
22 .map(|s| match s {
23 Seg::Key(k) => fig::Segment::Key(k.as_str()),
24 Seg::Index(i) => fig::Segment::Index(*i),
25 })
26 .collect()
27}
28
29/// The node a fig path names in `root`, or `None` when the path doesn't resolve.
30/// The empty path is the root itself.
31///
32/// The one walk from a path to its node. Public because `Model` is not the only
33/// thing that needs it: a [`Backend`](crate::Backend) lowering an op it has no
34/// native primitive for has to read the current tree, and it can't see the model
35/// (the model owns *it*). A frontend resolving a row's value by path is the other
36/// caller. Both would otherwise reimplement this traversal.
37pub fn value_at<'v>(root: &'v Value, path: &[Seg]) -> Option<&'v Value> {
38 let mut cur = root;
39 for seg in path {
40 cur = match (seg, cur) {
41 (Seg::Key(k), Value::Map(entries)) => {
42 &entries
43 .iter()
44 .find(|(mk, _)| matches!(mk, Value::Str(s) if s == k))?
45 .1
46 }
47 (Seg::Index(i), Value::Seq(items)) => items.get(*i)?,
48 _ => return None,
49 };
50 }
51 Some(cur)
52}
53
54/// The length of the sequence at `path`. `None` when the path doesn't resolve or
55/// names something that isn't a sequence — the two cases a caller sizing an
56/// append index or a reorder permutation has to tell apart from an empty list.
57pub fn seq_len(root: &Value, path: &[Seg]) -> Option<usize> {
58 match value_at(root, path)? {
59 Value::Seq(items) => Some(items.len()),
60 _ => None,
61 }
62}
63
64/// The mapping keys at `path`, in document order. `None` when the path doesn't
65/// resolve or names something that isn't a mapping.
66///
67/// Non-string keys are skipped: the editor addresses entries by name, so a key
68/// it cannot name is one it cannot reorder or rename.
69pub fn map_keys(root: &Value, path: &[Seg]) -> Option<Vec<String>> {
70 match value_at(root, path)? {
71 Value::Map(entries) => Some(
72 entries
73 .iter()
74 .filter_map(|(k, _)| match k {
75 Value::Str(s) => Some(s.clone()),
76 _ => None,
77 })
78 .collect(),
79 ),
80 _ => None,
81 }
82}
83
84/// The value kind of a row, for styling and container logic.
85#[derive(Clone, Copy, Debug, PartialEq, Eq)]
86pub enum VKind {
87 Null,
88 Bool,
89 Int,
90 Float,
91 Str,
92 Ext,
93 Map,
94 Seq,
95}
96
97impl VKind {
98 fn of(v: &Value) -> Self {
99 match v {
100 Value::Null => VKind::Null,
101 Value::Bool(_) => VKind::Bool,
102 Value::Int(_) | Value::Uint(_) => VKind::Int,
103 Value::Float(_) => VKind::Float,
104 Value::Str(_) => VKind::Str,
105 Value::Extended { .. } => VKind::Ext,
106 Value::Map(_) => VKind::Map,
107 Value::Seq(_) => VKind::Seq,
108 }
109 }
110}
111
112/// One visible line of the tree.
113#[derive(Clone, Debug)]
114pub struct Row {
115 /// Nesting depth (top-level entries are depth 0).
116 pub depth: usize,
117 /// The mapping key, or `[i]` for a sequence item.
118 pub label: String,
119 pub vkind: VKind,
120 /// A one-line rendering of the value (the scalar text, or `{n}` / `[n]`).
121 pub preview: String,
122 /// Meaningful only for containers: whether it is currently expanded.
123 pub expanded: bool,
124 /// The fig path to this node from the document root.
125 pub path: Vec<Seg>,
126}
127
128impl Row {
129 pub fn is_container(&self) -> bool {
130 matches!(self.vkind, VKind::Map | VKind::Seq)
131 }
132
133 /// A scalar can be edited in place; a container cannot.
134 pub fn is_scalar(&self) -> bool {
135 !self.is_container()
136 }
137}
138
139/// Render a mapping key `Value` as a display string.
140fn key_to_string(k: &Value) -> String {
141 match k {
142 Value::Str(s) => s.clone(),
143 Value::Int(i) => i.to_string(),
144 Value::Uint(u) => u.to_string(),
145 Value::Bool(b) => b.to_string(),
146 other => format!("{other:?}"),
147 }
148}
149
150/// A compact one-line preview of a value.
151pub fn preview(v: &Value) -> String {
152 match v {
153 Value::Null => "null".to_string(),
154 Value::Bool(b) => b.to_string(),
155 Value::Int(i) => i.to_string(),
156 Value::Uint(u) => u.to_string(),
157 Value::Float(f) => f.to_string(),
158 Value::Str(s) => s.clone(),
159 Value::Extended { text, .. } => text.clone(),
160 Value::Map(entries) => format!("{{{}}}", entries.len()),
161 Value::Seq(items) => format!("[{}]", items.len()),
162 }
163}
164
165/// The editable text a scalar starts with when you enter edit mode.
166pub fn edit_seed(v: &Value) -> String {
167 match v {
168 Value::Str(s) => s.clone(),
169 other => preview(other),
170 }
171}
172
173/// Build the flat row list from a document root, honoring the collapsed set
174/// (paths whose containers are collapsed) and a set of **top-level** mapping keys
175/// to hide.
176///
177/// Hiding is scoped to the root map's own entries — a nested key that happens to
178/// share a hidden name is untouched. The hidden entries stay in the underlying
179/// `Value` (and therefore in the document's bytes); they merely produce no row.
180/// This is how a consumer whose format reserves some top-level keys (e.g. prov's
181/// managed frontmatter — `id`, `prov`, `contents`, …) keeps them lossless while
182/// showing the user only their own fields. Because only the *projection* is
183/// filtered — never the `Value` itself — sibling reorders still see the full key
184/// order and leave the hidden keys in place.
185pub fn build_rows(
186 root: &Value,
187 collapsed: &HashSet<Vec<Seg>>,
188 hidden_top_level: &HashSet<String>,
189) -> Vec<Row> {
190 let mut rows = Vec::new();
191 match root {
192 // A map/seq root shows its children at depth 0 (no synthetic root row).
193 Value::Map(entries) => {
194 for (k, v) in entries {
195 let key = key_to_string(k);
196 if hidden_top_level.contains(&key) {
197 continue;
198 }
199 push_node(&key, v, vec![Seg::Key(key.clone())], 0, collapsed, &mut rows);
200 }
201 }
202 Value::Seq(items) => {
203 for (i, v) in items.iter().enumerate() {
204 push_node(&format!("[{i}]"), v, vec![Seg::Index(i)], 0, collapsed, &mut rows);
205 }
206 }
207 // A scalar (or empty/null) document is a single row.
208 other => push_node("", other, Vec::new(), 0, collapsed, &mut rows),
209 }
210 rows
211}
212
213fn push_node(
214 label: &str,
215 v: &Value,
216 path: Vec<Seg>,
217 depth: usize,
218 collapsed: &HashSet<Vec<Seg>>,
219 rows: &mut Vec<Row>,
220) {
221 let vkind = VKind::of(v);
222 let is_container = matches!(vkind, VKind::Map | VKind::Seq);
223 let expanded = is_container && !collapsed.contains(&path);
224
225 rows.push(Row {
226 depth,
227 label: label.to_string(),
228 vkind,
229 preview: preview(v),
230 expanded,
231 path: path.clone(),
232 });
233
234 if expanded {
235 match v {
236 Value::Map(entries) => {
237 for (k, child) in entries {
238 let key = key_to_string(k);
239 let mut p = path.clone();
240 p.push(Seg::Key(key.clone()));
241 push_node(&key, child, p, depth + 1, collapsed, rows);
242 }
243 }
244 Value::Seq(items) => {
245 for (i, child) in items.iter().enumerate() {
246 let mut p = path.clone();
247 p.push(Seg::Index(i));
248 push_node(&format!("[{i}]"), child, p, depth + 1, collapsed, rows);
249 }
250 }
251 _ => {}
252 }
253 }
254}
255
256/// Parse an edit-buffer string into a `Value`, inferring type by literal shape.
257/// A prototype heuristic — a schema layer would instead pick the type the key
258/// expects and validate against it. fig's editor reparse is the backstop: a
259/// value that can't splice validly is rejected and rolled back.
260pub fn parse_scalar(s: &str) -> Value {
261 let t = s.trim();
262 match t {
263 "true" => return Value::Bool(true),
264 "false" => return Value::Bool(false),
265 "null" => return Value::Null,
266 _ => {}
267 }
268 if let Ok(i) = t.parse::<i64>() {
269 return Value::Int(i);
270 }
271 if let Ok(u) = t.parse::<u64>() {
272 return Value::Uint(u);
273 }
274 if let Ok(f) = t.parse::<f64>() {
275 return Value::Float(f);
276 }
277 Value::Str(s.to_string())
278}