Skip to main content

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}