Skip to main content

ephemeris_documents/toml/
order.rs

1//! Table order and positions that make a merge converge.
2
3use toml_edit::{Item, Table};
4
5use super::decor::prefix_str;
6
7/// One table, in walk order.
8struct Slot {
9    depth: usize,
10    old: Option<isize>,
11    prints_header: bool,
12    has_glued_comment: bool,
13    rank: usize,
14    blank_before: bool,
15}
16
17impl Slot {
18    fn of(table: &Table, depth: usize) -> Self {
19        Self {
20            depth,
21            old: table.position(),
22            prints_header: !table.is_implicit() || Self::has_values(table),
23            has_glued_comment: prefix_str(table.decor())
24                .is_some_and(|prefix| !prefix.is_empty() && !prefix.starts_with(['\n', '\r'])),
25            rank: 0,
26            blank_before: false,
27        }
28    }
29
30    fn has_values(table: &Table) -> bool {
31        table.iter().any(|(_, item)| match item {
32            Item::Table(child) => child.is_dotted(),
33            Item::ArrayOfTables(_) => false,
34            Item::Value(_) | Item::None => true,
35        })
36    }
37
38    const fn is_new(&self) -> bool {
39        self.old.is_none()
40    }
41}
42
43/// The positions the tables of a merged tree get.
44///
45/// Existing tables keep their relative source order. A table without a
46/// position is new and goes after the last table of its parent's subtree, so
47/// it ends up at the end of its parent.
48pub(super) struct TableOrder(Vec<Slot>);
49
50impl TableOrder {
51    /// Plan the order of every table under `root`, including `root`.
52    pub(super) fn of(root: &mut Table) -> Self {
53        let mut slots = Vec::new();
54        let root_has_values = Slot::has_values(root);
55        Self::walk(root, 0, &mut |table, depth| {
56            slots.push(Slot::of(table, depth));
57        });
58        Self::place_root_first(&mut slots, root_has_values);
59        let order = Self::ordered(&slots);
60        for (rank, &index) in order.iter().enumerate() {
61            let blank_before = Self::needs_blank(&slots, &order, rank);
62            if let Some(slot) = slots.get_mut(index) {
63                slot.rank = rank;
64                slot.blank_before = blank_before;
65            }
66        }
67        Self(slots)
68    }
69
70    /// The root has no header, so it only prints when it holds values.
71    const fn place_root_first(slots: &mut [Slot], has_values: bool) {
72        if let Some(root) = slots.first_mut() {
73            root.old = None;
74            root.prints_header = has_values;
75        }
76    }
77
78    /// Walk indices in final order: existing tables by old position, then
79    /// every new table after the last table of its parent's subtree.
80    fn ordered(slots: &[Slot]) -> Vec<usize> {
81        let mut order: Vec<usize> = (0..slots.len())
82            .filter(|&index| slots.get(index).is_some_and(|slot| !slot.is_new()))
83            .collect();
84        order.sort_by_key(|&index| (slots.get(index).and_then(|slot| slot.old), index));
85        for (index, slot) in slots.iter().enumerate() {
86            if slot.is_new() {
87                let after = Self::slot_after_subtree(slots, &order, index);
88                order.insert(after, index);
89            }
90        }
91        order
92    }
93
94    /// The place for the new table at `index`. It goes right before its own
95    /// first existing descendant. Without one, it goes after the last table
96    /// of its parent's subtree. The root goes to the front.
97    fn slot_after_subtree(slots: &[Slot], order: &[usize], index: usize) -> usize {
98        let own = index.saturating_add(1)..Self::subtree_end(slots, index);
99        if let Some(at) = order.iter().position(|other| own.contains(other)) {
100            return at;
101        }
102        let depth = slots.get(index).map_or(0, |slot| slot.depth);
103        let Some(parent) = slots
104            .get(..index)
105            .and_then(|before| before.iter().rposition(|slot| slot.depth < depth))
106        else {
107            return 0;
108        };
109        let family = parent..Self::subtree_end(slots, parent);
110        order
111            .iter()
112            .rposition(|other| family.contains(other))
113            .map_or(0, |at| at.saturating_add(1))
114    }
115
116    /// The walk index after the last descendant of the table at `index`.
117    fn subtree_end(slots: &[Slot], index: usize) -> usize {
118        let depth = slots.get(index).map_or(0, |slot| slot.depth);
119        slots
120            .iter()
121            .enumerate()
122            .skip(index.saturating_add(1))
123            .find(|(_, slot)| slot.depth <= depth)
124            .map_or(slots.len(), |(at, _)| at)
125    }
126
127    /// Whether the table at `rank` needs a blank line before its decor. That
128    /// holds for an existing table that directly follows a new table that
129    /// prints a header, other than the root. It also holds for a new table
130    /// that brings a comment, which replaces the blank line a table gets by
131    /// default, when something is printed before it.
132    fn needs_blank(slots: &[Slot], order: &[usize], rank: usize) -> bool {
133        let slot = order.get(rank).and_then(|&index| slots.get(index));
134        let is_existing = slot.is_some_and(|slot| !slot.is_new());
135        let is_commented_header = rank > 0
136            && slot
137                .is_some_and(|slot| slot.is_new() && slot.prints_header && slot.has_glued_comment)
138            && Self::prints_before(slots, order, rank);
139        let follows_new_header = rank
140            .checked_sub(1)
141            .and_then(|before| order.get(before))
142            .is_some_and(|&before| {
143                before != 0
144                    && slots
145                        .get(before)
146                        .is_some_and(|previous| previous.is_new() && previous.prints_header)
147            });
148        (is_existing && follows_new_header) || is_commented_header
149    }
150
151    /// Whether any table or value before `rank` prints a line.
152    fn prints_before(slots: &[Slot], order: &[usize], rank: usize) -> bool {
153        order
154            .iter()
155            .take(rank)
156            .any(|&index| slots.get(index).is_some_and(|slot| slot.prints_header))
157    }
158
159    /// Number the tables under `root`, which must be the tree this order was
160    /// planned for, and separate a header that follows a new one by a blank
161    /// line.
162    pub(super) fn apply(&self, root: &mut Table) {
163        let mut index = 0usize;
164        Self::walk(root, 0, &mut |table, _| {
165            let slot = self.0.get(index);
166            let rank = slot.map_or(0, |slot| slot.rank);
167            table.set_position(isize::try_from(rank).ok());
168            if slot.is_some_and(|slot| slot.blank_before) {
169                let prefix = prefix_str(table.decor())
170                    .filter(|prefix| !prefix.starts_with(['\n', '\r']))
171                    .map(|prefix| format!("\n{prefix}"));
172                if let Some(prefix) = prefix {
173                    table.decor_mut().set_prefix(prefix);
174                }
175            }
176            index = index.saturating_add(1);
177        });
178    }
179
180    /// Drop the positions of every table in `item`. Positions that come from
181    /// another document mean nothing in this one.
182    pub(super) fn forget(item: &mut Item) {
183        match item {
184            Item::Table(table) => {
185                table.set_position(None);
186                for (_, child) in table.iter_mut() {
187                    Self::forget(child);
188                }
189            }
190            Item::ArrayOfTables(tables) => {
191                for table in tables.iter_mut() {
192                    table.set_position(None);
193                    for (_, child) in table.iter_mut() {
194                        Self::forget(child);
195                    }
196                }
197            }
198            Item::None | Item::Value(_) => {}
199        }
200    }
201
202    /// Visit `table` and every table below it, parents first. Dotted tables
203    /// have no header and no position, so they are only descended into.
204    fn walk(table: &mut Table, depth: usize, visit: &mut impl FnMut(&mut Table, usize)) {
205        let depth = if table.is_dotted() {
206            depth
207        } else {
208            visit(table, depth);
209            depth.saturating_add(1)
210        };
211        for (_, item) in table.iter_mut() {
212            match item {
213                Item::Table(child) => Self::walk(child, depth, visit),
214                Item::ArrayOfTables(children) => {
215                    for child in children.iter_mut() {
216                        Self::walk(child, depth, visit);
217                    }
218                }
219                Item::None | Item::Value(_) => {}
220            }
221        }
222    }
223}