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 pub(crate) 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 /// Whether this row's *label* can be changed — true for a mapping entry,
139 /// false for a sequence item, whose label is its index. The peer of
140 /// [`PageItem::can_rename`](crate::PageItem::can_rename), so the two
141 /// projections answer it the same way.
142 pub fn can_rename(&self) -> bool {
143 matches!(self.path.last(), Some(Seg::Key(_)))
144 }
145}
146
147/// Render a mapping key `Value` as a display string.
148pub(crate) fn key_to_string(k: &Value) -> String {
149 match k {
150 Value::Str(s) => s.clone(),
151 Value::Int(i) => i.to_string(),
152 Value::Uint(u) => u.to_string(),
153 Value::Bool(b) => b.to_string(),
154 other => format!("{other:?}"),
155 }
156}
157
158/// A compact one-line preview of a value.
159///
160/// One line means one line: a multi-line string (a YAML block scalar, a shell
161/// script in a `run:` key) is cut at its first line break with an ellipsis. A
162/// renderer that took the whole thing would draw a row several lines tall and
163/// throw the rest of the list out of alignment — and every caller of this asks
164/// for a value it can put on one row.
165///
166/// [`edit_seed`] is the deliberate exception: it hands back the *whole* string,
167/// so what you edit is never the abbreviation you were shown.
168pub fn preview(v: &Value) -> String {
169 match v {
170 Value::Null => "null".to_string(),
171 Value::Bool(b) => b.to_string(),
172 Value::Int(i) => i.to_string(),
173 Value::Uint(u) => u.to_string(),
174 Value::Float(f) => f.to_string(),
175 Value::Str(s) => first_line(s),
176 Value::Extended { text, .. } => first_line(text),
177 Value::Map(entries) => format!("{{{}}}", entries.len()),
178 Value::Seq(items) => format!("[{}]", items.len()),
179 }
180}
181
182/// `s` up to its first line break, marked with an ellipsis when there was more.
183fn first_line(s: &str) -> String {
184 match s.split_once('\n') {
185 Some((head, _)) => format!("{} …", head.trim_end()),
186 None => s.to_string(),
187 }
188}
189
190/// The editable text a scalar starts with when you enter edit mode.
191pub fn edit_seed(v: &Value) -> String {
192 match v {
193 Value::Str(s) => s.clone(),
194 other => preview(other),
195 }
196}
197
198/// Build the flat row list from a document root, honoring the collapsed set
199/// (paths whose containers are collapsed) and a set of **top-level** mapping keys
200/// to hide.
201///
202/// Hiding is scoped to the root map's own entries — a nested key that happens to
203/// share a hidden name is untouched. The hidden entries stay in the underlying
204/// `Value` (and therefore in the document's bytes); they merely produce no row.
205/// This is how a consumer whose format reserves some top-level keys (e.g. prov's
206/// managed frontmatter — `id`, `prov`, `contents`, …) keeps them lossless while
207/// showing the user only their own fields. Because only the *projection* is
208/// filtered — never the `Value` itself — sibling reorders still see the full key
209/// order and leave the hidden keys in place.
210pub fn build_rows(
211 root: &Value,
212 collapsed: &HashSet<Vec<Seg>>,
213 hidden_top_level: &HashSet<String>,
214) -> Vec<Row> {
215 let mut rows = Vec::new();
216 match root {
217 // A map/seq root shows its children at depth 0 (no synthetic root row).
218 Value::Map(entries) => {
219 for (k, v) in entries {
220 let key = key_to_string(k);
221 if hidden_top_level.contains(&key) {
222 continue;
223 }
224 push_node(
225 &key,
226 v,
227 vec![Seg::Key(key.clone())],
228 0,
229 collapsed,
230 &mut rows,
231 );
232 }
233 }
234 Value::Seq(items) => {
235 for (i, v) in items.iter().enumerate() {
236 push_node(
237 &format!("[{i}]"),
238 v,
239 vec![Seg::Index(i)],
240 0,
241 collapsed,
242 &mut rows,
243 );
244 }
245 }
246 // A scalar (or empty/null) document is a single row.
247 other => push_node("", other, Vec::new(), 0, collapsed, &mut rows),
248 }
249 rows
250}
251
252fn push_node(
253 label: &str,
254 v: &Value,
255 path: Vec<Seg>,
256 depth: usize,
257 collapsed: &HashSet<Vec<Seg>>,
258 rows: &mut Vec<Row>,
259) {
260 let vkind = VKind::of(v);
261 let is_container = matches!(vkind, VKind::Map | VKind::Seq);
262 let expanded = is_container && !collapsed.contains(&path);
263
264 rows.push(Row {
265 depth,
266 label: label.to_string(),
267 vkind,
268 preview: preview(v),
269 expanded,
270 path: path.clone(),
271 });
272
273 if expanded {
274 match v {
275 Value::Map(entries) => {
276 for (k, child) in entries {
277 let key = key_to_string(k);
278 let mut p = path.clone();
279 p.push(Seg::Key(key.clone()));
280 push_node(&key, child, p, depth + 1, collapsed, rows);
281 }
282 }
283 Value::Seq(items) => {
284 for (i, child) in items.iter().enumerate() {
285 let mut p = path.clone();
286 p.push(Seg::Index(i));
287 push_node(&format!("[{i}]"), child, p, depth + 1, collapsed, rows);
288 }
289 }
290 _ => {}
291 }
292 }
293}
294
295/// Parse an edit-buffer string into a `Value`, inferring type by literal shape.
296/// A prototype heuristic — a schema layer would instead pick the type the key
297/// expects and validate against it. fig's editor reparse is the backstop: a
298/// value that can't splice validly is rejected and rolled back.
299pub fn parse_scalar(s: &str) -> Value {
300 let t = s.trim();
301 match t {
302 "true" => return Value::Bool(true),
303 "false" => return Value::Bool(false),
304 "null" => return Value::Null,
305 _ => {}
306 }
307 if let Ok(i) = t.parse::<i64>() {
308 return Value::Int(i);
309 }
310 if let Ok(u) = t.parse::<u64>() {
311 return Value::Uint(u);
312 }
313 if let Ok(f) = t.parse::<f64>() {
314 return Value::Float(f);
315 }
316 Value::Str(s.to_string())
317}