Skip to main content

yaml_edit/nodes/mapping/
movements.rs

1//! Moving entries around a mapping: `Mapping::reorder_fields`,
2//! `move_after` and `move_before`.
3
4use super::Mapping;
5use crate::lex::SyntaxKind;
6use crate::nodes::trivia::Slot;
7use crate::nodes::trivia::{self, ends_with_line_break, FlowSeparator};
8use crate::nodes::{fresh_token, Lang, SyntaxNode};
9use rowan::ast::AstNode;
10
11type Element = rowan::SyntaxElement<Lang>;
12
13/// An entry being moved, with what goes along with it.
14struct Group {
15    /// The comment lines before it, with any blank lines among and after
16    /// them.
17    lines_before: Vec<Element>,
18    entry: SyntaxNode,
19}
20
21/// The layout that belongs to a position in a mapping rather than to the
22/// entry at it, so that it stays put when entries are moved around.
23struct SlotLayout {
24    /// For each entry of a flow mapping, the separator it ends with, if
25    /// that can be swapped for another entry's.
26    flow_separators: Vec<Option<FlowSeparator>>,
27    /// Whether the last entry of a flow mapping has a trailing comma.
28    last_has_comma: bool,
29    /// Whether the last entry of a block mapping ends its line.
30    last_terminated: bool,
31}
32
33impl Mapping {
34    /// Put the entries of this mapping in the order of `order`, which holds
35    /// each of them once. An entry takes along the rest of its line and, in
36    /// a block mapping, the comment lines before it back to the previous
37    /// entry or the blank lines after that; the layout that belongs to
38    /// positions, including those blank lines, stays put.
39    pub(super) fn arrange_entries(&self, order: &[SyntaxNode]) {
40        let all: Vec<Element> = self.0.children_with_tokens().collect();
41        let starts: Vec<usize> = all
42            .iter()
43            .enumerate()
44            .filter(|(_, c)| c.kind() == SyntaxKind::MAPPING_ENTRY)
45            .map(|(i, _)| i)
46            .collect();
47        assert_eq!(
48            starts.len(),
49            order.len(),
50            "the order names every entry once"
51        );
52        let Some(&first) = starts.first() else {
53            return;
54        };
55        let layout = self.slot_layout();
56        let flow = self.is_flow_style();
57        // A flow mapping's first entry has the indentation of its line
58        // inside the mapping, as the others do.
59        let mut prefix = all[..first].to_vec();
60        let first_indent = prefix
61            .last()
62            .filter(|c| flow && c.kind() == SyntaxKind::INDENT)
63            .cloned();
64        if first_indent.is_some() {
65            prefix.pop();
66        }
67
68        // What moves with each entry, and for each position the blank lines
69        // after it and the INDENT starting the next entry's line.
70        let mut groups: Vec<Group> = Vec::new();
71        let mut gaps: Vec<Vec<Element>> = Vec::new();
72        let mut line_indents = Vec::new();
73        let mut trailing = Vec::new();
74        let mut lines_before = Vec::new();
75        for (i, &at) in starts.iter().enumerate() {
76            let entry = all[at].as_node().expect("an entry is a node");
77            groups.push(Group {
78                lines_before: std::mem::take(&mut lines_before),
79                entry: entry.clone(),
80            });
81            let Some(&next) = starts.get(i + 1) else {
82                trailing = all[at + 1..].to_vec();
83                let gap_end = leading_blank_lines_end(&trailing);
84                gaps.push(trailing.drain(..gap_end).collect());
85                break;
86            };
87            let mut between = all[at + 1..next].to_vec();
88            let indent = between
89                .last()
90                .is_some_and(|c| c.kind() == SyntaxKind::INDENT)
91                .then(|| between.pop())
92                .flatten();
93            line_indents.push(indent);
94            lines_before = between.split_off(leading_blank_lines_end(&between));
95            gaps.push(between);
96        }
97
98        let ordered: Vec<Group> = order
99            .iter()
100            .map(|wanted| {
101                let pos = groups
102                    .iter()
103                    .position(|group| group.entry == *wanted)
104                    .expect("the order names entries of this mapping");
105                groups.remove(pos)
106            })
107            .collect();
108
109        // A block scalar would take the blank lines after it into its
110        // value, so they go after the next entry that can have them
111        // instead, or failing that the one before.
112        let ends_in_block_scalar: Vec<bool> = ordered
113            .iter()
114            .map(|group| crate::nodes::ending_block_scalar(&group.entry).is_some())
115            .collect();
116        for i in 0..gaps.len() {
117            if !ends_in_block_scalar[i] || gaps[i].is_empty() {
118                continue;
119            }
120            let home = (i + 1..gaps.len())
121                .chain((0..i).rev())
122                .find(|&j| !ends_in_block_scalar[j])
123                .expect("blank lines follow an entry that does not end in a block scalar");
124            let moved = std::mem::take(&mut gaps[i]);
125            gaps[home].extend(moved);
126        }
127
128        // Comment lines now before the first entry go where the parser
129        // puts them, ahead of the mapping.
130        let lines_lead = !ordered[0].lines_before.is_empty();
131        // Comment lines now first in a flow mapping may follow the `{` on
132        // its line.
133        let opening_line_open = flow
134            && prefix
135                .last()
136                .is_some_and(|c| c.kind() != SyntaxKind::NEWLINE);
137        let mut new_children = prefix;
138        let last_gap = gaps.pop().expect("there is an entry");
139        for (i, mut group) in ordered.into_iter().enumerate() {
140            let indent = match i.checked_sub(1) {
141                Some(i) => {
142                    new_children.append(&mut gaps[i]);
143                    line_indents[i].clone()
144                }
145                None if first_indent.is_some() => first_indent.clone(),
146                // The first entry's line is indented outside a block
147                // mapping, unless lines go before it.
148                None if lines_lead => line_indents[0]
149                    .as_ref()
150                    .and_then(|c| c.as_token())
151                    .map(|t| fresh_token(t.kind(), t.text()).into()),
152                None => None,
153            };
154            if i == 0 && opening_line_open {
155                if let Some(first) = group
156                    .lines_before
157                    .first_mut()
158                    .filter(|c| c.kind() == SyntaxKind::INDENT)
159                {
160                    *first = fresh_token(SyntaxKind::WHITESPACE, &first.to_string()).into();
161                }
162            }
163            new_children.extend(group.lines_before);
164            new_children.extend(indent);
165            new_children.push(group.entry.into());
166        }
167        new_children.extend(last_gap);
168        new_children.extend(trailing);
169
170        crate::nodes::kept_aliases::keeping_aliases_moved(&self.0, || {
171            crate::nodes::replace_children(&self.0, 0..all.len(), new_children);
172            if lines_lead && !flow {
173                crate::yaml::move_leading_lines_out(&self.0);
174            }
175            self.restore_slot_layout(&layout);
176            for entry in order {
177                crate::nodes::replaced_value::settle_lines_after_block_scalar(entry);
178            }
179        });
180    }
181
182    /// Take note of the layout of this mapping that belongs to positions
183    /// rather than entries, for [`restore_slot_layout`](Self::restore_slot_layout).
184    fn slot_layout(&self) -> SlotLayout {
185        let entries: Vec<SyntaxNode> = self.entries().map(|e| e.0).collect();
186        let flow_separators = if self.is_flow_style() {
187            entries.iter().map(FlowSeparator::of).collect()
188        } else {
189            Vec::new()
190        };
191        let last = entries.last();
192        SlotLayout {
193            flow_separators,
194            last_has_comma: last.is_some_and(trivia::has_comma),
195            last_terminated: last.map_or(true, ends_with_line_break),
196        }
197    }
198
199    /// Give each position of this mapping, whose entries have been moved
200    /// around since `layout` was taken, the layout it had then.
201    fn restore_slot_layout(&self, layout: &SlotLayout) {
202        let entries: Vec<SyntaxNode> = self.entries().map(|e| e.0).collect();
203        let Some((last, rest)) = entries.split_last() else {
204            return;
205        };
206        if !self.is_flow_style() {
207            for entry in rest {
208                if !ends_with_line_break(entry) {
209                    trivia::terminate(entry);
210                }
211            }
212            if !layout.last_terminated {
213                trivia::unterminate_last(&self.0);
214            }
215            return;
216        }
217        assert_eq!(
218            entries.len(),
219            layout.flow_separators.len(),
220            "moving entries keeps their number"
221        );
222        for (entry, slot) in entries.iter().zip(&layout.flow_separators) {
223            let is_last = entry == last;
224            match slot {
225                Some(slot) if FlowSeparator::swap(entry, slot) => {}
226                _ if is_last && !layout.last_has_comma => FlowSeparator::strip(entry),
227                _ => FlowSeparator::insert_comma(entry),
228            }
229        }
230    }
231
232    /// Move-or-insert `new_key: new_value` next to the entry for `ref_key`:
233    /// after it if `after` is set, before it otherwise. Returns `false` if
234    /// `ref_key` is not present (nothing to anchor on).
235    pub(super) fn move_around(
236        &self,
237        ref_key: &impl crate::AsYaml,
238        new_key: &impl crate::AsYaml,
239        new_value: &impl crate::AsYaml,
240        after: bool,
241    ) -> bool {
242        let Some(target) = self.find_entry_by_key(ref_key) else {
243            return false;
244        };
245        if let Some(existing) = self.find_entry_by_key(new_key) {
246            // Given its own value, the entry moves as it is, comments and
247            // anchor included, as reorder_fields would move it. Given
248            // another, it is replaced first, its old value going as with
249            // remove, so that its key moves the same way.
250            let value = existing.value().children().next();
251            let own_value = value.is_some() && value == new_value.value_node();
252            if existing.syntax() == target.syntax() {
253                if !own_value {
254                    self.set(new_key, new_value);
255                }
256                return true;
257            }
258            let old = if own_value {
259                existing.0
260            } else {
261                self.replace_entry(&existing.0, new_key, new_value, false)
262            };
263            let mut order: Vec<SyntaxNode> =
264                self.entries().map(|e| e.0).filter(|e| *e != old).collect();
265            let at = order
266                .iter()
267                .position(|e| e == target.syntax())
268                .expect("the target is in its mapping");
269            order.insert(at + usize::from(after), old);
270            self.arrange_entries(&order);
271            return true;
272        }
273        let target = target.syntax().clone();
274        let slot = if after {
275            Slot::After(target)
276        } else {
277            Slot::Before(target)
278        };
279        self.insert_new(slot, new_key, new_value);
280        true
281    }
282
283    /// Move a key-value pair to immediately after an existing key.
284    ///
285    /// If `new_key` already exists in the mapping, that entry is rebuilt with the
286    /// new value, the old one going as with [`remove`](Self::remove), and moved
287    /// after `after_key` - so the key ends up at the requested position regardless
288    /// of where it was before. Passing the entry's current value, as
289    /// [`get`](Self::get) returns it, moves the entry as it is, comments and
290    /// anchor included. Either way the entry moves as with
291    /// [`reorder_fields`](Self::reorder_fields), which says what happens to aliases.
292    ///
293    /// If `after_key` is not found, returns `false` and leaves the mapping unchanged.
294    /// Returns `true` on success.
295    ///
296    /// Use [`insert_after`](Self::insert_after) if you want existing entries to be
297    /// updated in-place rather than moved.
298    pub fn move_after(
299        &self,
300        after_key: impl crate::AsYaml,
301        new_key: impl crate::AsYaml,
302        new_value: impl crate::AsYaml,
303    ) -> bool {
304        self.move_around(&after_key, &new_key, &new_value, true)
305    }
306
307    /// Move a key-value pair to immediately before an existing key.
308    ///
309    /// If `new_key` already exists in the mapping, that entry is moved before
310    /// `before_key`, with the new value, as with [`move_after`](Self::move_after).
311    ///
312    /// If `before_key` is not found, returns `false` and leaves the mapping unchanged.
313    /// Returns `true` on success.
314    ///
315    /// Use [`insert_before`](Self::insert_before) if you want existing entries to be
316    /// updated in-place rather than moved.
317    pub fn move_before(
318        &self,
319        before_key: impl crate::AsYaml,
320        new_key: impl crate::AsYaml,
321        new_value: impl crate::AsYaml,
322    ) -> bool {
323        self.move_around(&before_key, &new_key, &new_value, false)
324    }
325}
326
327fn is_blank(element: &Element) -> bool {
328    matches!(
329        element.kind(),
330        SyntaxKind::NEWLINE | SyntaxKind::WHITESPACE | SyntaxKind::INDENT
331    )
332}
333
334/// Where the blank lines that start `elements` end: after the last line
335/// break before anything else.
336fn leading_blank_lines_end(elements: &[Element]) -> usize {
337    let first = elements
338        .iter()
339        .position(|e| !is_blank(e))
340        .unwrap_or(elements.len());
341    elements[..first]
342        .iter()
343        .rposition(|e| e.kind() == SyntaxKind::NEWLINE)
344        .map_or(0, |i| i + 1)
345}