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};
19use crate::lex::SyntaxKind;
20use crate::yaml::{dump_cst_to_string, ValueNode};
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, _)| 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 an [`Entry`](crate::Entry) view into the entry for `key`.
453 ///
454 /// Mirrors [`std::collections::BTreeMap::entry`], enabling
455 /// insert-or-update patterns while preserving formatting.
456 ///
457 /// ```rust
458 /// use std::str::FromStr;
459 /// use yaml_edit::Document;
460 ///
461 /// let doc = Document::from_str("name: Alice\n").unwrap();
462 /// let mapping = doc.as_mapping().unwrap();
463 ///
464 /// mapping.entry("age").or_insert(30);
465 /// mapping.entry("name").or_insert("Bob");
466 ///
467 /// assert_eq!(doc.to_string(), "name: Alice\nage: 30\n");
468 /// ```
469 pub fn entry<K: crate::AsYaml>(&self, key: K) -> crate::Entry<'_, K> {
470 match self.find_entry_by_key(&key) {
471 Some(entry) => crate::Entry::Occupied(crate::OccupiedEntry {
472 mapping: self,
473 entry,
474 }),
475 None => crate::Entry::Vacant(crate::VacantEntry { mapping: self, key }),
476 }
477 }
478
479 /// Find the child index of a mapping entry by its key.
480 ///
481 /// Returns the index within the mapping's children (including non-entry
482 /// tokens like whitespace), or `None` if no entry with the given key exists.
483 /// This index is suitable for use with `splice_children`.
484 pub fn find_entry_index_by_key(&self, key: impl crate::AsYaml) -> Option<usize> {
485 // Look through all children (not just entries, to get accurate index)
486 self.0
487 .children_with_tokens()
488 .enumerate()
489 .find_map(|(i, child)| {
490 let node = child.as_node()?;
491 if node.kind() != SyntaxKind::MAPPING_ENTRY {
492 return None;
493 }
494 let entry = MappingEntry::cast(node.clone())?;
495 if entry.key().is_some_and(|k| key_content_matches(&k, &key)) {
496 Some(i)
497 } else {
498 None
499 }
500 })
501 }
502
503 /// Set a key-value pair, replacing the existing value if the key exists or
504 /// appending a new entry if it does not. Accepts any value that implements
505 /// [`AsYaml`](crate::AsYaml) - scalars, mappings, sequences, etc.
506 ///
507 /// This method always succeeds; it never silently ignores input. See also
508 /// [`insert_after`](Self::insert_after) and [`insert_before`](Self::insert_before)
509 /// which return `bool` to indicate whether the anchor key was found.
510 ///
511 /// Mutates in place despite `&self` (see crate docs on interior mutability).
512 ///
513 /// `set` replaces the value with the node you pass. It does not infer
514 /// YAML 1.1 merge keys from a resolved map. To keep identity, pass an
515 /// alias node (`*name`) or a mapping that already contains `<<: *name`.
516 /// Merge **reads** (`get_resolved()`, `merged()`) stay separate from
517 /// this CST replace.
518 ///
519 /// An anchor on the replaced value stays, on the new value, so aliases
520 /// to it still resolve: `b: &x 2` set to `6` gives `b: &x 6`. A node
521 /// value that has an anchor of its own keeps it only if the old value
522 /// had none. The first alias to an anchor deeper in the old value
523 /// becomes a copy of the anchored node: `b: {c: &y 1}` and `d: *y` give
524 /// `b: 6` and `d: &y 1`.
525 ///
526 /// ```
527 /// use std::str::FromStr;
528 /// use yaml_edit::Document;
529 ///
530 /// let doc = Document::from_str(
531 /// "shared: &shared\n timeout: 30\n retries: 3\nservice_a: old\n",
532 /// )
533 /// .unwrap();
534 /// let mapping = doc.as_mapping().unwrap();
535 /// let merge = Document::from_str("<<: *shared\ntimeout: 60\n").unwrap();
536 /// mapping.set("service_a", merge.as_mapping().unwrap());
537 /// assert_eq!(
538 /// doc.to_string(),
539 /// "shared: &shared\n timeout: 30\n retries: 3\nservice_a:\n <<: *shared\n timeout: 60\n"
540 /// );
541 /// ```
542 pub fn set(&self, key: impl crate::AsYaml, value: impl crate::AsYaml) {
543 // An existing entry with this key takes the value in place.
544 let Some(entry) = self.find_entry_by_key(&key) else {
545 self.insert_new(Slot::End, key, value);
546 return;
547 };
548 self.styled_value(&value, |v, flow_context| entry.set_value(v, flow_context));
549 }
550
551 /// Detect if this mapping uses explicit key indicators (?)
552 pub(super) fn uses_explicit_keys(&self) -> bool {
553 // Only block explicit keys count, whose `?` is a child of
554 // MAPPING_ENTRY. In flow context the parser puts it inside KEY, and
555 // new entries there are written implicit.
556 for child in self.0.children() {
557 if child.kind() == SyntaxKind::MAPPING_ENTRY {
558 // Check if this entry has a QUESTION token as a child
559 if has_child_token(&child, |k| k == SyntaxKind::QUESTION) {
560 return true;
561 }
562 }
563 }
564 false
565 }
566
567 /// Hand `put` `value` as an entry of this mapping holds it, in flow form
568 /// in a flow mapping, and whether the mapping is flow. The flow form is
569 /// shifted to the entry's line when put in, so its continuation lines
570 /// are given relative to that.
571 fn styled_value(&self, value: &impl crate::AsYaml, put: impl FnOnce(&dyn crate::AsYaml, bool)) {
572 let flow_context = self.is_flow_style();
573 let flow = flow_context
574 .then(|| crate::nodes::flow_quoting(&self.0))
575 .and_then(|q| flow_form(value, 2, q));
576 match &flow {
577 Some(flow) => put(flow, flow_context),
578 None => put(value, flow_context),
579 }
580 }
581
582 /// Add an entry for `key`, which is not in this mapping yet, at `slot`,
583 /// written in the mapping's style.
584 fn insert_new(&self, slot: Slot, key: impl crate::AsYaml, value: impl crate::AsYaml) {
585 // A placeholder from `Mapping::new_pending_block` turns into a real
586 // block mapping now that it has an entry to hold, before the entry
587 // is laid out.
588 empty_collection::convert_placeholder_to_block(&self.0);
589 let flow = self
590 .is_flow_style()
591 .then(|| crate::nodes::flow_quoting(&self.0));
592 // The column of the key tells the constructor how far to shift a
593 // node-backed block value.
594 let new_entry = MappingEntry::new_in(
595 key,
596 value,
597 flow,
598 self.uses_explicit_keys(),
599 self.detect_indentation_level(),
600 );
601 self.attach(&new_entry.0, slot);
602 }
603
604 /// Splice `new_entry` into this mapping at `slot`.
605 pub(super) fn attach(&self, new_entry: &SyntaxNode, slot: Slot) {
606 if self.is_flow_style() {
607 self.brace_flow_pair();
608 trivia::attach_flow_entry(&self.0, slot, new_entry);
609 } else {
610 trivia::attach_entry(&self.0, slot, new_entry);
611 }
612 }
613
614 /// Set a key-value pair with field ordering support
615 /// If the key exists, updates its value. If the key doesn't exist, inserts it
616 /// at the correct position based on the provided field order.
617 /// Fields not in the order list are placed at the end.
618 pub fn set_with_field_order<I, K>(
619 &self,
620 key: impl crate::AsYaml,
621 value: impl crate::AsYaml,
622 field_order: I,
623 ) where
624 I: IntoIterator<Item = K>,
625 K: crate::AsYaml,
626 {
627 // Collect field_order so we can iterate it multiple times.
628 let field_order: Vec<K> = field_order.into_iter().collect();
629
630 // An existing key just takes the new value, wherever it sits.
631 if self.find_entry_by_key(&key).is_some() {
632 self.set(&key, &value);
633 return;
634 }
635
636 let Some(key_index) = field_order
637 .iter()
638 .position(|field| crate::as_yaml::yaml_eq(&key, field))
639 else {
640 self.insert_new(Slot::End, key, value);
641 return;
642 };
643 // The nearest preceding field that is present decides where the new
644 // entry goes. With none, it goes before the first present field that
645 // the order puts after it, and with no ordered key present at the end.
646 let before_later = || {
647 self.entries().find_map(|entry| {
648 let key_node = entry.key()?;
649 let pos = field_order
650 .iter()
651 .position(|field| key_content_matches(&key_node, field))?;
652 (pos > key_index).then_some(Slot::Before(entry.0))
653 })
654 };
655 let slot = field_order[..key_index]
656 .iter()
657 .rev()
658 .find_map(|field| self.find_entry_by_key(field))
659 .map(|entry| Slot::After(entry.0))
660 .or_else(before_later)
661 .unwrap_or(Slot::End);
662 self.insert_new(slot, key, value);
663 }
664
665 /// Detect the indentation level (in spaces) used by entries in this mapping.
666 ///
667 /// Returns `0` for root-level mappings where entries have no leading indentation.
668 pub fn detect_indentation_level(&self) -> usize {
669 trivia::entry_column(&self.0)
670 }
671
672 /// Insert a key-value pair at a specific index (0-based), preserving formatting.
673 ///
674 /// If `new_key` already exists in the mapping, the existing entry is replaced
675 /// with a newly built entry at the **same position** (the `index` argument is
676 /// ignored). Surrounding whitespace in the file is preserved, but the entry
677 /// node itself is rebuilt (comments attached to the old entry may be lost).
678 /// An anchor on the old value stays on the new one, as with
679 /// [`set`](Self::set).
680 /// If `index` is out of bounds, the entry is appended at the end.
681 pub fn insert_at_index_preserving(
682 &self,
683 index: usize,
684 new_key: impl crate::AsYaml,
685 new_value: impl crate::AsYaml,
686 ) {
687 match self.find_entry_by_key(&new_key) {
688 Some(old) => {
689 self.replace_entry(&old.0, new_key, new_value, true);
690 }
691 None => self.insert_new_entry_at(index, new_key, new_value),
692 }
693 }
694
695 /// Replace `old`, an entry of this mapping, with a new entry built the
696 /// way `set` builds one, in the same place, and return it. If
697 /// `keep_anchor` is set, an anchor on the old value goes on the new one
698 /// unless that is an alias, as with `set`; otherwise the old value goes
699 /// as with `remove`.
700 pub(super) fn replace_entry(
701 &self,
702 old: &SyntaxNode,
703 key: impl crate::AsYaml,
704 value: impl crate::AsYaml,
705 keep_anchor: bool,
706 ) -> SyntaxNode {
707 let kept = MappingEntry::cast(old.clone())
708 .map(|e| e.value())
709 .filter(|_| keep_anchor)
710 .and_then(|v| kept_anchor_token(&v, &value));
711 let anchor = kept.as_ref().map(|t| t.text().to_string());
712 let mut new = None;
713 crate::nodes::kept_aliases::keeping_aliases(old, kept, || {
714 // The entry is built without the value's anchor; putting the
715 // value in again settles which anchor it gets, as in set_value.
716 let entry = MappingEntry(self.splice_in_entry(old, key, &value));
717 self.put_value(&entry, &value, anchor.as_deref());
718 new = Some(entry.0.clone());
719 Some(entry.value())
720 });
721 new.expect("the edit ran")
722 }
723
724 /// Replace the value of `entry`, an entry of this mapping, with `value`
725 /// under `anchor`, as `set` does.
726 pub(super) fn put_value(
727 &self,
728 entry: &MappingEntry,
729 value: impl crate::AsYaml,
730 anchor: Option<&str>,
731 ) {
732 self.styled_value(&value, |v, flow_context| {
733 entry.replace_value_under(v, flow_context, anchor)
734 });
735 }
736
737 /// The edit of [`replace_entry`](Self::replace_entry), without the
738 /// kept anchor. Returns the new entry.
739 fn splice_in_entry(
740 &self,
741 old: &SyntaxNode,
742 key: impl crate::AsYaml,
743 value: impl crate::AsYaml,
744 ) -> SyntaxNode {
745 let flow_context = self.is_flow_style();
746 let new_entry = MappingEntry::new_in(
747 key,
748 value,
749 flow_context.then(|| crate::nodes::flow_quoting(&self.0)),
750 self.uses_explicit_keys(),
751 self.detect_indentation_level(),
752 )
753 .0;
754 if flow_context {
755 // The separator to the next entry lives in the entry before it,
756 // as does the line break ending its line.
757 let mut separator: Vec<_> = old
758 .children_with_tokens()
759 .skip_while(|c| c.kind() != SyntaxKind::COMMA)
760 .collect();
761 if separator.is_empty() {
762 separator.extend(
763 old.last_child_or_token()
764 .filter(|c| c.kind() == SyntaxKind::NEWLINE),
765 );
766 }
767 for c in &separator {
768 c.detach();
769 }
770 crate::nodes::append_children(&new_entry, separator);
771 } else {
772 // The new entry brings its own line break, unless the old one
773 // ended the input without one.
774 if !trivia::ends_with_line_break(old) {
775 let last = new_entry
776 .last_child_or_token()
777 .filter(|c| c.kind() == SyntaxKind::NEWLINE);
778 if let Some(last) = last {
779 last.detach();
780 }
781 }
782 }
783 let i = old.index();
784 replace_children(&self.0, i..i + 1, vec![new_entry.clone().into()]);
785 new_entry
786 }
787
788 /// Remove a key-value pair, returning the removed entry.
789 ///
790 /// Returns `Some(entry)` if the key existed and was removed, or `None` if
791 /// the key was not found. The returned [`MappingEntry`] is detached from
792 /// the tree; callers can inspect its key and value or re-insert it
793 /// elsewhere.
794 ///
795 /// The first alias to an anchor in the removed entry becomes a copy of
796 /// the anchored node, so it and later aliases keep their value.
797 ///
798 /// Mutates in place despite `&self` (see crate docs on interior mutability).
799 pub fn remove(&self, key: impl crate::AsYaml) -> Option<MappingEntry> {
800 let entry = self.entries().find(|e| e.key_matches(&key))?;
801 self.remove_entry(entry.syntax());
802 Some(entry)
803 }
804
805 /// Splice `removed`, an entry of this mapping, out of it with the
806 /// trivia that belongs to it.
807 fn remove_entry(&self, removed: &SyntaxNode) {
808 // Without its pair, a flow pair would leave `[]`, an empty sequence.
809 if is_flow_pair(&self.0) {
810 self.brace_flow_pair();
811 }
812 crate::nodes::kept_aliases::keeping_aliases(removed, None, || {
813 trivia::detach_entry(&self.0, removed);
814 None
815 });
816 }
817
818 /// Remove the nth occurrence of a key, returning the removed entry.
819 ///
820 /// Returns `Some(entry)` if the nth occurrence exists and was removed,
821 /// or `None` if there are fewer than `n+1` occurrences of the key.
822 /// The index `n` is 0-based (n=0 removes the first occurrence, n=1 removes
823 /// the second, etc.).
824 ///
825 /// This is useful for handling duplicate keys in YAML. While duplicate keys
826 /// are semantically ambiguous, they are allowed by the YAML spec, and this
827 /// method provides fine-grained control over which occurrence to remove.
828 ///
829 /// # Example
830 ///
831 /// ```rust
832 /// # use std::str::FromStr;
833 /// # use yaml_edit::Document;
834 /// let yaml = r#"
835 /// Reference: First
836 /// Reference: Second
837 /// Reference: Third
838 /// "#;
839 ///
840 /// let doc = Document::from_str(yaml).unwrap();
841 /// let mapping = doc.as_mapping().unwrap();
842 ///
843 /// // Remove the second occurrence (index 1)
844 /// let removed = mapping.remove_nth_occurrence("Reference", 1);
845 /// assert!(removed.is_some());
846 ///
847 /// // Now only two Reference entries remain
848 /// let count = mapping.find_all_entries_by_key("Reference").count();
849 /// assert_eq!(count, 2);
850 /// ```
851 ///
852 /// Mutates in place despite `&self` (see crate docs on interior mutability).
853 pub fn remove_nth_occurrence(&self, key: impl crate::AsYaml, n: usize) -> Option<MappingEntry> {
854 let entry = self.entries().filter(|e| e.key_matches(&key)).nth(n)?;
855 self.remove_entry(entry.syntax());
856 Some(entry)
857 }
858
859 /// Remove all key-value pairs from this mapping.
860 ///
861 /// Mutates in place despite `&self` (see crate docs on interior mutability).
862 pub fn clear(&self) {
863 let line_break = self
864 .entries()
865 .last()
866 .and_then(|e| trivia::line_break(e.syntax()));
867 // Entries are removed one by one, as keeping aliases resolving may
868 // rewrite a later key.
869 while let Some(entry) = self.entries().next() {
870 self.remove_entry(entry.syntax());
871 }
872 render_empty(&self.0, line_break.as_deref());
873 }
874
875 /// Rename a key while preserving its value and formatting.
876 ///
877 /// The new key is built using the same `AsYaml` infrastructure as other
878 /// write methods, so quoting and escaping are handled automatically.
879 /// Returns `true` if the key was found and renamed, `false` if `old_key`
880 /// does not exist.
881 ///
882 /// If `new_key` differs from `old_key` and is already present, the
883 /// existing `new_key` entry is removed, as with [`set`](Self::set) and
884 /// [`move_after`](Self::move_after), so the rename does not create a
885 /// duplicate key. The renamed entry keeps its position, value and
886 /// comments.
887 ///
888 /// The key keeps its anchor, so aliases to it still resolve; an anchor
889 /// on a `new_key` node is used only if the old key had none. The key's
890 /// tag is kept if `new_key` has no tag of its own and still means the
891 /// same with it: `!!str a` renamed to `"b"` gives `!!str b`, but renamed
892 /// to `1` gives `1`, as `!!str 1` would be a string. The first alias to
893 /// an anchor deeper in the old key becomes a copy of the anchored node.
894 ///
895 /// ```
896 /// # use std::str::FromStr;
897 /// # use yaml_edit::Document;
898 /// let doc = Document::from_str("a: 1\nb: 2\nc: 3\n").unwrap();
899 /// assert!(doc.as_mapping().unwrap().rename_key("a", "b"));
900 /// assert_eq!(doc.to_string(), "b: 1\nc: 3\n");
901 /// ```
902 ///
903 /// Mutates in place despite `&self` (see crate docs on interior mutability).
904 pub fn rename_key(&self, old_key: impl crate::AsYaml, new_key: impl crate::AsYaml) -> bool {
905 let Some(node) = self.find_entry_by_key(&old_key).map(|e| e.0) else {
906 return false;
907 };
908
909 let renames_to_itself = entry_key(&node).is_some_and(|k| key_content_matches(&k, &new_key));
910 if !renames_to_itself {
911 self.remove_other_entries(&node, &new_key);
912 }
913 let old = entry_key(&node).expect("a mapping entry has a key");
914 let kept = held_anchor_token(&old);
915 crate::nodes::kept_aliases::keeping_aliases(&old, kept, || {
916 Some(self.replace_key(&node, new_key, None))
917 });
918 true
919 }
920
921 /// Replace the key of `node`, an entry of this mapping, with `new_key`
922 /// under `anchor`, and return the new KEY.
923 pub(crate) fn replace_key(
924 &self,
925 node: &SyntaxNode,
926 new_key: impl crate::AsYaml,
927 anchor: Option<&str>,
928 ) -> SyntaxNode {
929 if let Some(key) = flow_form(&new_key, 2, crate::nodes::flow_quoting(&self.0)) {
930 return self.replace_key(node, key, anchor);
931 }
932 let indent = self.detect_indentation_level();
933 // A key spanning lines has to be explicit, with the `:` on a line
934 // of its own.
935 let make_explicit = !self.is_flow_style()
936 && !has_child_token(node, |k| k == SyntaxKind::QUESTION)
937 && super::key_spans_lines(&new_key);
938 let old_key = entry_key(node).expect("a mapping entry has a key");
939 let mut builder = GreenNodeBuilder::new();
940 builder.start_node(SyntaxKind::ROOT.into());
941 if make_explicit {
942 builder.token(SyntaxKind::QUESTION.into(), "?");
943 builder.token(SyntaxKind::WHITESPACE.into(), " ");
944 }
945 let flow = self
946 .is_flow_style()
947 .then(|| crate::nodes::flow_quoting(&self.0));
948 Self::rebuild_key(&mut builder, &old_key, &new_key, anchor, flow);
949 if make_explicit {
950 builder.token(SyntaxKind::NEWLINE.into(), "\n");
951 if indent > 0 {
952 builder.token(SyntaxKind::INDENT.into(), &" ".repeat(indent));
953 }
954 }
955 builder.finish_node();
956 let built = SyntaxNode::new_root_mut(builder.finish());
957 let new_key = built
958 .children()
959 .find(|n| n.kind() == SyntaxKind::KEY)
960 .expect("a KEY was just built");
961 if anchor.is_some() {
962 drop_tagged_anchor(&new_key);
963 }
964
965 // Whitespace between the key and the `:` would now start the `:`
966 // line. Detached one by one, as a multi-child splice can skip some.
967 if make_explicit {
968 let stale: Vec<_> = old_key
969 .siblings_with_tokens(rowan::Direction::Next)
970 .skip(1)
971 .map_while(|c| {
972 c.into_token()
973 .filter(|t| t.kind() == SyntaxKind::WHITESPACE)
974 })
975 .collect();
976 for token in stale {
977 token.detach();
978 }
979 }
980 let old_ended_line = trivia::ends_with_line_break(&old_key);
981 let i = old_key.index();
982 replace_children(
983 node,
984 i..i + 1,
985 built.children_with_tokens().collect::<Vec<_>>(),
986 );
987 if has_child_token(node, |k| k == SyntaxKind::QUESTION) {
988 Self::settle_explicit_key_line(&new_key, old_ended_line);
989 }
990 if self.is_flow_style() {
991 Self::separate_adjacent_value(&new_key);
992 }
993 crate::nodes::separate_key_from_colon(node);
994 new_key
995 }
996
997 /// Put a space after the `:` following `key` if the value starts right
998 /// after it and `key` is not JSON-like: only after a quoted scalar or a
999 /// flow collection does `"a":b` read as a key and a value, while `a:b`
1000 /// is one plain scalar.
1001 fn separate_adjacent_value(key: &SyntaxNode) {
1002 let trivia = |k: SyntaxKind| {
1003 matches!(
1004 k,
1005 SyntaxKind::WHITESPACE
1006 | SyntaxKind::INDENT
1007 | SyntaxKind::NEWLINE
1008 | SyntaxKind::COMMENT
1009 )
1010 };
1011 // The `:` may be on a later line, after a comment.
1012 let Some(colon) = key
1013 .siblings_with_tokens(rowan::Direction::Next)
1014 .skip(1)
1015 .find(|c| !trivia(c.kind()))
1016 .and_then(|c| c.into_token())
1017 .filter(|t| t.kind() == SyntaxKind::COLON)
1018 else {
1019 return;
1020 };
1021 let json_like = key
1022 .descendants_with_tokens()
1023 .filter_map(|c| c.into_token())
1024 .filter(|t| !t.text().is_empty() && !trivia(t.kind()))
1025 .last()
1026 .is_some_and(|t| {
1027 matches!(
1028 t.kind(),
1029 SyntaxKind::RIGHT_BRACKET | SyntaxKind::RIGHT_BRACE
1030 ) || (t.kind() == SyntaxKind::STRING && t.text().starts_with(['"', '\'']))
1031 });
1032 let value_adjacent = std::iter::successors(colon.next_token(), |t| t.next_token())
1033 .find_map(|t| t.text().chars().next())
1034 .is_some_and(|c| !c.is_whitespace() && !matches!(c, ',' | ']' | '}'));
1035 if !json_like && value_adjacent {
1036 let i = colon.index() + 1;
1037 insert_children(
1038 &colon.parent().expect("a COLON is in an entry"),
1039 i,
1040 vec![fresh_token(SyntaxKind::WHITESPACE, " ").into()],
1041 );
1042 }
1043 }
1044
1045 /// Fix up the line of a block explicit key whose content was replaced.
1046 ///
1047 /// A block key (`? - a\n`) ends its own line, while the entry holds the
1048 /// line break after an inline one (`? a` NEWLINE). An empty key may
1049 /// follow the `?` directly, a non-empty one needs a space.
1050 fn settle_explicit_key_line(key: &SyntaxNode, old_ended_line: bool) {
1051 let entry = key.parent().expect("a KEY is in an entry");
1052 let before = key.prev_sibling_or_token();
1053 if before
1054 .as_ref()
1055 .is_some_and(|c| c.kind() == SyntaxKind::QUESTION)
1056 && !key.text().is_empty()
1057 {
1058 let i = key.index();
1059 insert_children(
1060 &entry,
1061 i,
1062 vec![fresh_token(SyntaxKind::WHITESPACE, " ").into()],
1063 );
1064 }
1065 // Without a `:` line, the key's line is the entry's.
1066 let has_colon = has_child_token(&entry, |k| k == SyntaxKind::COLON);
1067 let ends_line = trivia::ends_with_line_break(key);
1068 let newline_after = if has_colon {
1069 key.next_sibling_or_token()
1070 } else {
1071 entry.last_child_or_token()
1072 }
1073 .filter(|c| c.kind() == SyntaxKind::NEWLINE);
1074 if ends_line && !old_ended_line {
1075 if let Some(newline) = newline_after {
1076 newline.detach();
1077 }
1078 } else if old_ended_line && !ends_line {
1079 if has_colon {
1080 insert_children(
1081 &entry,
1082 key.index() + 1,
1083 vec![fresh_token(SyntaxKind::NEWLINE, "\n").into()],
1084 );
1085 } else {
1086 trivia::terminate(&entry);
1087 }
1088 }
1089 }
1090
1091 /// Remove every entry other than `keep` whose key matches `key`.
1092 fn remove_other_entries(&self, keep: &SyntaxNode, key: &impl crate::AsYaml) {
1093 while let Some(other) = self.entries().find(|e| e.0 != *keep && e.key_matches(key)) {
1094 self.remove_entry(other.syntax());
1095 }
1096 }
1097
1098 /// Rebuild an entry's KEY node around `new_key`, keeping the layout
1099 /// tokens the old one carried.
1100 ///
1101 /// In a flow mapping the `?` sits inside KEY, so it carries over, as do
1102 /// any tokens after the key's content. The line break before block
1103 /// content (`?\n- a`) goes, as the new key is written inline, and the
1104 /// one after an empty key (`?\n}`) follows the new key.
1105 ///
1106 /// A comment before the old content stays on its line, or follows the
1107 /// new KEY if the content was on that line too.
1108 ///
1109 /// The old key's anchor stays; failing that the key takes `anchor`, or
1110 /// the anchor before a `new_key` node where it was copied from.
1111 fn rebuild_key(
1112 builder: &mut GreenNodeBuilder,
1113 old: &SyntaxNode,
1114 new_key: &impl crate::AsYaml,
1115 anchor: Option<&str>,
1116 flow: Option<crate::as_yaml::FlowQuoting>,
1117 ) {
1118 builder.start_node(SyntaxKind::KEY.into());
1119 let indent = old
1120 .parent()
1121 .map_or(0, |entry| crate::column::node_column(&entry));
1122 let leading: Vec<_> = old
1123 .children_with_tokens()
1124 .map_while(|c| c.into_token())
1125 .collect();
1126 let mut rest = &leading[..];
1127 if let Some((question, after)) = rest
1128 .split_first()
1129 .filter(|(t, _)| t.kind() == SyntaxKind::QUESTION)
1130 {
1131 builder.token(question.kind().into(), question.text());
1132 let ws = after
1133 .iter()
1134 .take_while(|t| t.kind() == SyntaxKind::WHITESPACE)
1135 .count();
1136 for t in &after[..ws] {
1137 builder.token(t.kind().into(), t.text());
1138 }
1139 if ws == 0 {
1140 builder.token(SyntaxKind::WHITESPACE.into(), " ");
1141 }
1142 rest = &after[ws..];
1143 }
1144 // A comment on the `?` line stays there if the content starts on a
1145 // later line, and otherwise goes after the new key.
1146 let comment = rest.iter().position(|t| t.kind() == SyntaxKind::COMMENT);
1147 let comment_lines = comment
1148 .filter(|&i| rest[i..].iter().any(|t| t.kind() == SyntaxKind::NEWLINE))
1149 .map(|i| &rest[i..]);
1150 let comment_after = comment
1151 .filter(|_| comment_lines.is_none())
1152 .map(|i| rest[i].text().to_string());
1153 let (old_anchor, tag) = key_annotations(old);
1154 let anchor = old_anchor
1155 .or_else(|| anchor.map(str::to_string))
1156 .or_else(|| new_key.value_node().and_then(|n| source_anchor_text(&n)));
1157 if let Some(anchor) = anchor {
1158 builder.token(SyntaxKind::ANCHOR.into(), &anchor);
1159 builder.token(SyntaxKind::WHITESPACE.into(), " ");
1160 }
1161 // On a line of its own, only a block sequence may start at the
1162 // column of the `?`; anything else has to be indented past it.
1163 let mut lines = comment_lines.unwrap_or_default();
1164 let line_indent = match lines.last() {
1165 Some(t) if t.kind() == SyntaxKind::INDENT => {
1166 lines = &lines[..lines.len() - 1];
1167 Some(t.text())
1168 }
1169 _ => None,
1170 };
1171 let block_sequence =
1172 new_key.kind() == crate::as_yaml::YamlKind::Sequence && !new_key.is_inline();
1173 let deeper = " ".repeat(indent + 2);
1174 let line_indent = match line_indent {
1175 Some(text) if block_sequence || text.len() > indent => Some(text),
1176 _ if comment_lines.is_some() && !block_sequence => Some(deeper.as_str()),
1177 other => other,
1178 };
1179 for t in lines {
1180 builder.token(t.kind().into(), t.text());
1181 }
1182 if let Some(text) = line_indent {
1183 builder.token(SyntaxKind::INDENT.into(), text);
1184 }
1185 match tag.and_then(|tag| retagged_key(&tag, new_key, flow)) {
1186 Some(tagged) => super::build_key_content(builder, &tagged, indent, flow),
1187 None => super::build_key_content(builder, new_key, indent, flow),
1188 }
1189 // What follows the old key's last node, such as the line break
1190 // ending an explicit key, stays after the new one.
1191 let last_node = old.last_child().expect("a KEY holds a node");
1192 for trailing in last_node
1193 .siblings_with_tokens(rowan::Direction::Next)
1194 .skip(1)
1195 .filter_map(|c| c.into_token())
1196 {
1197 builder.token(trailing.kind().into(), trailing.text());
1198 }
1199 builder.finish_node();
1200 if let Some(comment) = comment_after {
1201 builder.token(SyntaxKind::WHITESPACE.into(), " ");
1202 builder.token(SyntaxKind::COMMENT.into(), &comment);
1203 }
1204 // The line break after an empty key now follows the new one.
1205 let old_empty = old.children().next().is_some_and(|n| n.text().is_empty());
1206 if old_empty && comment.is_none() {
1207 for t in rest
1208 .iter()
1209 .filter(|t| matches!(t.kind(), SyntaxKind::NEWLINE | SyntaxKind::INDENT))
1210 {
1211 builder.token(t.kind().into(), t.text());
1212 }
1213 }
1214 }
1215
1216 /// Insert a key-value pair immediately after an existing key.
1217 ///
1218 /// If `key` already exists in the mapping, its value is updated in-place and
1219 /// it remains at its current position (it is **not** moved to after `after_key`).
1220 /// Returns `true` in both the update and the insert cases.
1221 /// Returns `false` only if `after_key` is not found.
1222 ///
1223 /// Use [`move_after`](Self::move_after) if you want
1224 /// an existing entry to be moved to the new position.
1225 ///
1226 /// Mutates in place despite `&self` (see crate docs on interior mutability).
1227 pub fn insert_after(
1228 &self,
1229 after_key: impl crate::AsYaml,
1230 key: impl crate::AsYaml,
1231 value: impl crate::AsYaml,
1232 ) -> bool {
1233 self.insert_around(after_key, key, value, true)
1234 }
1235
1236 /// Insert a key-value pair immediately before an existing key.
1237 ///
1238 /// If `key` already exists in the mapping, its value is updated in-place and
1239 /// it remains at its current position (it is **not** moved to before `before_key`).
1240 /// Returns `true` in both the update and the insert cases.
1241 /// Returns `false` only if `before_key` is not found.
1242 ///
1243 /// Use [`move_before`](Self::move_before) if you want
1244 /// an existing entry to be moved to the new position.
1245 ///
1246 /// Mutates in place despite `&self` (see crate docs on interior mutability).
1247 pub fn insert_before(
1248 &self,
1249 before_key: impl crate::AsYaml,
1250 key: impl crate::AsYaml,
1251 value: impl crate::AsYaml,
1252 ) -> bool {
1253 self.insert_around(before_key, key, value, false)
1254 }
1255
1256 /// Shared body of [`insert_after`](Self::insert_after) and
1257 /// [`insert_before`](Self::insert_before): an existing `key` is updated
1258 /// where it is, a new one goes next to `ref_key`.
1259 fn insert_around(
1260 &self,
1261 ref_key: impl crate::AsYaml,
1262 key: impl crate::AsYaml,
1263 value: impl crate::AsYaml,
1264 after: bool,
1265 ) -> bool {
1266 if self.find_entry_by_key(&key).is_some() {
1267 self.set(&key, &value);
1268 return self.find_entry_by_key(&ref_key).is_some();
1269 }
1270 self.move_around(&ref_key, &key, &value, after)
1271 }
1272
1273 /// Insert a key-value pair at a specific index (0-based).
1274 ///
1275 /// If `key` already exists in the mapping, its value is updated in-place and
1276 /// it remains at its current position (the `index` argument is ignored).
1277 /// If `index` is out of bounds, the entry is appended at the end.
1278 /// This method always succeeds; it never returns an error.
1279 ///
1280 /// Mutates in place despite `&self` (see crate docs on interior mutability).
1281 pub fn insert_at_index(
1282 &self,
1283 index: usize,
1284 key: impl crate::AsYaml,
1285 value: impl crate::AsYaml,
1286 ) {
1287 // Check if the key already exists - if so, just update it
1288 if self.find_entry_by_key(&key).is_some() {
1289 self.set(&key, &value);
1290 return;
1291 }
1292 self.insert_new_entry_at(index, key, value);
1293 }
1294
1295 /// Insert an entry for a key that is not in the mapping yet before the
1296 /// `index`th entry, or at the end if there are fewer.
1297 fn insert_new_entry_at(
1298 &self,
1299 index: usize,
1300 key: impl crate::AsYaml,
1301 value: impl crate::AsYaml,
1302 ) {
1303 let slot = self
1304 .entries()
1305 .nth(index)
1306 .map_or(Slot::End, |e| Slot::Before(e.0));
1307 self.insert_new(slot, key, value);
1308 }
1309
1310 crate::nodes::ast_node_spans!("mapping");
1311}
1312
1313impl Default for Mapping {
1314 fn default() -> Self {
1315 Self::new()
1316 }
1317}
1318
1319// Iterator trait implementations for Mapping
1320
1321impl<'a> IntoIterator for &'a Mapping {
1322 type Item = (crate::as_yaml::YamlNode, crate::as_yaml::YamlNode);
1323 type IntoIter =
1324 Box<dyn Iterator<Item = (crate::as_yaml::YamlNode, crate::as_yaml::YamlNode)> + 'a>;
1325
1326 fn into_iter(self) -> Self::IntoIter {
1327 Box::new(self.iter())
1328 }
1329}
1330
1331impl AsYaml for Mapping {
1332 fn as_node(&self) -> Option<&SyntaxNode> {
1333 Some(&self.0)
1334 }
1335
1336 fn kind(&self) -> YamlKind {
1337 YamlKind::Mapping
1338 }
1339
1340 fn build_content(
1341 &self,
1342 builder: &mut rowan::GreenNodeBuilder,
1343 indent: usize,
1344 _flow_context: bool,
1345 ) -> bool {
1346 let is_flow = self.is_flow_style();
1347 let mut delta =
1348 indent as isize - crate::as_yaml::source_reindent_base(&self.0, is_flow) as isize;
1349 if is_flow {
1350 delta = crate::as_yaml::clear_parent_column(&self.0, indent, delta);
1351 }
1352 builder.start_node(SyntaxKind::MAPPING.into());
1353 crate::as_yaml::copy_node_content_reindent(builder, &self.0, delta);
1354 builder.finish_node();
1355 self.0
1356 .last_token()
1357 .is_some_and(|t| t.kind() == SyntaxKind::NEWLINE)
1358 }
1359
1360 fn is_inline(&self) -> bool {
1361 ValueNode::is_inline(self)
1362 }
1363}