Skip to main content

yaml_edit/nodes/mapping/
mod.rs

1mod entry;
2mod helpers;
3mod movements;
4
5pub(crate) use helpers::build_block_value_node;
6pub(crate) use helpers::flow_form;
7pub(crate) use helpers::{
8    block_scalar_body_shift, drop_tagged_anchor, held_anchor_token, kept_anchor_token,
9    source_anchor_text,
10};
11
12use super::empty_collection::{self, render_empty};
13use super::trivia::{self, Slot};
14use super::{
15    entry_key, entry_value, fresh_token, has_child_token, held_node, insert_children,
16    replace_children, starts_a_flow_collection, Lang, Sequence, SyntaxNode,
17};
18use crate::as_yaml::{AsYaml, YamlKind, YamlNode};
19use crate::lex::SyntaxKind;
20use crate::yaml::{dump_cst_to_string, ValueNode};
21use rowan::ast::AstNode;
22use rowan::{GreenNodeBuilder, TextSize};
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, _)| held_node(&k))
54    }
55
56    /// Iterate over raw KEY/VALUE syntax nodes for each mapping entry.
57    ///
58    /// Each item is `(key_node, value_node)`, both raw CST wrapper nodes
59    /// (`KEY`/`VALUE`), not the content nodes inside them. Entries with a
60    /// missing key or value (which indicate a parse error in the source) are
61    /// silently skipped.
62    ///
63    /// For most use cases prefer [`iter`](Self::iter) (which yields
64    /// `(YamlNode, YamlNode)` pairs) or [`entries`](Self::entries) (which
65    /// yields typed [`MappingEntry`] handles that give access to the full
66    /// entry including key, value, and mutation methods).
67    pub(crate) fn pairs(&self) -> impl Iterator<Item = (SyntaxNode, SyntaxNode)> + '_ {
68        self.0
69            .children()
70            .filter(|n| n.kind() == SyntaxKind::MAPPING_ENTRY)
71            .filter_map(|entry| Some((entry_key(&entry)?, entry_value(&entry)?)))
72    }
73
74    /// Get the value associated with `key` as a [`YamlNode`](crate::as_yaml::YamlNode).
75    ///
76    /// Returns `None` if the key does not exist.
77    ///
78    /// Matching is semantic but type-strict, so a key's type has to match as
79    /// well as its text: `1` does not find the string key `"1"`, and the
80    /// null key of `: v` is found with
81    /// [`ScalarValue::null`](crate::ScalarValue::null) rather than `""`,
82    /// which finds the distinct empty-string key of `"": v`.
83    ///
84    /// ```rust
85    /// # use std::str::FromStr;
86    /// # use yaml_edit::{Document, ScalarValue};
87    /// let doc = Document::from_str(": a\n\"\": b\n").unwrap();
88    /// let mapping = doc.as_mapping().unwrap();
89    /// assert!(mapping.get(ScalarValue::null()).is_some());
90    /// assert!(mapping.get("").is_some());
91    /// ```
92    pub fn get(&self, key: impl crate::AsYaml) -> Option<crate::as_yaml::YamlNode> {
93        self.get_node(key)
94            .and_then(crate::as_yaml::YamlNode::from_syntax)
95    }
96
97    /// Get the raw content syntax node for `key` (for advanced CST access).
98    ///
99    /// Returns the content node inside the `VALUE` wrapper - i.e. the actual
100    /// `SCALAR`, `MAPPING`, or `SEQUENCE` node. Returns `None` if the key does
101    /// not exist. For most use cases prefer [`get`](Self::get).
102    pub(crate) fn get_node(&self, key: impl crate::AsYaml) -> Option<SyntaxNode> {
103        self.find_entry_by_key(key)
104            // VALUE nodes wrap the actual content, return the content instead
105            .and_then(|entry| entry.value().children().next())
106    }
107
108    /// Get the value for `key` as a nested [`Mapping`].
109    ///
110    /// Returns `None` if the key does not exist or its value is not a mapping.
111    /// A tag on the value is looked through: `key: !custom` over a block
112    /// mapping still gives that mapping.
113    pub fn get_mapping(&self, key: impl crate::AsYaml) -> Option<Mapping> {
114        self.get(key)?.mapping_content()
115    }
116
117    /// Modify a nested mapping in place by applying a closure to it.
118    ///
119    /// Returns `true` if `key` exists and its value is a mapping (and `f` was
120    /// called); returns `false` if the key is missing or its value is not a
121    /// mapping.
122    ///
123    /// Because [`Mapping`] uses interior mutability via rowan's `SyntaxNode`,
124    /// the closure receives a shared reference - mutations are still possible
125    /// through the node's `set`, `remove`, and other `&self` methods.
126    pub fn modify_mapping<F>(&self, key: impl crate::AsYaml, f: F) -> bool
127    where
128        F: FnOnce(&Mapping),
129    {
130        // The closure mutates through rowan's interior mutability, so the
131        // node found here is the one in the tree - nothing to splice back.
132        let Some(nested) = self.get_mapping(key) else {
133            return false;
134        };
135        f(&nested);
136        true
137    }
138
139    /// Get the value for `key` as a nested [`Sequence`].
140    ///
141    /// Returns `None` if the key does not exist or its value is not a sequence.
142    /// A tag on the value is looked through, as for
143    /// [`get_mapping`](Self::get_mapping).
144    pub fn get_sequence(&self, key: impl crate::AsYaml) -> Option<Sequence> {
145        self.get(key)?.sequence_content()
146    }
147
148    /// Returns `true` if this mapping contains an entry with the given key.
149    pub fn contains_key(&self, key: impl crate::AsYaml) -> bool {
150        self.find_entry_by_key(key).is_some()
151    }
152
153    /// Iterate over the raw `KEY` wrapper nodes for all entries.
154    ///
155    /// For most use cases prefer [`keys`](Self::keys) which yields
156    /// [`YamlNode`](crate::as_yaml::YamlNode) keys (formatting-preserving but
157    /// comparable via `yaml_eq`), or [`entries`](Self::entries) which yields
158    /// full [`MappingEntry`] handles.
159    pub(crate) fn key_nodes(&self) -> impl Iterator<Item = SyntaxNode> + '_ {
160        self.pairs().map(|(k, _)| k)
161    }
162
163    /// Check if the mapping is empty
164    pub fn is_empty(&self) -> bool {
165        self.pairs().next().is_none()
166    }
167
168    /// Get the number of key-value pairs in this mapping
169    pub fn len(&self) -> usize {
170        self.pairs().count()
171    }
172
173    /// Iterate over the values in this mapping as [`YamlNode`](crate::as_yaml::YamlNode)s.
174    ///
175    /// Only entries whose value can be successfully wrapped in a `YamlNode` are
176    /// yielded; malformed or unrecognised value nodes are silently skipped.
177    /// Use [`iter`](Self::iter) to get both the key and value simultaneously, or
178    /// `pairs()` for the raw `SyntaxNode` pairs.
179    pub fn values(&self) -> impl Iterator<Item = crate::as_yaml::YamlNode> + '_ {
180        self.pairs().filter_map(|(_, v)| held_node(&v))
181    }
182
183    /// Iterate over `(key, value)` pairs, both as [`YamlNode`](crate::as_yaml::YamlNode)s.
184    ///
185    /// Entries that cannot be fully wrapped (malformed key or value nodes) are
186    /// silently skipped. For raw CST nodes, use `pairs()`; for
187    /// typed entry handles, prefer [`entries`](Self::entries).
188    pub fn iter(
189        &self,
190    ) -> impl Iterator<Item = (crate::as_yaml::YamlNode, crate::as_yaml::YamlNode)> + '_ {
191        self.pairs()
192            .filter_map(|(k, v)| Some((held_node(&k)?, held_node(&v)?)))
193    }
194
195    /// Create a new empty mapping
196    pub fn new() -> Self {
197        Self::new_pending_block()
198    }
199
200    /// An empty mapping that renders as `{}` and stays flow when filled.
201    ///
202    /// Use this for a mapping that is meant to be flow-style in the output,
203    /// including one that is left empty.
204    pub fn new_flow() -> Self {
205        Self::empty_with_brace_text("{", "}")
206    }
207
208    /// An empty mapping that becomes a block mapping once an entry is added.
209    ///
210    /// YAML has no block syntax for an empty mapping: block collections are
211    /// written as their entries, so with none left there is nothing to write,
212    /// and a bare `key:` reads back as null. An empty mapping is therefore
213    /// `{}` whatever its eventual style, and block-vs-flow only becomes a
214    /// real choice once there is an entry.
215    ///
216    /// This renders as exactly `{}`, plus a zero-width token that marks it as
217    /// destined for block style. [`Mapping::set`] rewrites it into block form
218    /// when the first entry arrives; a `{}` that came from the source has no
219    /// such marker and keeps its flow style.
220    ///
221    /// A block mapping that loses its last entry to `remove` or `clear` is
222    /// written the same way, marker included.
223    ///
224    /// The marker lives only in the tree. Serializing and re-parsing yields
225    /// an ordinary `{}`, which stays flow, since YAML has nowhere to record
226    /// the intent.
227    pub fn new_pending_block() -> Self {
228        let mut builder = GreenNodeBuilder::new();
229        builder.start_node(SyntaxKind::MAPPING.into());
230        builder.token(SyntaxKind::LEFT_BRACE.into(), "{");
231        // Zero-width sentinel: renders as nothing, so this is still exactly
232        // `{}` in the text, but marks the mapping as one that should become
233        // block when it gets its first entry.
234        builder.token(SyntaxKind::WHITESPACE.into(), "");
235        builder.token(SyntaxKind::RIGHT_BRACE.into(), "}");
236        builder.finish_node();
237        Mapping(SyntaxNode::new_root_mut(builder.finish()))
238    }
239
240    fn empty_with_brace_text(open: &str, close: &str) -> Self {
241        let mut builder = GreenNodeBuilder::new();
242        builder.start_node(SyntaxKind::MAPPING.into());
243        builder.token(SyntaxKind::LEFT_BRACE.into(), open);
244        builder.token(SyntaxKind::RIGHT_BRACE.into(), close);
245        builder.finish_node();
246        Mapping(SyntaxNode::new_root_mut(builder.finish()))
247    }
248
249    /// Wrap a flow pair in braces, so that it reads as one mapping however
250    /// many entries it ends up with: `[a: 1, b: 2]` would be two.
251    fn brace_flow_pair(&self) {
252        if !is_flow_pair(&self.0) {
253            return;
254        }
255        let end = self.0.children_with_tokens().count();
256        insert_children(
257            &self.0,
258            end,
259            vec![fresh_token(SyntaxKind::RIGHT_BRACE, "}").into()],
260        );
261        insert_children(
262            &self.0,
263            0,
264            vec![fresh_token(SyntaxKind::LEFT_BRACE, "{").into()],
265        );
266    }
267
268    /// Reorder fields according to the specified order.
269    ///
270    /// Fields not in the order list will appear after the ordered fields,
271    /// in their original relative order.
272    ///
273    /// An alias that ends up before the node it refers to becomes a copy of
274    /// it, anchor included, and the node an alias where it can.
275    pub fn reorder_fields<I, K>(&self, order: I)
276    where
277        I: IntoIterator<Item = K>,
278        K: crate::AsYaml,
279    {
280        let order_keys: Vec<K> = order.into_iter().collect();
281        let entry_has_key = |entry: &SyntaxNode, key: &K| {
282            entry
283                .children()
284                .find(|n| n.kind() == SyntaxKind::KEY)
285                .is_some_and(|k| key_content_matches(&k, key))
286        };
287        // Take each named key's entry in the order given; anything the
288        // caller did not name keeps its relative position at the end.
289        let mut rest: Vec<SyntaxNode> = self.entries().map(|e| e.0).collect();
290        let mut ordered: Vec<SyntaxNode> = order_keys
291            .iter()
292            .filter_map(|key| {
293                let pos = rest.iter().position(|entry| entry_has_key(entry, key))?;
294                Some(rest.remove(pos))
295            })
296            .collect();
297        ordered.extend(rest);
298        self.arrange_entries(&ordered);
299    }
300}
301
302/// Is `node` a single pair in a flow sequence (`[a: 1]`), a flow mapping
303/// written without braces?
304pub(crate) fn is_flow_pair(node: &SyntaxNode) -> bool {
305    node.kind() == SyntaxKind::MAPPING
306        && !starts_a_flow_collection(node)
307        && node
308            .parent()
309            .filter(|p| p.kind() == SyntaxKind::SEQUENCE_ENTRY)
310            .and_then(|p| p.parent())
311            .is_some_and(|seq| starts_a_flow_collection(&seq))
312}
313
314/// Check whether the CST key node matches `key` using semantic equality.
315///
316/// An alias equals no node under `yaml_eq`, so an alias in a key is matched
317/// by an alias of the same name instead, not by the node it refers to:
318/// `*a` by `*a`, and `[*a]` by `[*a]`.
319///
320/// `key_node` may be a raw KEY wrapper.
321pub(super) fn key_content_matches(key_node: &SyntaxNode, key: impl crate::AsYaml) -> bool {
322    use crate::as_yaml::lookup_identity;
323    let Some(own) = lookup_identity(key_node) else {
324        return false;
325    };
326    let given = match key.value_node() {
327        Some(node) => lookup_identity(&node),
328        None => crate::as_yaml::identity_of(&key, None).ok().flatten(),
329    };
330    given.is_some_and(|given| given == own)
331}
332
333/// The anchor and tag on a KEY node, which sit in KEY itself or in the
334/// TAGGED_NODE it holds.
335fn key_annotations(key: &SyntaxNode) -> (Option<String>, Option<String>) {
336    let tagged = key
337        .children()
338        .next()
339        .filter(|n| n.kind() == SyntaxKind::TAGGED_NODE);
340    let tokens = key
341        .children_with_tokens()
342        .chain(tagged.iter().flat_map(|n| n.children_with_tokens()))
343        .filter_map(|c| c.into_token());
344    let (mut anchor, mut tag) = (None, None);
345    for token in tokens {
346        match token.kind() {
347            SyntaxKind::ANCHOR => anchor = anchor.or(Some(token.text().to_string())),
348            SyntaxKind::TAG => tag = tag.or(Some(token.text().to_string())),
349            _ => {}
350        }
351    }
352    (anchor, tag)
353}
354
355/// `key` under `tag`, if it has no tag of its own and is still the same key
356/// with it: `!!str` suits a string but not `1`, and a custom tag suits
357/// nothing given untagged.
358fn retagged_key(
359    tag: &str,
360    key: &impl crate::AsYaml,
361    flow: Option<crate::as_yaml::FlowQuoting>,
362) -> Option<crate::as_yaml::YamlNode> {
363    if matches!(key.kind(), YamlKind::Tagged(_)) {
364        return None;
365    }
366    let mut builder = GreenNodeBuilder::new();
367    builder.start_node(SyntaxKind::TAGGED_NODE.into());
368    builder.token(SyntaxKind::TAG.into(), tag);
369    builder.token(SyntaxKind::WHITESPACE.into(), " ");
370    super::build_key_content(&mut builder, key, 0, flow);
371    builder.finish_node();
372    let tagged = crate::as_yaml::YamlNode::from_syntax(SyntaxNode::new_root_mut(builder.finish()))?;
373    crate::as_yaml::yaml_eq(&tagged, key).then_some(tagged)
374}
375
376impl Mapping {
377    /// Check if this mapping is in flow style (JSON/inline format with `{}`).
378    ///
379    /// Returns `true` if the mapping uses flow style (e.g., `{key: value}`,
380    /// or a single pair in a flow sequence such as `[key: value]`), `false`
381    /// if it uses block style (e.g., `key: value`).
382    pub fn is_flow_style(&self) -> bool {
383        // Flow-style mappings start with LEFT_BRACE token. A block
384        // placeholder is `{}` in the text but is destined to become block, so
385        // it does not count as flow, unless a flow collection around it
386        // keeps it flow.
387        let becomes_block = empty_collection::is_block_placeholder(&self.0)
388            && !crate::nodes::sequence::must_render_flow(&self.0);
389        (starts_a_flow_collection(&self.0) && !becomes_block) || is_flow_pair(&self.0)
390    }
391
392    /// Find the [`MappingEntry`] whose key matches `key`, or `None` if not found.
393    ///
394    /// Matching is semantic (quoting style is ignored), so `"foo"`, `'foo'`,
395    /// and `foo` all match the scalar `"foo"`.
396    pub fn find_entry_by_key(&self, key: impl crate::AsYaml) -> Option<MappingEntry> {
397        self.0
398            .children()
399            .filter_map(MappingEntry::cast)
400            .find(|entry| entry.key().is_some_and(|k| key_content_matches(&k, &key)))
401    }
402
403    /// Find all entries with a given key.
404    ///
405    /// Returns an iterator over all [`MappingEntry`] instances that match the
406    /// given key. This is useful for handling duplicate keys in YAML (which
407    /// are allowed by the spec but semantically ambiguous).
408    ///
409    /// Matching is semantic (quoting style is ignored), so `"foo"`, `'foo'`,
410    /// and `foo` all match the scalar `"foo"`.
411    ///
412    /// # Example
413    ///
414    /// ```rust
415    /// # use std::str::FromStr;
416    /// # use yaml_edit::Document;
417    /// let yaml = r#"
418    /// Reference: First
419    /// Reference: Second
420    /// Reference: Third
421    /// "#;
422    ///
423    /// let doc = Document::from_str(yaml).unwrap();
424    /// let mapping = doc.as_mapping().unwrap();
425    ///
426    /// // Collect all Reference entries
427    /// let refs: Vec<_> = mapping.find_all_entries_by_key("Reference").collect();
428    /// assert_eq!(refs.len(), 3);
429    ///
430    /// // Remove all but the first occurrence
431    /// let _: Vec<()> = refs.into_iter().skip(1).map(|entry| entry.remove()).collect();
432    /// ```
433    pub fn find_all_entries_by_key<'a>(
434        &'a self,
435        key: impl crate::AsYaml + 'a,
436    ) -> impl Iterator<Item = MappingEntry> + 'a {
437        self.0
438            .children()
439            .filter_map(MappingEntry::cast)
440            .filter(move |entry| entry.key().is_some_and(|k| key_content_matches(&k, &key)))
441    }
442
443    /// Iterate over all entries in this mapping as typed [`MappingEntry`] handles.
444    ///
445    /// Each [`MappingEntry`] gives access to the key, value, and mutation
446    /// methods for that entry. For decoded `(key, value)` pairs, prefer
447    /// [`iter`](Self::iter); for raw CST nodes, use `pairs()`.
448    pub fn entries(&self) -> impl Iterator<Item = MappingEntry> {
449        self.0.children().filter_map(MappingEntry::cast)
450    }
451
452    /// Return the entry whose block value logically contains `offset`.
453    ///
454    /// Trailing indented blank lines past an entry's reported range are
455    /// attributed to that entry: anything indented past the key column
456    /// continues the value. Returns `None` for a cursor before the first
457    /// entry or on an un-indented line between entries.
458    ///
459    /// ```
460    /// use std::str::FromStr;
461    /// use yaml_edit::Document;
462    ///
463    /// let text = "Reference:\n  - Type: Book\n    \n";
464    /// let doc = Document::from_str(text).unwrap();
465    /// let mapping = doc.as_mapping().unwrap();
466    /// let entry = mapping.find_entry_by_offset(text, 30).unwrap();
467    /// assert!(entry.key_matches("Reference"));
468    /// ```
469    pub fn find_entry_by_offset(&self, source_text: &str, offset: usize) -> Option<MappingEntry> {
470        let offset = TextSize::try_from(offset).ok()?;
471        if let Some(e) = self.entries().find(|e| {
472            let r = e.syntax().text_range();
473            offset >= r.start() && offset < r.end()
474        }) {
475            return Some(e);
476        }
477        let cursor_col = crate::byte_offset_to_line_column(source_text, offset.into()).column;
478        self.entries()
479            .filter(|e| offset >= e.syntax().text_range().end())
480            .filter(|e| {
481                let key_col = match e.key_node() {
482                    Some(YamlNode::Scalar(s)) => s.start_position(source_text).column,
483                    _ => return false,
484                };
485                cursor_col > key_col
486            })
487            .last()
488    }
489
490    /// Return an [`Entry`](crate::Entry) view into the entry for `key`.
491    ///
492    /// Mirrors [`std::collections::BTreeMap::entry`], enabling
493    /// insert-or-update patterns while preserving formatting.
494    ///
495    /// ```rust
496    /// use std::str::FromStr;
497    /// use yaml_edit::Document;
498    ///
499    /// let doc = Document::from_str("name: Alice\n").unwrap();
500    /// let mapping = doc.as_mapping().unwrap();
501    ///
502    /// mapping.entry("age").or_insert(30);
503    /// mapping.entry("name").or_insert("Bob");
504    ///
505    /// assert_eq!(doc.to_string(), "name: Alice\nage: 30\n");
506    /// ```
507    pub fn entry<K: crate::AsYaml>(&self, key: K) -> crate::Entry<'_, K> {
508        match self.find_entry_by_key(&key) {
509            Some(entry) => crate::Entry::Occupied(crate::OccupiedEntry {
510                mapping: self,
511                entry,
512            }),
513            None => crate::Entry::Vacant(crate::VacantEntry { mapping: self, key }),
514        }
515    }
516
517    /// Find the child index of a mapping entry by its key.
518    ///
519    /// Returns the index within the mapping's children (including non-entry
520    /// tokens like whitespace), or `None` if no entry with the given key exists.
521    /// This index is suitable for use with `splice_children`.
522    pub fn find_entry_index_by_key(&self, key: impl crate::AsYaml) -> Option<usize> {
523        // Look through all children (not just entries, to get accurate index)
524        self.0
525            .children_with_tokens()
526            .enumerate()
527            .find_map(|(i, child)| {
528                let node = child.as_node()?;
529                if node.kind() != SyntaxKind::MAPPING_ENTRY {
530                    return None;
531                }
532                let entry = MappingEntry::cast(node.clone())?;
533                if entry.key().is_some_and(|k| key_content_matches(&k, &key)) {
534                    Some(i)
535                } else {
536                    None
537                }
538            })
539    }
540
541    /// Set a key-value pair, replacing the existing value if the key exists or
542    /// appending a new entry if it does not. Accepts any value that implements
543    /// [`AsYaml`](crate::AsYaml) - scalars, mappings, sequences, etc.
544    ///
545    /// This method always succeeds; it never silently ignores input. See also
546    /// [`insert_after`](Self::insert_after) and [`insert_before`](Self::insert_before)
547    /// which return `bool` to indicate whether the anchor key was found.
548    ///
549    /// Mutates in place despite `&self` (see crate docs on interior mutability).
550    ///
551    /// `set` replaces the value with the node you pass. It does not infer
552    /// YAML 1.1 merge keys from a resolved map. To keep identity, pass an
553    /// alias node (`*name`) or a mapping that already contains `<<: *name`.
554    /// Merge **reads** (`get_resolved()`, `merged()`) stay separate from
555    /// this CST replace.
556    ///
557    /// An anchor on the replaced value stays, on the new value, so aliases
558    /// to it still resolve: `b: &x 2` set to `6` gives `b: &x 6`. A node
559    /// value that has an anchor of its own keeps it only if the old value
560    /// had none. The first alias to an anchor deeper in the old value
561    /// becomes a copy of the anchored node: `b: {c: &y 1}` and `d: *y` give
562    /// `b: 6` and `d: &y 1`.
563    ///
564    /// ```
565    /// use std::str::FromStr;
566    /// use yaml_edit::Document;
567    ///
568    /// let doc = Document::from_str(
569    ///     "shared: &shared\n  timeout: 30\n  retries: 3\nservice_a: old\n",
570    /// )
571    /// .unwrap();
572    /// let mapping = doc.as_mapping().unwrap();
573    /// let merge = Document::from_str("<<: *shared\ntimeout: 60\n").unwrap();
574    /// mapping.set("service_a", merge.as_mapping().unwrap());
575    /// assert_eq!(
576    ///     doc.to_string(),
577    ///     "shared: &shared\n  timeout: 30\n  retries: 3\nservice_a:\n  <<: *shared\n  timeout: 60\n"
578    /// );
579    /// ```
580    pub fn set(&self, key: impl crate::AsYaml, value: impl crate::AsYaml) {
581        // An existing entry with this key takes the value in place.
582        let Some(entry) = self.find_entry_by_key(&key) else {
583            self.insert_new(Slot::End, key, value);
584            return;
585        };
586        self.styled_value(&value, |v, flow_context| entry.set_value(v, flow_context));
587    }
588
589    /// Detect if this mapping uses explicit key indicators (?)
590    pub(super) fn uses_explicit_keys(&self) -> bool {
591        // Only block explicit keys count, whose `?` is a child of
592        // MAPPING_ENTRY. In flow context the parser puts it inside KEY, and
593        // new entries there are written implicit.
594        for child in self.0.children() {
595            if child.kind() == SyntaxKind::MAPPING_ENTRY {
596                // Check if this entry has a QUESTION token as a child
597                if has_child_token(&child, |k| k == SyntaxKind::QUESTION) {
598                    return true;
599                }
600            }
601        }
602        false
603    }
604
605    /// Hand `put` `value` as an entry of this mapping holds it, in flow form
606    /// in a flow mapping, and whether the mapping is flow. The flow form is
607    /// shifted to the entry's line when put in, so its continuation lines
608    /// are given relative to that.
609    fn styled_value(&self, value: &impl crate::AsYaml, put: impl FnOnce(&dyn crate::AsYaml, bool)) {
610        let flow_context = self.is_flow_style();
611        let flow = flow_context
612            .then(|| crate::nodes::flow_quoting(&self.0))
613            .and_then(|q| flow_form(value, 2, q));
614        match &flow {
615            Some(flow) => put(flow, flow_context),
616            None => put(value, flow_context),
617        }
618    }
619
620    /// Add an entry for `key`, which is not in this mapping yet, at `slot`,
621    /// written in the mapping's style.
622    fn insert_new(&self, slot: Slot, key: impl crate::AsYaml, value: impl crate::AsYaml) {
623        // A placeholder from `Mapping::new_pending_block` turns into a real
624        // block mapping now that it has an entry to hold, before the entry
625        // is laid out.
626        empty_collection::convert_placeholder_to_block(&self.0);
627        let flow = self
628            .is_flow_style()
629            .then(|| crate::nodes::flow_quoting(&self.0));
630        // The column of the key tells the constructor how far to shift a
631        // node-backed block value.
632        let new_entry = MappingEntry::new_in(
633            key,
634            value,
635            flow,
636            self.uses_explicit_keys(),
637            self.detect_indentation_level(),
638        );
639        self.attach(&new_entry.0, slot);
640    }
641
642    /// Splice `new_entry` into this mapping at `slot`.
643    pub(super) fn attach(&self, new_entry: &SyntaxNode, slot: Slot) {
644        if self.is_flow_style() {
645            self.brace_flow_pair();
646            trivia::attach_flow_entry(&self.0, slot, new_entry);
647        } else {
648            trivia::attach_entry(&self.0, slot, new_entry);
649        }
650    }
651
652    /// Set a key-value pair with field ordering support
653    /// If the key exists, updates its value. If the key doesn't exist, inserts it
654    /// at the correct position based on the provided field order.
655    /// Fields not in the order list are placed at the end.
656    pub fn set_with_field_order<I, K>(
657        &self,
658        key: impl crate::AsYaml,
659        value: impl crate::AsYaml,
660        field_order: I,
661    ) where
662        I: IntoIterator<Item = K>,
663        K: crate::AsYaml,
664    {
665        // Collect field_order so we can iterate it multiple times.
666        let field_order: Vec<K> = field_order.into_iter().collect();
667
668        // An existing key just takes the new value, wherever it sits.
669        if self.find_entry_by_key(&key).is_some() {
670            self.set(&key, &value);
671            return;
672        }
673
674        let Some(key_index) = field_order
675            .iter()
676            .position(|field| crate::as_yaml::yaml_eq(&key, field))
677        else {
678            self.insert_new(Slot::End, key, value);
679            return;
680        };
681        // The nearest preceding field that is present decides where the new
682        // entry goes. With none, it goes before the first present field that
683        // the order puts after it, and with no ordered key present at the end.
684        let before_later = || {
685            self.entries().find_map(|entry| {
686                let key_node = entry.key()?;
687                let pos = field_order
688                    .iter()
689                    .position(|field| key_content_matches(&key_node, field))?;
690                (pos > key_index).then_some(Slot::Before(entry.0))
691            })
692        };
693        let slot = field_order[..key_index]
694            .iter()
695            .rev()
696            .find_map(|field| self.find_entry_by_key(field))
697            .map(|entry| Slot::After(entry.0))
698            .or_else(before_later)
699            .unwrap_or(Slot::End);
700        self.insert_new(slot, key, value);
701    }
702
703    /// Detect the indentation level (in spaces) used by entries in this mapping.
704    ///
705    /// Returns `0` for root-level mappings where entries have no leading indentation.
706    pub fn detect_indentation_level(&self) -> usize {
707        trivia::entry_column(&self.0)
708    }
709
710    /// Insert a key-value pair at a specific index (0-based), preserving formatting.
711    ///
712    /// If `new_key` already exists in the mapping, the existing entry is replaced
713    /// with a newly built entry at the **same position** (the `index` argument is
714    /// ignored). Surrounding whitespace in the file is preserved, but the entry
715    /// node itself is rebuilt (comments attached to the old entry may be lost).
716    /// An anchor on the old value stays on the new one, as with
717    /// [`set`](Self::set).
718    /// If `index` is out of bounds, the entry is appended at the end.
719    pub fn insert_at_index_preserving(
720        &self,
721        index: usize,
722        new_key: impl crate::AsYaml,
723        new_value: impl crate::AsYaml,
724    ) {
725        match self.find_entry_by_key(&new_key) {
726            Some(old) => {
727                self.replace_entry(&old.0, new_key, new_value, true);
728            }
729            None => self.insert_new_entry_at(index, new_key, new_value),
730        }
731    }
732
733    /// Replace `old`, an entry of this mapping, with a new entry built the
734    /// way `set` builds one, in the same place, and return it. If
735    /// `keep_anchor` is set, an anchor on the old value goes on the new one
736    /// unless that is an alias, as with `set`; otherwise the old value goes
737    /// as with `remove`.
738    pub(super) fn replace_entry(
739        &self,
740        old: &SyntaxNode,
741        key: impl crate::AsYaml,
742        value: impl crate::AsYaml,
743        keep_anchor: bool,
744    ) -> SyntaxNode {
745        let kept = MappingEntry::cast(old.clone())
746            .map(|e| e.value())
747            .filter(|_| keep_anchor)
748            .and_then(|v| kept_anchor_token(&v, &value));
749        let anchor = kept.as_ref().map(|t| t.text().to_string());
750        let mut new = None;
751        crate::nodes::kept_aliases::keeping_aliases(old, kept, || {
752            // The entry is built without the value's anchor; putting the
753            // value in again settles which anchor it gets, as in set_value.
754            let entry = MappingEntry(self.splice_in_entry(old, key, &value));
755            self.put_value(&entry, &value, anchor.as_deref());
756            new = Some(entry.0.clone());
757            Some(entry.value())
758        });
759        new.expect("the edit ran")
760    }
761
762    /// Replace the value of `entry`, an entry of this mapping, with `value`
763    /// under `anchor`, as `set` does.
764    pub(super) fn put_value(
765        &self,
766        entry: &MappingEntry,
767        value: impl crate::AsYaml,
768        anchor: Option<&str>,
769    ) {
770        self.styled_value(&value, |v, flow_context| {
771            entry.replace_value_under(v, flow_context, anchor)
772        });
773    }
774
775    /// The edit of [`replace_entry`](Self::replace_entry), without the
776    /// kept anchor. Returns the new entry.
777    fn splice_in_entry(
778        &self,
779        old: &SyntaxNode,
780        key: impl crate::AsYaml,
781        value: impl crate::AsYaml,
782    ) -> SyntaxNode {
783        let flow_context = self.is_flow_style();
784        let new_entry = MappingEntry::new_in(
785            key,
786            value,
787            flow_context.then(|| crate::nodes::flow_quoting(&self.0)),
788            self.uses_explicit_keys(),
789            self.detect_indentation_level(),
790        )
791        .0;
792        if flow_context {
793            // The separator to the next entry lives in the entry before it,
794            // as does the line break ending its line.
795            let mut separator: Vec<_> = old
796                .children_with_tokens()
797                .skip_while(|c| c.kind() != SyntaxKind::COMMA)
798                .collect();
799            if separator.is_empty() {
800                separator.extend(
801                    old.last_child_or_token()
802                        .filter(|c| c.kind() == SyntaxKind::NEWLINE),
803                );
804            }
805            for c in &separator {
806                c.detach();
807            }
808            crate::nodes::append_children(&new_entry, separator);
809        } else {
810            // The new entry brings its own line break, unless the old one
811            // ended the input without one.
812            if !trivia::ends_with_line_break(old) {
813                let last = new_entry
814                    .last_child_or_token()
815                    .filter(|c| c.kind() == SyntaxKind::NEWLINE);
816                if let Some(last) = last {
817                    last.detach();
818                }
819            }
820        }
821        let i = old.index();
822        replace_children(&self.0, i..i + 1, vec![new_entry.clone().into()]);
823        new_entry
824    }
825
826    /// Remove a key-value pair, returning the removed entry.
827    ///
828    /// Returns `Some(entry)` if the key existed and was removed, or `None` if
829    /// the key was not found. The returned [`MappingEntry`] is detached from
830    /// the tree; callers can inspect its key and value or re-insert it
831    /// elsewhere.
832    ///
833    /// The first alias to an anchor in the removed entry becomes a copy of
834    /// the anchored node, so it and later aliases keep their value.
835    ///
836    /// Mutates in place despite `&self` (see crate docs on interior mutability).
837    pub fn remove(&self, key: impl crate::AsYaml) -> Option<MappingEntry> {
838        let entry = self.entries().find(|e| e.key_matches(&key))?;
839        self.remove_entry(entry.syntax());
840        Some(entry)
841    }
842
843    /// Splice `removed`, an entry of this mapping, out of it with the
844    /// trivia that belongs to it.
845    fn remove_entry(&self, removed: &SyntaxNode) {
846        // Without its pair, a flow pair would leave `[]`, an empty sequence.
847        if is_flow_pair(&self.0) {
848            self.brace_flow_pair();
849        }
850        crate::nodes::kept_aliases::keeping_aliases(removed, None, || {
851            trivia::detach_entry(&self.0, removed);
852            None
853        });
854    }
855
856    /// Remove the nth occurrence of a key, returning the removed entry.
857    ///
858    /// Returns `Some(entry)` if the nth occurrence exists and was removed,
859    /// or `None` if there are fewer than `n+1` occurrences of the key.
860    /// The index `n` is 0-based (n=0 removes the first occurrence, n=1 removes
861    /// the second, etc.).
862    ///
863    /// This is useful for handling duplicate keys in YAML. While duplicate keys
864    /// are semantically ambiguous, they are allowed by the YAML spec, and this
865    /// method provides fine-grained control over which occurrence to remove.
866    ///
867    /// # Example
868    ///
869    /// ```rust
870    /// # use std::str::FromStr;
871    /// # use yaml_edit::Document;
872    /// let yaml = r#"
873    /// Reference: First
874    /// Reference: Second
875    /// Reference: Third
876    /// "#;
877    ///
878    /// let doc = Document::from_str(yaml).unwrap();
879    /// let mapping = doc.as_mapping().unwrap();
880    ///
881    /// // Remove the second occurrence (index 1)
882    /// let removed = mapping.remove_nth_occurrence("Reference", 1);
883    /// assert!(removed.is_some());
884    ///
885    /// // Now only two Reference entries remain
886    /// let count = mapping.find_all_entries_by_key("Reference").count();
887    /// assert_eq!(count, 2);
888    /// ```
889    ///
890    /// Mutates in place despite `&self` (see crate docs on interior mutability).
891    pub fn remove_nth_occurrence(&self, key: impl crate::AsYaml, n: usize) -> Option<MappingEntry> {
892        let entry = self.entries().filter(|e| e.key_matches(&key)).nth(n)?;
893        self.remove_entry(entry.syntax());
894        Some(entry)
895    }
896
897    /// Remove all key-value pairs from this mapping.
898    ///
899    /// Mutates in place despite `&self` (see crate docs on interior mutability).
900    pub fn clear(&self) {
901        let line_break = self
902            .entries()
903            .last()
904            .and_then(|e| trivia::line_break(e.syntax()));
905        // Entries are removed one by one, as keeping aliases resolving may
906        // rewrite a later key.
907        while let Some(entry) = self.entries().next() {
908            self.remove_entry(entry.syntax());
909        }
910        render_empty(&self.0, line_break.as_deref());
911    }
912
913    /// Rename a key while preserving its value and formatting.
914    ///
915    /// The new key is built using the same `AsYaml` infrastructure as other
916    /// write methods, so quoting and escaping are handled automatically.
917    /// Returns `true` if the key was found and renamed, `false` if `old_key`
918    /// does not exist.
919    ///
920    /// If `new_key` differs from `old_key` and is already present, the
921    /// existing `new_key` entry is removed, as with [`set`](Self::set) and
922    /// [`move_after`](Self::move_after), so the rename does not create a
923    /// duplicate key. The renamed entry keeps its position, value and
924    /// comments.
925    ///
926    /// The key keeps its anchor, so aliases to it still resolve; an anchor
927    /// on a `new_key` node is used only if the old key had none. The key's
928    /// tag is kept if `new_key` has no tag of its own and still means the
929    /// same with it: `!!str a` renamed to `"b"` gives `!!str b`, but renamed
930    /// to `1` gives `1`, as `!!str 1` would be a string. The first alias to
931    /// an anchor deeper in the old key becomes a copy of the anchored node.
932    ///
933    /// ```
934    /// # use std::str::FromStr;
935    /// # use yaml_edit::Document;
936    /// let doc = Document::from_str("a: 1\nb: 2\nc: 3\n").unwrap();
937    /// assert!(doc.as_mapping().unwrap().rename_key("a", "b"));
938    /// assert_eq!(doc.to_string(), "b: 1\nc: 3\n");
939    /// ```
940    ///
941    /// Mutates in place despite `&self` (see crate docs on interior mutability).
942    pub fn rename_key(&self, old_key: impl crate::AsYaml, new_key: impl crate::AsYaml) -> bool {
943        let Some(node) = self.find_entry_by_key(&old_key).map(|e| e.0) else {
944            return false;
945        };
946
947        let renames_to_itself = entry_key(&node).is_some_and(|k| key_content_matches(&k, &new_key));
948        if !renames_to_itself {
949            self.remove_other_entries(&node, &new_key);
950        }
951        let old = entry_key(&node).expect("a mapping entry has a key");
952        let kept = held_anchor_token(&old);
953        crate::nodes::kept_aliases::keeping_aliases(&old, kept, || {
954            Some(self.replace_key(&node, new_key, None))
955        });
956        true
957    }
958
959    /// Replace the key of `node`, an entry of this mapping, with `new_key`
960    /// under `anchor`, and return the new KEY.
961    pub(crate) fn replace_key(
962        &self,
963        node: &SyntaxNode,
964        new_key: impl crate::AsYaml,
965        anchor: Option<&str>,
966    ) -> SyntaxNode {
967        if let Some(key) = flow_form(&new_key, 2, crate::nodes::flow_quoting(&self.0)) {
968            return self.replace_key(node, key, anchor);
969        }
970        let indent = self.detect_indentation_level();
971        // A key spanning lines has to be explicit, with the `:` on a line
972        // of its own.
973        let make_explicit = !self.is_flow_style()
974            && !has_child_token(node, |k| k == SyntaxKind::QUESTION)
975            && super::key_spans_lines(&new_key);
976        let old_key = entry_key(node).expect("a mapping entry has a key");
977        let mut builder = GreenNodeBuilder::new();
978        builder.start_node(SyntaxKind::ROOT.into());
979        if make_explicit {
980            builder.token(SyntaxKind::QUESTION.into(), "?");
981            builder.token(SyntaxKind::WHITESPACE.into(), " ");
982        }
983        let flow = self
984            .is_flow_style()
985            .then(|| crate::nodes::flow_quoting(&self.0));
986        Self::rebuild_key(&mut builder, &old_key, &new_key, anchor, flow);
987        if make_explicit {
988            builder.token(SyntaxKind::NEWLINE.into(), "\n");
989            if indent > 0 {
990                builder.token(SyntaxKind::INDENT.into(), &" ".repeat(indent));
991            }
992        }
993        builder.finish_node();
994        let built = SyntaxNode::new_root_mut(builder.finish());
995        let new_key = built
996            .children()
997            .find(|n| n.kind() == SyntaxKind::KEY)
998            .expect("a KEY was just built");
999        if anchor.is_some() {
1000            drop_tagged_anchor(&new_key);
1001        }
1002
1003        // Whitespace between the key and the `:` would now start the `:`
1004        // line. Detached one by one, as a multi-child splice can skip some.
1005        if make_explicit {
1006            let stale: Vec<_> = old_key
1007                .siblings_with_tokens(rowan::Direction::Next)
1008                .skip(1)
1009                .map_while(|c| {
1010                    c.into_token()
1011                        .filter(|t| t.kind() == SyntaxKind::WHITESPACE)
1012                })
1013                .collect();
1014            for token in stale {
1015                token.detach();
1016            }
1017        }
1018        let old_ended_line = trivia::ends_with_line_break(&old_key);
1019        let i = old_key.index();
1020        replace_children(
1021            node,
1022            i..i + 1,
1023            built.children_with_tokens().collect::<Vec<_>>(),
1024        );
1025        if has_child_token(node, |k| k == SyntaxKind::QUESTION) {
1026            Self::settle_explicit_key_line(&new_key, old_ended_line);
1027        }
1028        if self.is_flow_style() {
1029            Self::separate_adjacent_value(&new_key);
1030        }
1031        crate::nodes::separate_key_from_colon(node);
1032        new_key
1033    }
1034
1035    /// Put a space after the `:` following `key` if the value starts right
1036    /// after it and `key` is not JSON-like: only after a quoted scalar or a
1037    /// flow collection does `"a":b` read as a key and a value, while `a:b`
1038    /// is one plain scalar.
1039    fn separate_adjacent_value(key: &SyntaxNode) {
1040        let trivia = |k: SyntaxKind| {
1041            matches!(
1042                k,
1043                SyntaxKind::WHITESPACE
1044                    | SyntaxKind::INDENT
1045                    | SyntaxKind::NEWLINE
1046                    | SyntaxKind::COMMENT
1047            )
1048        };
1049        // The `:` may be on a later line, after a comment.
1050        let Some(colon) = key
1051            .siblings_with_tokens(rowan::Direction::Next)
1052            .skip(1)
1053            .find(|c| !trivia(c.kind()))
1054            .and_then(|c| c.into_token())
1055            .filter(|t| t.kind() == SyntaxKind::COLON)
1056        else {
1057            return;
1058        };
1059        let json_like = key
1060            .descendants_with_tokens()
1061            .filter_map(|c| c.into_token())
1062            .filter(|t| !t.text().is_empty() && !trivia(t.kind()))
1063            .last()
1064            .is_some_and(|t| {
1065                matches!(
1066                    t.kind(),
1067                    SyntaxKind::RIGHT_BRACKET | SyntaxKind::RIGHT_BRACE
1068                ) || (t.kind() == SyntaxKind::STRING && t.text().starts_with(['"', '\'']))
1069            });
1070        let value_adjacent = std::iter::successors(colon.next_token(), |t| t.next_token())
1071            .find_map(|t| t.text().chars().next())
1072            .is_some_and(|c| !c.is_whitespace() && !matches!(c, ',' | ']' | '}'));
1073        if !json_like && value_adjacent {
1074            let i = colon.index() + 1;
1075            insert_children(
1076                &colon.parent().expect("a COLON is in an entry"),
1077                i,
1078                vec![fresh_token(SyntaxKind::WHITESPACE, " ").into()],
1079            );
1080        }
1081    }
1082
1083    /// Fix up the line of a block explicit key whose content was replaced.
1084    ///
1085    /// A block key (`? - a\n`) ends its own line, while the entry holds the
1086    /// line break after an inline one (`? a` NEWLINE). An empty key may
1087    /// follow the `?` directly, a non-empty one needs a space.
1088    fn settle_explicit_key_line(key: &SyntaxNode, old_ended_line: bool) {
1089        let entry = key.parent().expect("a KEY is in an entry");
1090        let before = key.prev_sibling_or_token();
1091        if before
1092            .as_ref()
1093            .is_some_and(|c| c.kind() == SyntaxKind::QUESTION)
1094            && !key.text().is_empty()
1095        {
1096            let i = key.index();
1097            insert_children(
1098                &entry,
1099                i,
1100                vec![fresh_token(SyntaxKind::WHITESPACE, " ").into()],
1101            );
1102        }
1103        // Without a `:` line, the key's line is the entry's.
1104        let has_colon = has_child_token(&entry, |k| k == SyntaxKind::COLON);
1105        let ends_line = trivia::ends_with_line_break(key);
1106        let newline_after = if has_colon {
1107            key.next_sibling_or_token()
1108        } else {
1109            entry.last_child_or_token()
1110        }
1111        .filter(|c| c.kind() == SyntaxKind::NEWLINE);
1112        if ends_line && !old_ended_line {
1113            if let Some(newline) = newline_after {
1114                newline.detach();
1115            }
1116        } else if old_ended_line && !ends_line {
1117            if has_colon {
1118                insert_children(
1119                    &entry,
1120                    key.index() + 1,
1121                    vec![fresh_token(SyntaxKind::NEWLINE, "\n").into()],
1122                );
1123            } else {
1124                trivia::terminate(&entry);
1125            }
1126        }
1127    }
1128
1129    /// Remove every entry other than `keep` whose key matches `key`.
1130    fn remove_other_entries(&self, keep: &SyntaxNode, key: &impl crate::AsYaml) {
1131        while let Some(other) = self.entries().find(|e| e.0 != *keep && e.key_matches(key)) {
1132            self.remove_entry(other.syntax());
1133        }
1134    }
1135
1136    /// Rebuild an entry's KEY node around `new_key`, keeping the layout
1137    /// tokens the old one carried.
1138    ///
1139    /// In a flow mapping the `?` sits inside KEY, so it carries over, as do
1140    /// any tokens after the key's content. The line break before block
1141    /// content (`?\n- a`) goes, as the new key is written inline, and the
1142    /// one after an empty key (`?\n}`) follows the new key.
1143    ///
1144    /// A comment before the old content stays on its line, or follows the
1145    /// new KEY if the content was on that line too.
1146    ///
1147    /// The old key's anchor stays; failing that the key takes `anchor`, or
1148    /// the anchor before a `new_key` node where it was copied from.
1149    fn rebuild_key(
1150        builder: &mut GreenNodeBuilder,
1151        old: &SyntaxNode,
1152        new_key: &impl crate::AsYaml,
1153        anchor: Option<&str>,
1154        flow: Option<crate::as_yaml::FlowQuoting>,
1155    ) {
1156        builder.start_node(SyntaxKind::KEY.into());
1157        let indent = old
1158            .parent()
1159            .map_or(0, |entry| crate::column::node_column(&entry));
1160        let leading: Vec<_> = old
1161            .children_with_tokens()
1162            .map_while(|c| c.into_token())
1163            .collect();
1164        let mut rest = &leading[..];
1165        if let Some((question, after)) = rest
1166            .split_first()
1167            .filter(|(t, _)| t.kind() == SyntaxKind::QUESTION)
1168        {
1169            builder.token(question.kind().into(), question.text());
1170            let ws = after
1171                .iter()
1172                .take_while(|t| t.kind() == SyntaxKind::WHITESPACE)
1173                .count();
1174            for t in &after[..ws] {
1175                builder.token(t.kind().into(), t.text());
1176            }
1177            if ws == 0 {
1178                builder.token(SyntaxKind::WHITESPACE.into(), " ");
1179            }
1180            rest = &after[ws..];
1181        }
1182        // A comment on the `?` line stays there if the content starts on a
1183        // later line, and otherwise goes after the new key.
1184        let comment = rest.iter().position(|t| t.kind() == SyntaxKind::COMMENT);
1185        let comment_lines = comment
1186            .filter(|&i| rest[i..].iter().any(|t| t.kind() == SyntaxKind::NEWLINE))
1187            .map(|i| &rest[i..]);
1188        let comment_after = comment
1189            .filter(|_| comment_lines.is_none())
1190            .map(|i| rest[i].text().to_string());
1191        let (old_anchor, tag) = key_annotations(old);
1192        let anchor = old_anchor
1193            .or_else(|| anchor.map(str::to_string))
1194            .or_else(|| new_key.value_node().and_then(|n| source_anchor_text(&n)));
1195        if let Some(anchor) = anchor {
1196            builder.token(SyntaxKind::ANCHOR.into(), &anchor);
1197            builder.token(SyntaxKind::WHITESPACE.into(), " ");
1198        }
1199        // On a line of its own, only a block sequence may start at the
1200        // column of the `?`; anything else has to be indented past it.
1201        let mut lines = comment_lines.unwrap_or_default();
1202        let line_indent = match lines.last() {
1203            Some(t) if t.kind() == SyntaxKind::INDENT => {
1204                lines = &lines[..lines.len() - 1];
1205                Some(t.text())
1206            }
1207            _ => None,
1208        };
1209        let block_sequence =
1210            new_key.kind() == crate::as_yaml::YamlKind::Sequence && !new_key.is_inline();
1211        let deeper = " ".repeat(indent + 2);
1212        let line_indent = match line_indent {
1213            Some(text) if block_sequence || text.len() > indent => Some(text),
1214            _ if comment_lines.is_some() && !block_sequence => Some(deeper.as_str()),
1215            other => other,
1216        };
1217        for t in lines {
1218            builder.token(t.kind().into(), t.text());
1219        }
1220        if let Some(text) = line_indent {
1221            builder.token(SyntaxKind::INDENT.into(), text);
1222        }
1223        match tag.and_then(|tag| retagged_key(&tag, new_key, flow)) {
1224            Some(tagged) => super::build_key_content(builder, &tagged, indent, flow),
1225            None => super::build_key_content(builder, new_key, indent, flow),
1226        }
1227        // What follows the old key's last node, such as the line break
1228        // ending an explicit key, stays after the new one.
1229        let last_node = old.last_child().expect("a KEY holds a node");
1230        for trailing in last_node
1231            .siblings_with_tokens(rowan::Direction::Next)
1232            .skip(1)
1233            .filter_map(|c| c.into_token())
1234        {
1235            builder.token(trailing.kind().into(), trailing.text());
1236        }
1237        builder.finish_node();
1238        if let Some(comment) = comment_after {
1239            builder.token(SyntaxKind::WHITESPACE.into(), "  ");
1240            builder.token(SyntaxKind::COMMENT.into(), &comment);
1241        }
1242        // The line break after an empty key now follows the new one.
1243        let old_empty = old.children().next().is_some_and(|n| n.text().is_empty());
1244        if old_empty && comment.is_none() {
1245            for t in rest
1246                .iter()
1247                .filter(|t| matches!(t.kind(), SyntaxKind::NEWLINE | SyntaxKind::INDENT))
1248            {
1249                builder.token(t.kind().into(), t.text());
1250            }
1251        }
1252    }
1253
1254    /// Insert a key-value pair immediately after an existing key.
1255    ///
1256    /// If `key` already exists in the mapping, its value is updated in-place and
1257    /// it remains at its current position (it is **not** moved to after `after_key`).
1258    /// Returns `true` in both the update and the insert cases.
1259    /// Returns `false` only if `after_key` is not found.
1260    ///
1261    /// Use [`move_after`](Self::move_after) if you want
1262    /// an existing entry to be moved to the new position.
1263    ///
1264    /// Mutates in place despite `&self` (see crate docs on interior mutability).
1265    pub fn insert_after(
1266        &self,
1267        after_key: impl crate::AsYaml,
1268        key: impl crate::AsYaml,
1269        value: impl crate::AsYaml,
1270    ) -> bool {
1271        self.insert_around(after_key, key, value, true)
1272    }
1273
1274    /// Insert a key-value pair immediately before an existing key.
1275    ///
1276    /// If `key` already exists in the mapping, its value is updated in-place and
1277    /// it remains at its current position (it is **not** moved to before `before_key`).
1278    /// Returns `true` in both the update and the insert cases.
1279    /// Returns `false` only if `before_key` is not found.
1280    ///
1281    /// Use [`move_before`](Self::move_before) if you want
1282    /// an existing entry to be moved to the new position.
1283    ///
1284    /// Mutates in place despite `&self` (see crate docs on interior mutability).
1285    pub fn insert_before(
1286        &self,
1287        before_key: impl crate::AsYaml,
1288        key: impl crate::AsYaml,
1289        value: impl crate::AsYaml,
1290    ) -> bool {
1291        self.insert_around(before_key, key, value, false)
1292    }
1293
1294    /// Shared body of [`insert_after`](Self::insert_after) and
1295    /// [`insert_before`](Self::insert_before): an existing `key` is updated
1296    /// where it is, a new one goes next to `ref_key`.
1297    fn insert_around(
1298        &self,
1299        ref_key: impl crate::AsYaml,
1300        key: impl crate::AsYaml,
1301        value: impl crate::AsYaml,
1302        after: bool,
1303    ) -> bool {
1304        if self.find_entry_by_key(&key).is_some() {
1305            self.set(&key, &value);
1306            return self.find_entry_by_key(&ref_key).is_some();
1307        }
1308        self.move_around(&ref_key, &key, &value, after)
1309    }
1310
1311    /// Insert a key-value pair at a specific index (0-based).
1312    ///
1313    /// If `key` already exists in the mapping, its value is updated in-place and
1314    /// it remains at its current position (the `index` argument is ignored).
1315    /// If `index` is out of bounds, the entry is appended at the end.
1316    /// This method always succeeds; it never returns an error.
1317    ///
1318    /// Mutates in place despite `&self` (see crate docs on interior mutability).
1319    pub fn insert_at_index(
1320        &self,
1321        index: usize,
1322        key: impl crate::AsYaml,
1323        value: impl crate::AsYaml,
1324    ) {
1325        // Check if the key already exists - if so, just update it
1326        if self.find_entry_by_key(&key).is_some() {
1327            self.set(&key, &value);
1328            return;
1329        }
1330        self.insert_new_entry_at(index, key, value);
1331    }
1332
1333    /// Insert an entry for a key that is not in the mapping yet before the
1334    /// `index`th entry, or at the end if there are fewer.
1335    fn insert_new_entry_at(
1336        &self,
1337        index: usize,
1338        key: impl crate::AsYaml,
1339        value: impl crate::AsYaml,
1340    ) {
1341        let slot = self
1342            .entries()
1343            .nth(index)
1344            .map_or(Slot::End, |e| Slot::Before(e.0));
1345        self.insert_new(slot, key, value);
1346    }
1347
1348    crate::nodes::ast_node_spans!("mapping");
1349}
1350
1351impl Default for Mapping {
1352    fn default() -> Self {
1353        Self::new()
1354    }
1355}
1356
1357// Iterator trait implementations for Mapping
1358
1359impl<'a> IntoIterator for &'a Mapping {
1360    type Item = (crate::as_yaml::YamlNode, crate::as_yaml::YamlNode);
1361    type IntoIter =
1362        Box<dyn Iterator<Item = (crate::as_yaml::YamlNode, crate::as_yaml::YamlNode)> + 'a>;
1363
1364    fn into_iter(self) -> Self::IntoIter {
1365        Box::new(self.iter())
1366    }
1367}
1368
1369impl AsYaml for Mapping {
1370    fn as_node(&self) -> Option<&SyntaxNode> {
1371        Some(&self.0)
1372    }
1373
1374    fn kind(&self) -> YamlKind {
1375        YamlKind::Mapping
1376    }
1377
1378    fn build_content(
1379        &self,
1380        builder: &mut rowan::GreenNodeBuilder,
1381        indent: usize,
1382        _flow_context: bool,
1383    ) -> bool {
1384        let is_flow = self.is_flow_style();
1385        let mut delta =
1386            indent as isize - crate::as_yaml::source_reindent_base(&self.0, is_flow) as isize;
1387        if is_flow {
1388            delta = crate::as_yaml::clear_parent_column(&self.0, indent, delta);
1389        }
1390        builder.start_node(SyntaxKind::MAPPING.into());
1391        crate::as_yaml::copy_node_content_reindent(builder, &self.0, delta);
1392        builder.finish_node();
1393        self.0
1394            .last_token()
1395            .is_some_and(|t| t.kind() == SyntaxKind::NEWLINE)
1396    }
1397
1398    fn is_inline(&self) -> bool {
1399        ValueNode::is_inline(self)
1400    }
1401}