Skip to main content

yaml_edit/nodes/mapping/
mod.rs

1mod compare;
2mod entry;
3mod helpers;
4mod movements;
5
6use helpers::{
7    append_comma_space_to_entry, ensure_top_level_empty_renders_as_flow, index_after_entry_line,
8    FlowInsertPos,
9};
10
11use super::{
12    ensure_trailing_newline, entry_key, entry_line_terminated, entry_value, fresh_token,
13    has_child_token, trailing_newline_reachable, Lang, Sequence, SyntaxNode,
14};
15use crate::as_yaml::{AsYaml, YamlKind};
16use crate::lex::SyntaxKind;
17use crate::yaml::{
18    collapse_empty_child_collection_in_parent as collapse_empty_child_mapping_in_parent,
19    dump_cst_to_string, ends_with_newline, ValueNode,
20};
21use rowan::ast::AstNode;
22use rowan::GreenNodeBuilder;
23
24ast_node!(
25    MappingEntry,
26    MAPPING_ENTRY,
27    "A key-value pair in a YAML mapping"
28);
29
30ast_node!(Mapping, MAPPING, "A YAML mapping (key-value pairs)");
31
32impl Mapping {
33    /// Dump the CST (Concrete Syntax Tree) structure to a human-readable string.
34    ///
35    /// This is intended for debugging and testing. The output shows the full
36    /// node hierarchy with indentation.
37    pub fn dump_cst(&self) -> String {
38        dump_cst_to_string(&self.0, 0)
39    }
40
41    /// Iterate over all keys in this mapping as [`YamlNode`](crate::as_yaml::YamlNode)s.
42    ///
43    /// Each key is returned as a [`YamlNode`](crate::as_yaml::YamlNode) wrapping
44    /// the underlying CST node, preserving quoting style and other formatting.
45    /// The nodes implement [`AsYaml`](crate::AsYaml), so they can be passed
46    /// back to [`get`](Self::get), [`contains_key`](Self::contains_key), etc.,
47    /// and compared semantically with [`yaml_eq`](crate::yaml_eq).
48    ///
49    /// Prefer [`entries`](Self::entries) when you also need the values, or
50    /// [`iter`](Self::iter) for `(key, value)` pairs as `(YamlNode, YamlNode)`.
51    /// For raw CST nodes, use `pairs()`.
52    pub fn keys(&self) -> impl Iterator<Item = crate::as_yaml::YamlNode> + '_ {
53        self.pairs().filter_map(|(k, _)| {
54            k.children()
55                .next()
56                .and_then(crate::as_yaml::YamlNode::from_syntax)
57        })
58    }
59
60    /// Iterate over raw KEY/VALUE syntax nodes for each mapping entry.
61    ///
62    /// Each item is `(key_node, value_node)`, both raw CST wrapper nodes
63    /// (`KEY`/`VALUE`), not the content nodes inside them. Entries with a
64    /// missing key or value (which indicate a parse error in the source) are
65    /// silently skipped.
66    ///
67    /// For most use cases prefer [`iter`](Self::iter) (which yields
68    /// `(YamlNode, YamlNode)` pairs) or [`entries`](Self::entries) (which
69    /// yields typed [`MappingEntry`] handles that give access to the full
70    /// entry including key, value, and mutation methods).
71    pub(crate) fn pairs(&self) -> impl Iterator<Item = (SyntaxNode, SyntaxNode)> + '_ {
72        self.0
73            .children()
74            .filter(|n| n.kind() == SyntaxKind::MAPPING_ENTRY)
75            .filter_map(|entry| Some((entry_key(&entry)?, entry_value(&entry)?)))
76    }
77
78    /// Get the value associated with `key` as a [`YamlNode`](crate::as_yaml::YamlNode).
79    ///
80    /// Returns `None` if the key does not exist.
81    ///
82    /// Matching is semantic but type-strict, so a key's type has to match as
83    /// well as its text: `1` does not find the string key `"1"`, and the
84    /// null key of `: v` is found with
85    /// [`ScalarValue::null`](crate::ScalarValue::null) rather than `""`,
86    /// which finds the distinct empty-string key of `"": v`.
87    ///
88    /// ```rust
89    /// # use std::str::FromStr;
90    /// # use yaml_edit::{Document, ScalarValue};
91    /// let doc = Document::from_str(": a\n\"\": b\n").unwrap();
92    /// let mapping = doc.as_mapping().unwrap();
93    /// assert!(mapping.get(ScalarValue::null()).is_some());
94    /// assert!(mapping.get("").is_some());
95    /// ```
96    pub fn get(&self, key: impl crate::AsYaml) -> Option<crate::as_yaml::YamlNode> {
97        self.get_node(key)
98            .and_then(crate::as_yaml::YamlNode::from_syntax)
99    }
100
101    /// Get the raw content syntax node for `key` (for advanced CST access).
102    ///
103    /// Returns the content node inside the `VALUE` wrapper - i.e. the actual
104    /// `SCALAR`, `MAPPING`, or `SEQUENCE` node. Returns `None` if the key does
105    /// not exist. For most use cases prefer [`get`](Self::get).
106    pub(crate) fn get_node(&self, key: impl crate::AsYaml) -> Option<SyntaxNode> {
107        self.find_entry_by_key(key)
108            .and_then(|entry| entry.value())
109            .and_then(|value_node| {
110                // VALUE nodes wrap the actual content, return the content instead
111                value_node.children().next()
112            })
113    }
114
115    /// Get the value for `key` as a nested [`Mapping`].
116    ///
117    /// Returns `None` if the key does not exist or its value is not a mapping.
118    pub fn get_mapping(&self, key: impl crate::AsYaml) -> Option<Mapping> {
119        self.get(key).and_then(|n| n.as_mapping().cloned())
120    }
121
122    /// Modify a nested mapping in place by applying a closure to it.
123    ///
124    /// Returns `true` if `key` exists and its value is a mapping (and `f` was
125    /// called); returns `false` if the key is missing or its value is not a
126    /// mapping.
127    ///
128    /// Because [`Mapping`] uses interior mutability via rowan's `SyntaxNode`,
129    /// the closure receives a shared reference - mutations are still possible
130    /// through the node's `set`, `remove`, and other `&self` methods.
131    pub fn modify_mapping<F>(&self, key: impl crate::AsYaml, f: F) -> bool
132    where
133        F: FnOnce(&Mapping),
134    {
135        // The closure mutates through rowan's interior mutability, so the
136        // node found here is the one in the tree - nothing to splice back.
137        let nested = self.0.children().find_map(|node| {
138            if node.kind() != SyntaxKind::MAPPING_ENTRY {
139                return None;
140            }
141            let key_node = entry_key(&node)?;
142            if !key_content_matches(&key_node, &key) {
143                return None;
144            }
145            entry_value(&node)?
146                .children()
147                .find(|n| n.kind() == SyntaxKind::MAPPING)
148        });
149        let Some(mapping_node) = nested else {
150            return false;
151        };
152        f(&Mapping(mapping_node));
153        true
154    }
155
156    /// Get the value for `key` as a nested [`Sequence`].
157    ///
158    /// Returns `None` if the key does not exist or its value is not a sequence.
159    pub fn get_sequence(&self, key: impl crate::AsYaml) -> Option<Sequence> {
160        self.get(key).and_then(|n| n.as_sequence().cloned())
161    }
162
163    /// Returns `true` if this mapping contains an entry with the given key.
164    pub fn contains_key(&self, key: impl crate::AsYaml) -> bool {
165        self.find_entry_by_key(key).is_some()
166    }
167
168    /// Iterate over the raw `KEY` wrapper nodes for all entries.
169    ///
170    /// For most use cases prefer [`keys`](Self::keys) which yields
171    /// [`YamlNode`](crate::as_yaml::YamlNode) keys (formatting-preserving but
172    /// comparable via `yaml_eq`), or [`entries`](Self::entries) which yields
173    /// full [`MappingEntry`] handles.
174    pub(crate) fn key_nodes(&self) -> impl Iterator<Item = SyntaxNode> + '_ {
175        self.pairs().map(|(k, _)| k)
176    }
177
178    /// Check if the mapping is empty
179    pub fn is_empty(&self) -> bool {
180        self.pairs().next().is_none()
181    }
182
183    /// Get the number of key-value pairs in this mapping
184    pub fn len(&self) -> usize {
185        self.pairs().count()
186    }
187
188    /// Iterate over the values in this mapping as [`YamlNode`](crate::as_yaml::YamlNode)s.
189    ///
190    /// Only entries whose value can be successfully wrapped in a `YamlNode` are
191    /// yielded; malformed or unrecognised value nodes are silently skipped.
192    /// Use [`iter`](Self::iter) to get both the key and value simultaneously, or
193    /// `pairs()` for the raw `SyntaxNode` pairs.
194    pub fn values(&self) -> impl Iterator<Item = crate::as_yaml::YamlNode> + '_ {
195        self.pairs().filter_map(|(_, value_node)| {
196            // VALUE node contains the actual content as children
197            value_node
198                .children()
199                .next()
200                .and_then(crate::as_yaml::YamlNode::from_syntax)
201        })
202    }
203
204    /// Iterate over `(key, value)` pairs, both as [`YamlNode`](crate::as_yaml::YamlNode)s.
205    ///
206    /// Entries that cannot be fully wrapped (malformed key or value nodes) are
207    /// silently skipped. For raw CST nodes, use `pairs()`; for
208    /// typed entry handles, prefer [`entries`](Self::entries).
209    pub fn iter(
210        &self,
211    ) -> impl Iterator<Item = (crate::as_yaml::YamlNode, crate::as_yaml::YamlNode)> + '_ {
212        self.pairs().filter_map(|(key_node, value_node)| {
213            // KEY and VALUE nodes wrap the actual content - extract children
214            let key = key_node
215                .children()
216                .next()
217                .and_then(crate::as_yaml::YamlNode::from_syntax)?;
218            let value = value_node
219                .children()
220                .next()
221                .and_then(crate::as_yaml::YamlNode::from_syntax)?;
222            Some((key, value))
223        })
224    }
225
226    /// Create a new empty mapping
227    pub fn new() -> Self {
228        Self::new_pending_block()
229    }
230
231    /// An empty mapping that renders as `{}` and stays flow when filled.
232    ///
233    /// Use this for a mapping that is meant to be flow-style in the output,
234    /// including one that is left empty.
235    pub fn new_flow() -> Self {
236        Self::empty_with_brace_text("{", "}")
237    }
238
239    /// An empty mapping that becomes a block mapping once an entry is added.
240    ///
241    /// YAML has no block syntax for an empty mapping: block collections are
242    /// written as their entries, so with none left there is nothing to write,
243    /// and a bare `key:` reads back as null. An empty mapping is therefore
244    /// `{}` whatever its eventual style, and block-vs-flow only becomes a
245    /// real choice once there is an entry.
246    ///
247    /// This renders as exactly `{}`, plus a zero-width token that marks it as
248    /// destined for block style. [`Mapping::set`] rewrites it into block form
249    /// when the first entry arrives; a `{}` that came from the source has no
250    /// such marker and keeps its flow style.
251    ///
252    /// The marker lives only in the tree. Serializing and re-parsing yields
253    /// an ordinary `{}`, which stays flow, since YAML has nowhere to record
254    /// the intent.
255    pub fn new_pending_block() -> Self {
256        let mut builder = GreenNodeBuilder::new();
257        builder.start_node(SyntaxKind::MAPPING.into());
258        builder.token(SyntaxKind::LEFT_BRACE.into(), "{");
259        // Zero-width sentinel: renders as nothing, so this is still exactly
260        // `{}` in the text, but marks the mapping as one that should become
261        // block when it gets its first entry.
262        builder.token(SyntaxKind::WHITESPACE.into(), "");
263        builder.token(SyntaxKind::RIGHT_BRACE.into(), "}");
264        builder.finish_node();
265        Mapping(SyntaxNode::new_root_mut(builder.finish()))
266    }
267
268    fn empty_with_brace_text(open: &str, close: &str) -> Self {
269        let mut builder = GreenNodeBuilder::new();
270        builder.start_node(SyntaxKind::MAPPING.into());
271        builder.token(SyntaxKind::LEFT_BRACE.into(), open);
272        builder.token(SyntaxKind::RIGHT_BRACE.into(), close);
273        builder.finish_node();
274        Mapping(SyntaxNode::new_root_mut(builder.finish()))
275    }
276
277    /// Is this an empty mapping awaiting its first entry? See
278    /// [`Mapping::new_pending_block`].
279    pub(crate) fn is_block_placeholder(&self) -> bool {
280        !self
281            .0
282            .children()
283            .any(|c| c.kind() == SyntaxKind::MAPPING_ENTRY)
284            && self
285                .0
286                .children_with_tokens()
287                .filter_map(|c| c.into_token())
288                .any(|t| t.kind() == SyntaxKind::WHITESPACE && t.text().is_empty())
289    }
290
291    /// Slice this mapping's children into leading decoration, one
292    /// `(entry, postscript)` group per entry, and trailing decoration.
293    ///
294    /// A group carries the tokens between its entry and the next, so
295    /// reshuffling groups keeps a between-entry comment glued to the entry it
296    /// visually followed.
297    #[allow(clippy::type_complexity)]
298    fn slice_into_entry_groups(
299        &self,
300        all: &[rowan::NodeOrToken<SyntaxNode, rowan::SyntaxToken<Lang>>],
301    ) -> (
302        Vec<rowan::NodeOrToken<SyntaxNode, rowan::SyntaxToken<Lang>>>,
303        Vec<(
304            SyntaxNode,
305            Vec<rowan::NodeOrToken<SyntaxNode, rowan::SyntaxToken<Lang>>>,
306        )>,
307        Vec<rowan::NodeOrToken<SyntaxNode, rowan::SyntaxToken<Lang>>>,
308    ) {
309        let entry_indices: Vec<usize> = all
310            .iter()
311            .enumerate()
312            .filter_map(|(i, c)| {
313                c.as_node()
314                    .filter(|n| n.kind() == SyntaxKind::MAPPING_ENTRY)
315                    .map(|_| i)
316            })
317            .collect();
318
319        let leading = all[..*entry_indices.first().unwrap_or(&all.len())].to_vec();
320        let mut trailing = entry_indices
321            .last()
322            .map(|&i| all[i + 1..].to_vec())
323            .unwrap_or_default();
324
325        // An explicit-key entry (`? k` / `: v`) doesn't own its line break;
326        // the NEWLINE sits beside it. For the last entry that token lands in
327        // `trailing`, which stays put -- so reordering it to a non-final
328        // position would run it into the entry that follows. Hand it back so
329        // it travels with the entry.
330        let last_needs_terminator = entry_indices.last().is_some_and(|&i| {
331            all[i]
332                .as_node()
333                .is_some_and(|n| !trailing_newline_reachable(n))
334        });
335        let last_entry_terminator = (last_needs_terminator
336            && trailing
337                .first()
338                .is_some_and(|c| c.kind() == SyntaxKind::NEWLINE))
339        .then(|| trailing.remove(0));
340
341        let mut groups: Vec<(SyntaxNode, Vec<_>)> = entry_indices
342            .windows(2)
343            .map(|w| {
344                let entry = all[w[0]].as_node().cloned().unwrap();
345                let postscript = all[w[0] + 1..w[1]].to_vec();
346                (entry, postscript)
347            })
348            .collect();
349        if let Some(&last) = entry_indices.last() {
350            let postscript = last_entry_terminator.into_iter().collect();
351            groups.push((all[last].as_node().cloned().unwrap(), postscript));
352        }
353        (leading, groups, trailing)
354    }
355
356    /// Reorder fields according to the specified order.
357    ///
358    /// Fields not in the order list will appear after the ordered fields,
359    /// in their original relative order.
360    pub fn reorder_fields<I, K>(&self, order: I)
361    where
362        I: IntoIterator<Item = K>,
363        K: crate::AsYaml,
364    {
365        let order_keys: Vec<K> = order.into_iter().collect();
366
367        // Slice the children into leading decoration, (entry, tokens
368        // that follow it up to the next entry), and trailing
369        // decoration. Reshuffling (entry, postscript) pairs keeps a
370        // between-entry comment glued to the entry it visually
371        // followed.
372        let all: Vec<_> = self.0.children_with_tokens().collect();
373        let (leading, mut groups, trailing) = self.slice_into_entry_groups(&all);
374
375        // Take each named key's group in the order given; anything the
376        // caller did not name keeps its relative position at the end.
377        let entry_has_key = |entry: &SyntaxNode, key: &K| {
378            entry
379                .children()
380                .find(|n| n.kind() == SyntaxKind::KEY)
381                .is_some_and(|k| key_content_matches(&k, key))
382        };
383        let mut ordered: Vec<_> = order_keys
384            .iter()
385            .filter_map(|key| {
386                let pos = groups
387                    .iter()
388                    .position(|(entry, _)| entry_has_key(entry, key))?;
389                Some(groups.remove(pos))
390            })
391            .collect();
392        ordered.extend(groups);
393
394        let mut new_children = leading;
395        for (entry, postscript) in ordered {
396            new_children.push(entry.into());
397            new_children.extend(postscript);
398        }
399        new_children.extend(trailing);
400
401        self.0.splice_children(0..all.len(), new_children);
402    }
403}
404
405/// Check whether the CST key node matches `key` using semantic equality.
406///
407/// `key_node` may be a raw KEY wrapper; `from_syntax_peeled` unwraps it.
408pub(super) fn key_content_matches(key_node: &SyntaxNode, key: impl crate::AsYaml) -> bool {
409    match crate::as_yaml::YamlNode::from_syntax_peeled(key_node.clone()) {
410        Some(node) => crate::as_yaml::yaml_eq(&node, &key),
411        None => false,
412    }
413}
414
415impl Mapping {
416    /// Check if this mapping is in flow style (JSON/inline format with `{}`).
417    ///
418    /// Returns `true` if the mapping uses flow style (e.g., `{key: value}`),
419    /// `false` if it uses block style (e.g., `key: value`).
420    pub fn is_flow_style(&self) -> bool {
421        // Flow-style mappings start with LEFT_BRACE token. A block
422        // placeholder is `{}` in the text but is destined to become block, so
423        // it does not count as flow.
424        has_child_token(&self.0, |k| k == SyntaxKind::LEFT_BRACE) && !self.is_block_placeholder()
425    }
426
427    /// Find the [`MappingEntry`] whose key matches `key`, or `None` if not found.
428    ///
429    /// Matching is semantic (quoting style is ignored), so `"foo"`, `'foo'`,
430    /// and `foo` all match the scalar `"foo"`.
431    pub fn find_entry_by_key(&self, key: impl crate::AsYaml) -> Option<MappingEntry> {
432        self.0
433            .children()
434            .filter_map(MappingEntry::cast)
435            .find(|entry| entry.key().is_some_and(|k| key_content_matches(&k, &key)))
436    }
437
438    /// Find all entries with a given key.
439    ///
440    /// Returns an iterator over all [`MappingEntry`] instances that match the
441    /// given key. This is useful for handling duplicate keys in YAML (which
442    /// are allowed by the spec but semantically ambiguous).
443    ///
444    /// Matching is semantic (quoting style is ignored), so `"foo"`, `'foo'`,
445    /// and `foo` all match the scalar `"foo"`.
446    ///
447    /// # Example
448    ///
449    /// ```rust
450    /// # use std::str::FromStr;
451    /// # use yaml_edit::Document;
452    /// let yaml = r#"
453    /// Reference: First
454    /// Reference: Second
455    /// Reference: Third
456    /// "#;
457    ///
458    /// let doc = Document::from_str(yaml).unwrap();
459    /// let mapping = doc.as_mapping().unwrap();
460    ///
461    /// // Collect all Reference entries
462    /// let refs: Vec<_> = mapping.find_all_entries_by_key("Reference").collect();
463    /// assert_eq!(refs.len(), 3);
464    ///
465    /// // Remove all but the first occurrence
466    /// let _: Vec<()> = refs.into_iter().skip(1).map(|entry| entry.remove()).collect();
467    /// ```
468    pub fn find_all_entries_by_key<'a>(
469        &'a self,
470        key: impl crate::AsYaml + 'a,
471    ) -> impl Iterator<Item = MappingEntry> + 'a {
472        self.0
473            .children()
474            .filter_map(MappingEntry::cast)
475            .filter(move |entry| entry.key().is_some_and(|k| key_content_matches(&k, &key)))
476    }
477
478    /// Iterate over all entries in this mapping as typed [`MappingEntry`] handles.
479    ///
480    /// Each [`MappingEntry`] gives access to the key, value, and mutation
481    /// methods for that entry. For decoded `(key, value)` pairs, prefer
482    /// [`iter`](Self::iter); for raw CST nodes, use `pairs()`.
483    pub fn entries(&self) -> impl Iterator<Item = MappingEntry> {
484        self.0.children().filter_map(MappingEntry::cast)
485    }
486
487    /// Return an [`Entry`](crate::Entry) view into the entry for `key`.
488    ///
489    /// Mirrors [`std::collections::BTreeMap::entry`], enabling
490    /// insert-or-update patterns while preserving formatting.
491    ///
492    /// ```rust
493    /// use std::str::FromStr;
494    /// use yaml_edit::Document;
495    ///
496    /// let doc = Document::from_str("name: Alice\n").unwrap();
497    /// let mapping = doc.as_mapping().unwrap();
498    ///
499    /// mapping.entry("age").or_insert(30);
500    /// mapping.entry("name").or_insert("Bob");
501    ///
502    /// assert_eq!(doc.to_string(), "name: Alice\nage: 30\n");
503    /// ```
504    pub fn entry<K: crate::AsYaml>(&self, key: K) -> crate::Entry<'_, K> {
505        match self.find_entry_by_key(&key) {
506            Some(entry) => crate::Entry::Occupied(crate::OccupiedEntry {
507                mapping: self,
508                entry,
509            }),
510            None => crate::Entry::Vacant(crate::VacantEntry { mapping: self, key }),
511        }
512    }
513
514    /// Find the child index of a mapping entry by its key.
515    ///
516    /// Returns the index within the mapping's children (including non-entry
517    /// tokens like whitespace), or `None` if no entry with the given key exists.
518    /// This index is suitable for use with `splice_children`.
519    pub fn find_entry_index_by_key(&self, key: impl crate::AsYaml) -> Option<usize> {
520        // Look through all children (not just entries, to get accurate index)
521        self.0
522            .children_with_tokens()
523            .enumerate()
524            .find_map(|(i, child)| {
525                let node = child.as_node()?;
526                if node.kind() != SyntaxKind::MAPPING_ENTRY {
527                    return None;
528                }
529                let entry = MappingEntry::cast(node.clone())?;
530                if entry.key().is_some_and(|k| key_content_matches(&k, &key)) {
531                    Some(i)
532                } else {
533                    None
534                }
535            })
536    }
537
538    /// Set a key-value pair, replacing the existing value if the key exists or
539    /// appending a new entry if it does not. Accepts any value that implements
540    /// [`AsYaml`](crate::AsYaml) - scalars, mappings, sequences, etc.
541    ///
542    /// This method always succeeds; it never silently ignores input. See also
543    /// [`insert_after`](Self::insert_after) and [`insert_before`](Self::insert_before)
544    /// which return `bool` to indicate whether the anchor key was found.
545    ///
546    /// Mutates in place despite `&self` (see crate docs on interior mutability).
547    ///
548    /// `set` replaces the value with the node you pass. It does not infer
549    /// YAML 1.1 merge keys from a resolved map. To keep identity, pass an
550    /// alias node (`*name`) or a mapping that already contains `<<: *name`.
551    /// Merge **reads** (`get_resolved()`, `merged()`) stay separate from
552    /// this CST replace.
553    ///
554    /// ```
555    /// use std::str::FromStr;
556    /// use yaml_edit::Document;
557    ///
558    /// let doc = Document::from_str(
559    ///     "shared: &shared\n  timeout: 30\n  retries: 3\nservice_a: old\n",
560    /// )
561    /// .unwrap();
562    /// let mapping = doc.as_mapping().unwrap();
563    /// let merge = Document::from_str("<<: *shared\ntimeout: 60\n").unwrap();
564    /// mapping.set("service_a", merge.as_mapping().unwrap());
565    /// assert_eq!(
566    ///     doc.to_string(),
567    ///     "shared: &shared\n  timeout: 30\n  retries: 3\nservice_a:\n  <<: *shared\n  timeout: 60\n"
568    /// );
569    /// ```
570    pub fn set(&self, key: impl crate::AsYaml, value: impl crate::AsYaml) {
571        self.set_as_yaml(key, value);
572    }
573
574    /// Detect if this mapping uses explicit key indicators (?)
575    pub(super) fn uses_explicit_keys(&self) -> bool {
576        // Check if any existing entries use explicit key format
577        // The QUESTION token is a child of MAPPING_ENTRY (sibling to KEY), not inside KEY
578        for child in self.0.children() {
579            if child.kind() == SyntaxKind::MAPPING_ENTRY {
580                // Check if this entry has a QUESTION token as a child
581                if has_child_token(&child, |k| k == SyntaxKind::QUESTION) {
582                    return true;
583                }
584            }
585        }
586        false
587    }
588
589    /// Internal unified method to set any YAML value type
590    fn set_as_yaml<K: crate::AsYaml, V: crate::AsYaml>(&self, key: K, value: V) {
591        // A placeholder from `Mapping::new_pending_block` turns into a real block
592        // mapping now that it has an entry to hold. This runs before the
593        // flow_context check below so the entry is laid out as block.
594        if self.is_block_placeholder() && !crate::nodes::sequence::must_render_flow(&self.0) {
595            crate::yaml::convert_placeholder_to_block(&self.0);
596        }
597
598        // Detect if this mapping is in flow style (JSON format)
599        let flow_context = self.is_flow_style();
600
601        // Detect if existing entries use explicit keys
602        let use_explicit_keys = self.uses_explicit_keys();
603
604        // An existing entry with this key takes the value in place.
605        let existing = self
606            .0
607            .children_with_tokens()
608            .enumerate()
609            .find_map(|(i, c)| {
610                let entry = MappingEntry::cast(c.into_node()?)?;
611                let matches = entry.key().is_some_and(|k| key_content_matches(&k, &key));
612                matches.then_some((i, entry))
613            });
614        if let Some((i, entry)) = existing {
615            entry.set_value(value, flow_context);
616            self.0.splice_children(i..i + 1, vec![entry.0.into()]);
617            return;
618        }
619
620        // Entry doesn't exist, create a new one. Tell the constructor at
621        // what column its key will sit so a node-backed block value has its
622        // interior indentation shifted to line up under the new entry.
623        let key_indent = self.detect_indentation_level();
624        let new_entry =
625            MappingEntry::new_at_indent(key, value, flow_context, use_explicit_keys, key_indent);
626        self.insert_entry_cst(&new_entry.0);
627    }
628
629    /// Insert `new_entry` into a flow-style mapping (`{...}`) at the position
630    /// dictated by `where_at`, wiring up `, ` separators so the entries stay
631    /// syntactically valid inside the braces.
632    pub(super) fn insert_flow_entry_cst_at(&self, new_entry: &SyntaxNode, where_at: FlowInsertPos) {
633        let brace_pos = self.0.children_with_tokens().position(|c| {
634            c.as_token()
635                .is_some_and(|t| t.kind() == SyntaxKind::RIGHT_BRACE)
636        });
637
638        // Resolve the target index. Anchor by node identity so a subsequent
639        // splice targets the right slot even if we've added separators.
640        let target_idx = match where_at {
641            FlowInsertPos::End => {
642                brace_pos.unwrap_or_else(|| self.0.children_with_tokens().count())
643            }
644            FlowInsertPos::After(ref n) => self
645                .0
646                .children_with_tokens()
647                .position(|c| c.as_node() == Some(n))
648                .map_or_else(|| brace_pos.unwrap_or(0), |i| i + 1),
649            FlowInsertPos::Before(ref n) => self
650                .0
651                .children_with_tokens()
652                .position(|c| c.as_node() == Some(n))
653                .unwrap_or_else(|| brace_pos.unwrap_or(0)),
654        };
655
656        // If a MAPPING_ENTRY precedes the target position, append `, ` to
657        // it so the two entries are properly delimited.
658        let prev_entry = self
659            .0
660            .children_with_tokens()
661            .take(target_idx)
662            .filter_map(|c| c.into_node())
663            .filter(|n| n.kind() == SyntaxKind::MAPPING_ENTRY)
664            .last();
665        if let Some(prev) = prev_entry {
666            append_comma_space_to_entry(&prev);
667        }
668        // If a MAPPING_ENTRY follows the target position, append `, ` to
669        // the *new* entry so it separates from the next one.
670        let has_following_entry = self.0.children_with_tokens().skip(target_idx).any(|c| {
671            c.as_node()
672                .is_some_and(|n| n.kind() == SyntaxKind::MAPPING_ENTRY)
673        });
674        if has_following_entry {
675            append_comma_space_to_entry(new_entry);
676        }
677
678        self.0
679            .splice_children(target_idx..target_idx, vec![new_entry.clone().into()]);
680    }
681
682    /// Internal method to insert a new entry at the end (does not check for duplicates)
683    fn insert_entry_cst(&self, new_entry: &SyntaxNode) {
684        if self.is_flow_style() {
685            self.insert_flow_entry_cst_at(new_entry, FlowInsertPos::End);
686            return;
687        }
688
689        // Count children and check if last entry has trailing newline
690        let mut count = 0;
691        let mut last_mapping_entry: Option<SyntaxNode> = None;
692
693        for child in self.0.children_with_tokens() {
694            count += 1;
695            if let Some(node) = child
696                .as_node()
697                .filter(|n| n.kind() == SyntaxKind::MAPPING_ENTRY)
698            {
699                last_mapping_entry = Some(node.clone());
700            }
701        }
702
703        // Does the last entry end with a NEWLINE leaf? Checking
704        // last_token().kind() == NEWLINE alone misses the case where
705        // the tail token is a zero-width implicit-null (e.g. `? b\n`
706        // under a tagged mapping) but a real NEWLINE sits just before
707        // it in the token stream. Walk the token tail instead.
708        let has_trailing_newline = if let Some(entry) = &last_mapping_entry {
709            entry_line_terminated(entry)
710        } else {
711            trailing_newline_reachable(&self.0)
712        };
713
714        let mut new_elements = Vec::new();
715        let insert_pos = count;
716
717        // Give the previous entry its own trailing NEWLINE (see
718        // insert_at_index for why standalone separators are fragile).
719        if count > 0 && !has_trailing_newline {
720            if let Some(prev_entry) = &last_mapping_entry {
721                ensure_trailing_newline(prev_entry);
722            }
723        }
724
725        let indent_level = self.detect_indentation_level();
726        if indent_level > 0 && count > 0 {
727            new_elements.push(fresh_token(SyntaxKind::INDENT, &" ".repeat(indent_level)).into());
728        }
729
730        new_elements.push(new_entry.clone().into());
731
732        // Note: We don't add a trailing newline here because MappingEntry::new()
733        // already adds one as part of the newline ownership model (entries own their trailing newlines)
734
735        self.0.splice_children(insert_pos..insert_pos, new_elements);
736
737        // If the mapping had been empty and sits inside a `key:\n    \n`
738        // placeholder, strip the outer MAPPING_ENTRY's trailing NEWLINE
739        // that acted as the placeholder marker (see issue #18).
740        if count == 0 {
741            crate::yaml::detach_empty_collection_placeholder_newline(&self.0, new_entry);
742        }
743    }
744
745    /// Set a key-value pair with field ordering support
746    /// If the key exists, updates its value. If the key doesn't exist, inserts it
747    /// at the correct position based on the provided field order.
748    /// Fields not in the order list are placed at the end.
749    pub fn set_with_field_order<I, K>(
750        &self,
751        key: impl crate::AsYaml,
752        value: impl crate::AsYaml,
753        field_order: I,
754    ) where
755        I: IntoIterator<Item = K>,
756        K: crate::AsYaml,
757    {
758        // Collect field_order so we can iterate it multiple times.
759        let field_order: Vec<K> = field_order.into_iter().collect();
760
761        // An existing key just takes the new value, wherever it sits.
762        if self.find_entry_by_key(&key).is_some() {
763            self.set_as_yaml(&key, &value);
764            return;
765        }
766
767        let children: Vec<_> = self.0.children_with_tokens().collect();
768
769        // Key doesn't exist, need to find the correct insertion position based on field order.
770        // Find position of this key in the field order (if it matches any)
771        let key_position_in_order = field_order
772            .iter()
773            .position(|field| crate::as_yaml::yaml_eq(&key, field));
774
775        if let Some(key_index) = key_position_in_order {
776            // Key is in the field order, find the right position to insert
777            let mut insert_after_node: Option<SyntaxNode> = None;
778            let mut insert_before_node: Option<SyntaxNode> = None;
779
780            // The nearest preceding field that is actually present decides
781            // where the new entry goes.
782            for field in field_order.iter().take(key_index).rev() {
783                if let Some(entry) = self.find_entry_by_key(field) {
784                    insert_after_node = Some(entry.0);
785                    break;
786                }
787            }
788
789            // If no predecessor found, look for the first existing key in document order
790            // that comes after this one in field_order
791            if insert_after_node.is_none() {
792                // Nothing precedes it, so go before the first present field
793                // that the order puts after it.
794                insert_before_node = self.entries().find_map(|entry| {
795                    let key_node = entry.key()?;
796                    let pos = field_order
797                        .iter()
798                        .position(|field| key_content_matches(&key_node, field))?;
799                    (pos > key_index).then_some(entry.0)
800                });
801            }
802
803            // Build the new entry with proper newline ownership
804            let flow_context = self.is_flow_style();
805            let use_explicit_keys = self.uses_explicit_keys();
806            let key_indent = self.detect_indentation_level();
807            let new_entry = MappingEntry::new_at_indent(
808                &key,
809                &value,
810                flow_context,
811                use_explicit_keys,
812                key_indent,
813            )
814            .0;
815
816            match (insert_after_node, insert_before_node) {
817                (Some(after), _) if flow_context => {
818                    self.insert_flow_entry_cst_at(&new_entry, FlowInsertPos::After(after));
819                }
820                (Some(after), _) => self.insert_block_entry_after(&children, &new_entry, &after),
821                (None, Some(before)) if flow_context => {
822                    self.insert_flow_entry_cst_at(&new_entry, FlowInsertPos::Before(before));
823                }
824                (None, Some(before)) => {
825                    self.insert_block_entry_before(&children, &new_entry, &before)
826                }
827                // No ordered key is present yet, so order says nothing about
828                // where this one goes.
829                (None, None) => self.set_as_yaml(&key, &value),
830            }
831        } else {
832            // Key is not in field order, append at the end using CST
833            self.set_as_yaml(&key, &value);
834        }
835    }
836
837    /// The INDENT token entries of this mapping are preceded by, if any.
838    fn entry_indent_token(&self) -> Option<rowan::SyntaxToken<Lang>> {
839        let level = self.detect_indentation_level();
840        (level > 0).then(|| super::fresh_token(SyntaxKind::INDENT, &" ".repeat(level)))
841    }
842
843    /// Splice `new_entry` into this block mapping on the line after `after`.
844    fn insert_block_entry_after(
845        &self,
846        children: &[rowan::NodeOrToken<SyntaxNode, rowan::SyntaxToken<Lang>>],
847        new_entry: &SyntaxNode,
848        after: &SyntaxNode,
849    ) {
850        ensure_trailing_newline(after);
851        let idx = children
852            .iter()
853            .position(|c| c.as_node() == Some(after))
854            .expect("after was found in children earlier");
855        let at = index_after_entry_line(&self.0, idx);
856        let mut elements: Vec<rowan::NodeOrToken<SyntaxNode, rowan::SyntaxToken<Lang>>> = self
857            .entry_indent_token()
858            .into_iter()
859            .map(Into::into)
860            .collect();
861        elements.push(new_entry.clone().into());
862        self.0.splice_children(at..at, elements);
863    }
864
865    /// Splice `new_entry` into this block mapping ahead of `before`.
866    fn insert_block_entry_before(
867        &self,
868        children: &[rowan::NodeOrToken<SyntaxNode, rowan::SyntaxToken<Lang>>],
869        new_entry: &SyntaxNode,
870        before: &SyntaxNode,
871    ) {
872        let idx = children
873            .iter()
874            .position(|c| c.as_node() == Some(before))
875            .expect("before was found in children earlier");
876        if let Some(prev) = children[..idx].iter().rev().find_map(|c| {
877            c.as_node()
878                .filter(|n| n.kind() == SyntaxKind::MAPPING_ENTRY)
879        }) {
880            ensure_trailing_newline(prev);
881        }
882        // The indent preceding `before` (the parent VALUE's for the first
883        // child, a sibling INDENT token otherwise) now serves the new entry,
884        // so the displaced `before` needs its own sibling INDENT.
885        let mut elements: Vec<rowan::NodeOrToken<SyntaxNode, rowan::SyntaxToken<Lang>>> =
886            vec![new_entry.clone().into()];
887        elements.extend(self.entry_indent_token().map(Into::into));
888        self.0.splice_children(idx..idx, elements);
889    }
890
891    /// Detect the indentation level (in spaces) used by entries in this mapping.
892    ///
893    /// Returns `0` for root-level mappings where entries have no leading indentation.
894    pub fn detect_indentation_level(&self) -> usize {
895        // Look for an INDENT token that precedes an entry inside this mapping.
896        // For a multi-entry mapping the parser stores that column here. An
897        // INDENT introducing a COMMENT says nothing about where keys sit, so
898        // skip those: comments may be indented arbitrarily.
899        let children: Vec<_> = self.0.children_with_tokens().collect();
900        for (i, child) in children.iter().enumerate() {
901            if let Some(token) = child.as_token() {
902                if token.kind() == SyntaxKind::INDENT {
903                    let introduces_comment = children[i + 1..]
904                        .iter()
905                        .find(|c| !matches!(c.kind(), SyntaxKind::WHITESPACE))
906                        .is_some_and(|c| c.kind() == SyntaxKind::COMMENT);
907                    if !introduces_comment {
908                        return token.text().len();
909                    }
910                }
911            }
912        }
913        // Single-entry mapping (or empty): the column of the first entry
914        // isn't stored here, it's the INDENT that precedes the MAPPING node
915        // in its parent's VALUE. Fall back to that.
916        crate::as_yaml::source_base_indent(&self.0)
917    }
918
919    /// The child position of the entry whose key matches `entry`'s, if any.
920    ///
921    /// The position is in `children_with_tokens` space, which is what
922    /// `splice_children` indexes, so it is not the entry's ordinal.
923    fn child_index_of_matching_entry(&self, entry: &SyntaxNode) -> Option<usize> {
924        let key_node = MappingEntry::cast(entry.clone())?.key()?;
925        let existing = self.entries().find(|e| {
926            e.key()
927                .is_some_and(|k| self.compare_key_nodes(&k, &key_node))
928        })?;
929        self.0
930            .children_with_tokens()
931            .position(|c| c.as_node() == Some(existing.syntax()))
932    }
933
934    /// Insert a key-value pair at a specific index (0-based), preserving formatting.
935    ///
936    /// If `new_key` already exists in the mapping, the existing entry is replaced
937    /// with a newly built entry at the **same position** (the `index` argument is
938    /// ignored). Surrounding whitespace in the file is preserved, but the entry
939    /// node itself is rebuilt (comments attached to the old entry may be lost).
940    /// If `index` is out of bounds, the entry is appended at the end.
941    pub fn insert_at_index_preserving(
942        &self,
943        index: usize,
944        new_key: impl crate::AsYaml,
945        new_value: impl crate::AsYaml,
946    ) {
947        // Create the new entry using create_mapping_entry
948        let (new_entry, _has_trailing_newline) = self.create_mapping_entry(new_key, new_value);
949
950        // An existing entry with this key is replaced where it stands, so the
951        // index is only consulted for a key that is not there yet. New
952        // entries always end with a newline (DESIGN.md), so the old entry's
953        // own terminator goes with it.
954        if let Some(i) = self.child_index_of_matching_entry(&new_entry) {
955            self.0.splice_children(i..i + 1, vec![new_entry.into()]);
956            return;
957        }
958
959        // Key doesn't exist, insert at the specified index
960        let children: Vec<_> = self.0.children_with_tokens().collect();
961        // Count existing MAPPING_ENTRY nodes to find insertion point
962        let mut entry_indices = Vec::new();
963        for (i, child) in children.iter().enumerate() {
964            if child
965                .as_node()
966                .is_some_and(|n| n.kind() == SyntaxKind::MAPPING_ENTRY)
967            {
968                entry_indices.push(i);
969            }
970        }
971
972        let mut new_elements = Vec::new();
973
974        // Determine insertion position in children_with_tokens space
975        let insert_pos = if entry_indices.is_empty() {
976            // Empty mapping - insert at beginning
977            0
978        } else if index >= entry_indices.len() {
979            // Index beyond end - insert after last entry
980            let last_entry_idx = entry_indices[entry_indices.len() - 1];
981            // Find the end of the last entry (including its newline if present)
982            let mut pos = last_entry_idx + 1;
983            while pos < children.len() {
984                if let Some(token) = children[pos].as_token() {
985                    if token.kind() == SyntaxKind::NEWLINE {
986                        pos += 1;
987                        break;
988                    }
989                }
990                pos += 1;
991            }
992            pos
993        } else {
994            // Insert before the entry at the specified index
995            entry_indices[index]
996        };
997
998        // Add newline before entry if inserting after existing content
999        // Check if previous element ends with newline (either as token or inside node)
1000        if insert_pos > 0 && !entry_indices.is_empty() {
1001            let has_newline_before = if let Some(child) = children.get(insert_pos - 1) {
1002                match child {
1003                    rowan::NodeOrToken::Token(t) => t.kind() == SyntaxKind::NEWLINE,
1004                    rowan::NodeOrToken::Node(n) => ends_with_newline(n),
1005                }
1006            } else {
1007                false
1008            };
1009
1010            if !has_newline_before {
1011                new_elements.push(fresh_token(SyntaxKind::NEWLINE, "\n").into());
1012            }
1013        }
1014
1015        // Use the already-created MAPPING_ENTRY node (already ends with newline)
1016        new_elements.push(new_entry.into());
1017
1018        // Insert at the calculated position
1019        self.0.splice_children(insert_pos..insert_pos, new_elements);
1020    }
1021
1022    /// Remove a key-value pair, returning the removed entry.
1023    ///
1024    /// Returns `Some(entry)` if the key existed and was removed, or `None` if
1025    /// the key was not found. The returned [`MappingEntry`] is detached from
1026    /// the tree; callers can inspect its key and value or re-insert it
1027    /// elsewhere.
1028    ///
1029    /// Mutates in place despite `&self` (see crate docs on interior mutability).
1030    pub fn remove(&self, key: impl crate::AsYaml) -> Option<MappingEntry> {
1031        let children: Vec<_> = self.0.children_with_tokens().collect();
1032
1033        for (i, child) in children.iter().enumerate() {
1034            let Some(node) = child.as_node() else {
1035                continue;
1036            };
1037            if node.kind() != SyntaxKind::MAPPING_ENTRY {
1038                continue;
1039            }
1040            let Some(entry) = MappingEntry::cast(node.clone()) else {
1041                continue;
1042            };
1043            if !entry.key_matches(&key) {
1044                continue;
1045            }
1046            self.remove_entry_at(&children, i, node);
1047            return Some(entry);
1048        }
1049        None
1050    }
1051
1052    /// Splice the entry at child index `i` out of this mapping, and
1053    /// restore the tail-newline convention on the new last entry.
1054    ///
1055    /// Shared by [`Mapping::remove`] and [`Mapping::remove_nth_occurrence`].
1056    /// The rule is: if we're removing the last MAPPING_ENTRY and it had
1057    /// no trailing NEWLINE (an unterminated source), strip the trailing
1058    /// NEWLINE from the new last entry too so the doc's terminator
1059    /// character is preserved. Otherwise leave the previous entry's
1060    /// NEWLINE in place.
1061    fn remove_entry_at(
1062        &self,
1063        children: &[rowan::NodeOrToken<SyntaxNode, rowan::SyntaxToken<Lang>>],
1064        i: usize,
1065        removed: &SyntaxNode,
1066    ) {
1067        let is_last = !children.iter().skip(i + 1).any(|c| {
1068            c.as_node()
1069                .is_some_and(|n| n.kind() == SyntaxKind::MAPPING_ENTRY)
1070        });
1071        let removed_had_newline = removed
1072            .last_token()
1073            .is_some_and(|t| t.kind() == SyntaxKind::NEWLINE);
1074
1075        // In a block mapping every entry but the first is preceded by an
1076        // INDENT token that puts it at the right column; the first
1077        // entry's indent sits outside, on the parent VALUE. That token
1078        // belongs to the entry, so it goes with it -- otherwise it
1079        // orphans onto the following entry, over-indenting it, or is
1080        // left dangling as trailing whitespace when the entry was last.
1081        let indent_before = i.checked_sub(1).and_then(|p| {
1082            children[p]
1083                .as_token()
1084                .filter(|t| t.kind() == SyntaxKind::INDENT)
1085                .cloned()
1086        });
1087
1088        self.0.splice_children(i..(i + 1), vec![]);
1089        // Removing the first entry promotes the next one, but the parent
1090        // VALUE already supplies its indent, so the INDENT now leading
1091        // this mapping would double it. Drop whichever applies.
1092        let orphaned_indent = indent_before.or_else(|| {
1093            self.0
1094                .children_with_tokens()
1095                .next()
1096                .and_then(|c| c.into_token())
1097                .filter(|t| t.kind() == SyntaxKind::INDENT)
1098        });
1099        if let Some(indent) = orphaned_indent {
1100            indent.detach();
1101        }
1102
1103        // Nested mapping drained by remove: collapse to `key: {}` so
1104        // `set_path("a.b.c", v) + remove_path(...)` doesn't leave a
1105        // dangling indent scaffold. Top-level drain is left alone here
1106        // -- callers may add more entries and injecting `{}` would
1107        // poison subsequent block-style `set`s; `Mapping::clear`
1108        // handles the explicit-empty case.
1109        if !self
1110            .0
1111            .children()
1112            .any(|c| c.kind() == SyntaxKind::MAPPING_ENTRY)
1113        {
1114            collapse_empty_child_mapping_in_parent(&self.0);
1115        }
1116
1117        if !(is_last && i > 0 && !removed_had_newline) {
1118            return;
1119        }
1120        let Some(prev_entry_node) = children[..i].iter().rev().find_map(|c| {
1121            c.as_node()
1122                .filter(|n| n.kind() == SyntaxKind::MAPPING_ENTRY)
1123        }) else {
1124            return;
1125        };
1126        if !prev_entry_node
1127            .last_token()
1128            .is_some_and(|t| t.kind() == SyntaxKind::NEWLINE)
1129        {
1130            return;
1131        }
1132        let count = prev_entry_node.children_with_tokens().count();
1133        prev_entry_node.splice_children((count - 1)..count, vec![]);
1134    }
1135
1136    /// Remove the nth occurrence of a key, returning the removed entry.
1137    ///
1138    /// Returns `Some(entry)` if the nth occurrence exists and was removed,
1139    /// or `None` if there are fewer than `n+1` occurrences of the key.
1140    /// The index `n` is 0-based (n=0 removes the first occurrence, n=1 removes
1141    /// the second, etc.).
1142    ///
1143    /// This is useful for handling duplicate keys in YAML. While duplicate keys
1144    /// are semantically ambiguous, they are allowed by the YAML spec, and this
1145    /// method provides fine-grained control over which occurrence to remove.
1146    ///
1147    /// # Example
1148    ///
1149    /// ```rust
1150    /// # use std::str::FromStr;
1151    /// # use yaml_edit::Document;
1152    /// let yaml = r#"
1153    /// Reference: First
1154    /// Reference: Second
1155    /// Reference: Third
1156    /// "#;
1157    ///
1158    /// let doc = Document::from_str(yaml).unwrap();
1159    /// let mapping = doc.as_mapping().unwrap();
1160    ///
1161    /// // Remove the second occurrence (index 1)
1162    /// let removed = mapping.remove_nth_occurrence("Reference", 1);
1163    /// assert!(removed.is_some());
1164    ///
1165    /// // Now only two Reference entries remain
1166    /// let count = mapping.find_all_entries_by_key("Reference").count();
1167    /// assert_eq!(count, 2);
1168    /// ```
1169    ///
1170    /// Mutates in place despite `&self` (see crate docs on interior mutability).
1171    pub fn remove_nth_occurrence(&self, key: impl crate::AsYaml, n: usize) -> Option<MappingEntry> {
1172        let children: Vec<_> = self.0.children_with_tokens().collect();
1173
1174        let mut occurrence_count = 0;
1175        for (i, child) in children.iter().enumerate() {
1176            let Some(node) = child.as_node() else {
1177                continue;
1178            };
1179            if node.kind() != SyntaxKind::MAPPING_ENTRY {
1180                continue;
1181            }
1182            let Some(entry) = MappingEntry::cast(node.clone()) else {
1183                continue;
1184            };
1185            if !entry.key_matches(&key) {
1186                continue;
1187            }
1188            if occurrence_count != n {
1189                occurrence_count += 1;
1190                continue;
1191            }
1192            self.remove_entry_at(&children, i, node);
1193            return Some(entry);
1194        }
1195        None
1196    }
1197
1198    /// Remove all key-value pairs from this mapping.
1199    ///
1200    /// Mutates in place despite `&self` (see crate docs on interior mutability).
1201    pub fn clear(&self) {
1202        let keys: Vec<crate::as_yaml::YamlNode> = self.keys().collect();
1203        for key in keys {
1204            self.remove(key);
1205        }
1206        ensure_top_level_empty_renders_as_flow(&self.0);
1207    }
1208
1209    /// Rename a key while preserving its value and formatting.
1210    ///
1211    /// The new key is built using the same `AsYaml` infrastructure as other
1212    /// write methods, so quoting and escaping are handled automatically.
1213    /// Returns `true` if the key was found and renamed, `false` if `old_key`
1214    /// does not exist.
1215    ///
1216    /// Mutates in place despite `&self` (see crate docs on interior mutability).
1217    pub fn rename_key(&self, old_key: impl crate::AsYaml, new_key: impl crate::AsYaml) -> bool {
1218        let children: Vec<_> = self.0.children_with_tokens().collect();
1219
1220        for (i, child) in children.iter().enumerate() {
1221            let Some(node) = child
1222                .as_node()
1223                .filter(|n| n.kind() == SyntaxKind::MAPPING_ENTRY)
1224            else {
1225                continue;
1226            };
1227            let Some(key_node) = entry_key(node) else {
1228                continue;
1229            };
1230            if !key_content_matches(&key_node, &old_key) {
1231                continue;
1232            }
1233
1234            let mut builder = GreenNodeBuilder::new();
1235            builder.start_node(SyntaxKind::MAPPING_ENTRY.into());
1236
1237            for entry_child in node.children_with_tokens() {
1238                match entry_child {
1239                    rowan::NodeOrToken::Node(n) if n.kind() == SyntaxKind::KEY => {
1240                        Self::rebuild_key(&mut builder, &n, &new_key);
1241                    }
1242                    rowan::NodeOrToken::Node(n) => {
1243                        crate::yaml::copy_node_to_builder(&mut builder, &n);
1244                    }
1245                    rowan::NodeOrToken::Token(t) => {
1246                        builder.token(t.kind().into(), t.text());
1247                    }
1248                }
1249            }
1250
1251            builder.finish_node();
1252            let new_entry = SyntaxNode::new_root_mut(builder.finish());
1253            self.0.splice_children(i..i + 1, vec![new_entry.into()]);
1254            return true;
1255        }
1256        false
1257    }
1258
1259    /// Rebuild an entry's KEY node around `new_key`, keeping the layout
1260    /// tokens the old one carried.
1261    ///
1262    /// An explicit key (`? k\n  : v`) puts the `:` on its own line, and the
1263    /// parser keeps that line break and indent inside KEY. They describe the
1264    /// entry's layout rather than the key, so they carry over; dropped, the
1265    /// entry collapses onto one line and reparses as one node.
1266    fn rebuild_key(builder: &mut GreenNodeBuilder, old: &SyntaxNode, new_key: &impl crate::AsYaml) {
1267        builder.start_node(SyntaxKind::KEY.into());
1268        super::build_key_content(builder, new_key);
1269        for trailing in old
1270            .children_with_tokens()
1271            .skip_while(|c| !matches!(c.as_token().map(|t| t.kind()), Some(SyntaxKind::NEWLINE)))
1272            .filter_map(|c| c.into_token())
1273        {
1274            builder.token(trailing.kind().into(), trailing.text());
1275        }
1276        builder.finish_node();
1277    }
1278
1279    /// Helper to create a MAPPING_ENTRY node from key and value strings
1280    pub(super) fn create_mapping_entry(
1281        &self,
1282        key: impl crate::AsYaml,
1283        value: impl crate::AsYaml,
1284    ) -> (SyntaxNode, bool) {
1285        let mut builder = GreenNodeBuilder::new();
1286        builder.start_node(SyntaxKind::MAPPING_ENTRY.into());
1287
1288        // Add KEY node
1289        builder.start_node(SyntaxKind::KEY.into());
1290        super::build_key_content(&mut builder, &key);
1291        builder.finish_node(); // KEY
1292
1293        // Add colon
1294        builder.token(SyntaxKind::COLON.into(), ":");
1295
1296        // Check if value is inline - if not, add newline + indent before VALUE
1297        if !value.is_inline() {
1298            builder.token(SyntaxKind::NEWLINE.into(), "\n");
1299            builder.token(SyntaxKind::INDENT.into(), "  "); // 2-space indent
1300        } else {
1301            builder.token(SyntaxKind::WHITESPACE.into(), " ");
1302        }
1303
1304        // Add VALUE node
1305        builder.start_node(SyntaxKind::VALUE.into());
1306        let value_ends_with_newline = value.build_content(&mut builder, 2, false);
1307        builder.finish_node(); // VALUE
1308
1309        // Every block-style MAPPING_ENTRY ends with NEWLINE
1310        // Only add if the value content didn't already end with one
1311        let added_newline = if !value_ends_with_newline {
1312            builder.token(SyntaxKind::NEWLINE.into(), "\n");
1313            true
1314        } else {
1315            false
1316        };
1317
1318        builder.finish_node(); // MAPPING_ENTRY
1319        (
1320            SyntaxNode::new_root_mut(builder.finish()),
1321            added_newline || value_ends_with_newline,
1322        )
1323    }
1324
1325    /// Insert a key-value pair immediately after an existing key.
1326    ///
1327    /// If `key` already exists in the mapping, its value is updated in-place and
1328    /// it remains at its current position (it is **not** moved to after `after_key`).
1329    /// Returns `true` in both the update and the insert cases.
1330    /// Returns `false` only if `after_key` is not found.
1331    ///
1332    /// Use [`move_after`](Self::move_after) if you want
1333    /// an existing entry to be moved to the new position.
1334    ///
1335    /// Mutates in place despite `&self` (see crate docs on interior mutability).
1336    pub fn insert_after(
1337        &self,
1338        after_key: impl crate::AsYaml,
1339        key: impl crate::AsYaml,
1340        value: impl crate::AsYaml,
1341    ) -> bool {
1342        // Check if the new key already exists - if so, just update it
1343        if self.find_entry_by_key(&key).is_some() {
1344            self.set_as_yaml(&key, &value);
1345            return self.find_entry_by_key(&after_key).is_some();
1346        }
1347
1348        // Key doesn't exist yet - delegate to move_after, which already contains
1349        // the correct insertion logic (including newline handling for entries
1350        // that lack a trailing newline). The two methods differ only in what
1351        // they do when the key *already* exists.
1352        self.move_after(after_key, key, value)
1353    }
1354
1355    /// Insert a key-value pair immediately before an existing key.
1356    ///
1357    /// If `key` already exists in the mapping, its value is updated in-place and
1358    /// it remains at its current position (it is **not** moved to before `before_key`).
1359    /// Returns `true` in both the update and the insert cases.
1360    /// Returns `false` only if `before_key` is not found.
1361    ///
1362    /// Use [`move_before`](Self::move_before) if you want
1363    /// an existing entry to be moved to the new position.
1364    ///
1365    /// Mutates in place despite `&self` (see crate docs on interior mutability).
1366    pub fn insert_before(
1367        &self,
1368        before_key: impl crate::AsYaml,
1369        key: impl crate::AsYaml,
1370        value: impl crate::AsYaml,
1371    ) -> bool {
1372        // Key exists → update in-place (don't move).
1373        if self.find_entry_by_key(&key).is_some() {
1374            self.set_as_yaml(&key, &value);
1375            // Only return true if before_key also exists (contract: false when
1376            // reference key is not found).
1377            return self.find_entry_by_key(&before_key).is_some();
1378        }
1379
1380        // Key doesn't exist yet - delegate to move_before, which already contains
1381        // the correct insertion logic. The two methods differ only in what they do
1382        // when the key *already* exists.
1383        self.move_before(before_key, key, value)
1384    }
1385
1386    /// Insert a key-value pair at a specific index (0-based).
1387    ///
1388    /// If `key` already exists in the mapping, its value is updated in-place and
1389    /// it remains at its current position (the `index` argument is ignored).
1390    /// If `index` is out of bounds, the entry is appended at the end.
1391    /// This method always succeeds; it never returns an error.
1392    ///
1393    /// Mutates in place despite `&self` (see crate docs on interior mutability).
1394    pub fn insert_at_index(
1395        &self,
1396        index: usize,
1397        key: impl crate::AsYaml,
1398        value: impl crate::AsYaml,
1399    ) {
1400        // Check if the key already exists - if so, just update it
1401        if self.find_entry_by_key(&key).is_some() {
1402            self.set_as_yaml(&key, &value);
1403            return;
1404        }
1405
1406        // Create the new mapping entry
1407        let flow_context = self.is_flow_style();
1408        let use_explicit_keys = self.uses_explicit_keys();
1409        let key_indent = self.detect_indentation_level();
1410        let new_entry =
1411            MappingEntry::new_at_indent(&key, &value, flow_context, use_explicit_keys, key_indent);
1412
1413        // Flow needs bracket-aware placement (see insert_flow_entry_cst_at);
1414        // the block splice below would drop after `}`.
1415        if flow_context {
1416            let actual_index = index.min(self.entries().count());
1417            let where_at = self
1418                .entries()
1419                .nth(actual_index)
1420                .map_or(FlowInsertPos::End, |e| {
1421                    FlowInsertPos::Before(e.syntax().clone())
1422                });
1423            self.insert_flow_entry_cst_at(&new_entry.0, where_at);
1424            return;
1425        }
1426
1427        // Count existing entries to determine actual insertion position
1428        let entry_count = self.entries().count();
1429        let actual_index = index.min(entry_count);
1430
1431        // Find the position in children_with_tokens corresponding to the nth entry
1432        let mut entry_positions = Vec::new();
1433        for (i, child) in self.0.children_with_tokens().enumerate() {
1434            if child
1435                .as_node()
1436                .is_some_and(|n| n.kind() == SyntaxKind::MAPPING_ENTRY)
1437            {
1438                entry_positions.push(i);
1439            }
1440        }
1441
1442        // Determine where to insert
1443        let insert_pos = if actual_index < entry_positions.len() {
1444            entry_positions[actual_index]
1445        } else {
1446            self.0.children_with_tokens().count()
1447        };
1448
1449        let mut new_elements = Vec::new();
1450
1451        if insert_pos > 0 {
1452            // Give the previous entry its own trailing NEWLINE so
1453            // the "each block MAPPING_ENTRY owns its terminator"
1454            // invariant holds (see src/nodes/mod.rs); a standalone
1455            // NEWLINE between entries would render fine now but be
1456            // fragile against later reshuffles.
1457            // An INDENT directly before the insertion point belongs to the
1458            // entry being displaced, not to the line above; look past it for
1459            // whatever actually terminates the previous line.
1460            let preceding: Vec<_> = self
1461                .0
1462                .children_with_tokens()
1463                .take(insert_pos)
1464                .filter(|c| c.kind() != SyntaxKind::INDENT)
1465                .collect();
1466            if let Some(prev_node) = preceding.last() {
1467                if let rowan::NodeOrToken::Node(prev) = prev_node {
1468                    if prev.kind() == SyntaxKind::MAPPING_ENTRY {
1469                        ensure_trailing_newline(prev);
1470                    }
1471                } else if let rowan::NodeOrToken::Token(t) = prev_node {
1472                    if t.kind() != SyntaxKind::NEWLINE {
1473                        new_elements.push(fresh_token(SyntaxKind::NEWLINE, "\n").into());
1474                    }
1475                }
1476            }
1477        }
1478
1479        // Add the new entry
1480        new_elements.push(new_entry.0.into());
1481
1482        // Inserting before an existing entry hands the new entry the indent
1483        // that already precedes it, so the displaced entry needs a fresh one.
1484        // Appending at the end has no such donor: the indent goes in front.
1485        let displaces_entry = insert_pos < self.0.children_with_tokens().count();
1486        let indent_level = self.detect_indentation_level();
1487        if indent_level > 0 {
1488            let token = fresh_token(SyntaxKind::INDENT, &" ".repeat(indent_level));
1489            if displaces_entry {
1490                new_elements.push(token.into());
1491            } else {
1492                new_elements.insert(0, token.into());
1493            }
1494        }
1495
1496        // Insert at the calculated position
1497        self.0.splice_children(insert_pos..insert_pos, new_elements);
1498    }
1499
1500    crate::nodes::ast_node_spans!("mapping");
1501}
1502
1503impl Default for Mapping {
1504    fn default() -> Self {
1505        Self::new()
1506    }
1507}
1508
1509// Iterator trait implementations for Mapping
1510
1511impl<'a> IntoIterator for &'a Mapping {
1512    type Item = (crate::as_yaml::YamlNode, crate::as_yaml::YamlNode);
1513    type IntoIter =
1514        Box<dyn Iterator<Item = (crate::as_yaml::YamlNode, crate::as_yaml::YamlNode)> + 'a>;
1515
1516    fn into_iter(self) -> Self::IntoIter {
1517        Box::new(self.iter())
1518    }
1519}
1520
1521impl AsYaml for Mapping {
1522    fn as_node(&self) -> Option<&SyntaxNode> {
1523        Some(&self.0)
1524    }
1525
1526    fn kind(&self) -> YamlKind {
1527        YamlKind::Mapping
1528    }
1529
1530    fn build_content(
1531        &self,
1532        builder: &mut rowan::GreenNodeBuilder,
1533        indent: usize,
1534        _flow_context: bool,
1535    ) -> bool {
1536        builder.start_node(SyntaxKind::MAPPING.into());
1537        crate::as_yaml::copy_node_content_with_indent(builder, &self.0, indent);
1538        builder.finish_node();
1539        self.0
1540            .last_token()
1541            .is_some_and(|t| t.kind() == SyntaxKind::NEWLINE)
1542    }
1543
1544    fn is_inline(&self) -> bool {
1545        ValueNode::is_inline(self)
1546    }
1547}
1548
1549#[cfg(test)]
1550mod tests;