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;