Skip to main content

yaml_edit/nodes/mapping/
entry.rs

1//! `impl MappingEntry` -- the key/value/set_value/discard/remove API
2//! on a single MAPPING_ENTRY node, plus its private layout helpers
3//! (indent_of_line, insert_at, set_block_value, rebuild_with_inline_value).
4//!
5//! Split out of `nodes/mapping/mod.rs` unchanged.
6
7use super::helpers::{
8    build_block_value_node, built_block_node, drop_tagged_anchor, find_key_line_comment, flow_form,
9    has_comment, kept_anchor_token, source_anchor_text, value_is_block,
10};
11use super::{key_content_matches, MappingEntry};
12use crate::as_yaml::{build_in, FlowQuoting};
13use crate::lex::SyntaxKind;
14use crate::nodes::replaced_value::{add_to_first_line, insert_comment_line_above};
15use crate::nodes::{
16    entry_key, entry_value, fresh_token, has_child_token, insert_children, replace_children,
17    trivia, Lang, SyntaxNode,
18};
19use rowan::GreenNodeBuilder;
20
21impl MappingEntry {
22    /// Return the raw `KEY` wrapper node of this entry.
23    ///
24    /// The returned node has kind `KEY` and wraps the actual key content
25    /// (a scalar, mapping, or sequence node). Returns `None` for malformed
26    /// entries that have no key node.
27    ///
28    /// To compare the key against a value, prefer [`key_matches`](Self::key_matches).
29    pub(crate) fn key(&self) -> Option<SyntaxNode> {
30        entry_key(&self.0)
31    }
32
33    /// Return `true` if the key of this entry matches `key`.
34    ///
35    /// Uses semantic YAML equality, so quoting style differences are ignored:
36    /// `"foo"`, `'foo'`, and `foo` all match the scalar `"foo"`. Returns
37    /// `false` if this entry has no key node.
38    pub fn key_matches(&self, key: impl crate::AsYaml) -> bool {
39        self.key().is_some_and(|k| key_content_matches(&k, key))
40    }
41
42    /// Return the raw `VALUE` wrapper node of this entry.
43    ///
44    /// The returned node has kind `VALUE` and wraps the actual value content
45    /// (a scalar, mapping, or sequence node). The parser gives every entry
46    /// one, an implicit null where the value is omitted.
47    pub(crate) fn value(&self) -> SyntaxNode {
48        entry_value(&self.0).expect("MAPPING_ENTRY without a VALUE")
49    }
50
51    /// Get the key of this entry as a [`YamlNode`](crate::as_yaml::YamlNode).
52    ///
53    /// Returns `None` for malformed entries that have no key.
54    pub fn key_node(&self) -> Option<crate::as_yaml::YamlNode> {
55        self.key().and_then(|k| crate::nodes::held_node(&k))
56    }
57
58    /// Get the value of this entry as a [`YamlNode`](crate::as_yaml::YamlNode).
59    ///
60    /// Returns `None` for malformed entries whose value holds no node.
61    pub fn value_node(&self) -> Option<crate::as_yaml::YamlNode> {
62        crate::nodes::held_node(&self.value())
63    }
64
65    /// Create a new mapping entry (key-value pair) not yet attached to any mapping.
66    ///
67    /// The entry is built as a standalone CST node; attach it to a mapping with
68    /// one of the `insert_*` methods. Block-style values (mappings, sequences)
69    /// are indented with 2 spaces relative to the key.
70    ///
71    /// This builds the entry assuming the key will sit at column 0. If the
72    /// entry will be inserted into a mapping at a deeper column, use
73    /// [`new_at_indent`](Self::new_at_indent) instead so a node-backed block
74    /// value's interior indentation lines up with the target column.
75    pub fn new(
76        key: impl crate::AsYaml,
77        value: impl crate::AsYaml,
78        flow_context: bool,
79        use_explicit_key: bool,
80    ) -> Self {
81        Self::new_at_indent(key, value, flow_context, use_explicit_key, 0)
82    }
83
84    /// Like [`new`](Self::new) but builds the entry as if its key were
85    /// placed at column `key_indent`. Only affects how a node-backed block
86    /// collection value is re-indented; other value kinds render the same
87    /// as with [`new`](Self::new).
88    pub fn new_at_indent(
89        key: impl crate::AsYaml,
90        value: impl crate::AsYaml,
91        flow_context: bool,
92        use_explicit_key: bool,
93        key_indent: usize,
94    ) -> Self {
95        Self::new_in(
96            key,
97            value,
98            flow_context.then_some(FlowQuoting::Minimal),
99            use_explicit_key,
100            key_indent,
101        )
102    }
103
104    /// Like [`new_at_indent`](Self::new_at_indent), for a flow mapping
105    /// quoting strings as `flow` says if it is set.
106    ///
107    /// A node-backed key preceded by an `&anchor` in its parent keeps that
108    /// anchor, so aliases to it still resolve.
109    pub(crate) fn new_in(
110        key: impl crate::AsYaml,
111        value: impl crate::AsYaml,
112        flow: Option<FlowQuoting>,
113        use_explicit_key: bool,
114        key_indent: usize,
115    ) -> Self {
116        let key_anchor = key.value_node().and_then(|n| source_anchor_text(&n));
117        Self::build(
118            key,
119            value,
120            flow,
121            use_explicit_key,
122            key_indent,
123            key_anchor.as_deref(),
124        )
125    }
126
127    /// Build an entry as [`new_in`](Self::new_in) does, putting
128    /// `key_anchor` on the key.
129    fn build(
130        key: impl crate::AsYaml,
131        value: impl crate::AsYaml,
132        flow: Option<FlowQuoting>,
133        use_explicit_key: bool,
134        key_indent: usize,
135        key_anchor: Option<&str>,
136    ) -> Self {
137        let flow_context = flow.is_some();
138        let quoting = flow.unwrap_or(FlowQuoting::Minimal);
139        // build_key_content shifts the flow form by key_indent, so its
140        // continuation lines are given relative to the key.
141        if let Some(key) = flow_form(&key, 2, quoting) {
142            // Comments make the key span several lines, which only an
143            // explicit key may do.
144            let multiline = has_comment(key.syntax());
145            let explicit = use_explicit_key || multiline;
146            return Self::build(key, value, flow, explicit, key_indent, key_anchor);
147        }
148        if !use_explicit_key && !flow_context && crate::nodes::key_spans_lines(&key) {
149            return Self::build(key, value, flow, true, key_indent, key_anchor);
150        }
151        // Built inline below, the flow form is shifted by key_indent, so its
152        // continuation lines are given relative to the key.
153        if let Some(value) = flow.and_then(|q| flow_form(&value, 2, q)) {
154            return Self::build(key, value, flow, use_explicit_key, key_indent, key_anchor);
155        }
156        let mut builder = GreenNodeBuilder::new();
157        builder.start_node(SyntaxKind::MAPPING_ENTRY.into());
158
159        // The parser puts the `?` beside KEY in a block mapping and inside
160        // it in a flow mapping.
161        let question = |builder: &mut GreenNodeBuilder| {
162            builder.token(SyntaxKind::QUESTION.into(), "?");
163            builder.token(SyntaxKind::WHITESPACE.into(), " ");
164        };
165        if use_explicit_key && !flow_context {
166            question(&mut builder);
167        }
168
169        builder.start_node(SyntaxKind::KEY.into());
170        if use_explicit_key && flow_context {
171            question(&mut builder);
172        }
173        if let Some(anchor) = key_anchor {
174            builder.token(SyntaxKind::ANCHOR.into(), anchor);
175            builder.token(SyntaxKind::WHITESPACE.into(), " ");
176        }
177        crate::nodes::build_key_content(&mut builder, &key, key_indent, flow);
178        let key_has_newline = false;
179        debug_assert!(!key_has_newline, "Keys should not end with newlines");
180        let colon_on_own_line = use_explicit_key && !flow_context;
181        builder.finish_node();
182
183        // The `:` goes on its own line at the entry's own column; the parser
184        // keeps that line break and indent as siblings of KEY. In a flow
185        // mapping the `:` simply follows the key.
186        if colon_on_own_line {
187            builder.token(SyntaxKind::NEWLINE.into(), "\n");
188            if key_indent > 0 {
189                builder.token(SyntaxKind::INDENT.into(), &" ".repeat(key_indent));
190            }
191        }
192
193        builder.token(SyntaxKind::COLON.into(), ":");
194
195        // Node-backed block collections and block scalars need the same
196        // re-indent treatment that MappingEntry::set_value applies when
197        // the source's own base indent doesn't already put it at the
198        // correct target column: put the value on the next line and shift
199        // its interior indentation to line up under this entry.
200        //
201        // When key_indent == 0 (root-level insertion), col-0 dashes render
202        // as valid compact-style block content, so we leave sequences with
203        // source_base == 0 alone to preserve existing formatting choices.
204        // A col-0 mapping there would read as siblings of the key, and for
205        // nested insertion (key_indent > 0) any col-0 content would be
206        // parsed as escaping the parent's scope, so we must re-indent.
207        let built = (!flow_context).then(|| built_block_node(&value)).flatten();
208        let block_source = value
209            .value_node()
210            .filter(|_| !flow_context && !value.is_inline())
211            .or(built)
212            .filter(|_| {
213                matches!(
214                    value.kind(),
215                    crate::as_yaml::YamlKind::Mapping
216                        | crate::as_yaml::YamlKind::Sequence
217                        | crate::as_yaml::YamlKind::Scalar
218                        | crate::as_yaml::YamlKind::Tagged(_)
219                )
220            })
221            // Route through build_block_value_node whenever indentation
222            // needs to shift, OR when the source is empty (so a future
223            // insertion has an INDENT hint to drop into rather than
224            // landing at column 0).
225            .filter(|src| {
226                key_indent > 0
227                    || src.kind() != SyntaxKind::SEQUENCE
228                    || crate::column::node_column(src) > 0
229                    || src.children().next().is_none()
230            });
231
232        let value_ends_with_newline = if let Some(source) = block_source {
233            let block = build_block_value_node(&source, key_indent + 2, None);
234            if block.on_key_line {
235                builder.token(SyntaxKind::WHITESPACE.into(), " ");
236            }
237            builder.start_node(block.node.kind().into());
238            crate::as_yaml::copy_node_content(&mut builder, &block.node);
239            builder.finish_node();
240            block.ends_with_newline
241        } else {
242            // For inline values, put the separator WHITESPACE at the
243            // MAPPING_ENTRY level (between COLON and VALUE), matching
244            // the parser convention. That way `set_value`'s targeted
245            // splice, which only replaces the VALUE node in place,
246            // preserves the space between key and new value.
247            let inline = value.is_inline();
248            if inline {
249                builder.token(SyntaxKind::WHITESPACE.into(), " ");
250            }
251            builder.start_node(SyntaxKind::VALUE.into());
252            let ends_with_newline = match (inline, value.kind()) {
253                (true, _) => {
254                    // TAGGED_NODE values (!!set, !!omap, !!pairs) are
255                    // inline but may end with newlines from their
256                    // block-style content. A multi-line flow value has its
257                    // continuation lines shifted to clear the key's column,
258                    // as does a block scalar's body.
259                    build_in(&value, &mut builder, key_indent, flow)
260                }
261                // Block mappings and sequences start on new line but don't get pre-indented
262                // They handle their own indentation via build_content
263                (false, crate::as_yaml::YamlKind::Mapping | crate::as_yaml::YamlKind::Sequence) => {
264                    builder.token(SyntaxKind::NEWLINE.into(), "\n");
265                    build_in(&value, &mut builder, 0, flow)
266                }
267                // Block scalars (literal/folded) get newline and indent
268                (false, _) => {
269                    builder.token(SyntaxKind::NEWLINE.into(), "\n");
270                    builder.token(SyntaxKind::INDENT.into(), "  ");
271                    build_in(&value, &mut builder, 2, flow)
272                }
273            };
274            builder.finish_node(); // VALUE
275            ends_with_newline
276        };
277
278        // Every block-style MAPPING_ENTRY ends with NEWLINE (newline ownership
279        // model). Flow-style entries live inside `{}` on a single line, so
280        // adding a NEWLINE there would break the container.
281        if !flow_context && !value_ends_with_newline {
282            builder.token(SyntaxKind::NEWLINE.into(), "\n");
283        }
284
285        builder.finish_node(); // MAPPING_ENTRY
286        let entry = SyntaxNode::new_root_mut(builder.finish());
287        crate::nodes::separate_key_from_colon(&entry);
288        MappingEntry(entry)
289    }
290
291    /// Column at which this entry's key sits on its line.
292    ///
293    /// Returns 0 for root-level entries or entries that share a line with
294    /// something else (an explicit-key form, a flow-style parent, etc.).
295    fn indent_of_line(&self) -> usize {
296        crate::column::node_column(&self.0)
297    }
298
299    /// Index of the entry's VALUE child in its own children list.
300    fn value_index(&self) -> usize {
301        self.value().index()
302    }
303
304    /// True if the entry's VALUE opens with a key-line comment, as the
305    /// parser lays out `key:  # note` followed by a block collection.
306    fn value_starts_with_comment(&self) -> bool {
307        self.value()
308            .first_token()
309            .is_some_and(|t| t.kind() == SyntaxKind::COMMENT)
310    }
311
312    /// Insert `tokens` at position `at` in `self.0`.
313    fn insert_at(&self, at: usize, tokens: Vec<rowan::SyntaxToken<Lang>>) {
314        let elems: Vec<_> = tokens.into_iter().map(Into::into).collect();
315        insert_children(&self.0, at, elems);
316    }
317
318    /// Give a value that took the place of an omitted one, at child
319    /// `value_idx`, the separator it lacked: `a:` becomes `a: v`, not the
320    /// single plain scalar `a:v`, and in flow context `{a}` becomes
321    /// `{a: v}`, not `{av}`. A comment right after it is moved off it too.
322    fn separate_filled_value(&self, value_idx: usize, flow_context: bool) {
323        let precedes_comment = self
324            .0
325            .children_with_tokens()
326            .nth(value_idx + 1)
327            .is_some_and(|c| c.kind() == SyntaxKind::COMMENT);
328        if precedes_comment {
329            self.insert_at(
330                value_idx + 1,
331                vec![fresh_token(SyntaxKind::WHITESPACE, " ")],
332            );
333        }
334        let has_colon = self
335            .0
336            .children_with_tokens()
337            .take(value_idx)
338            .any(|c| c.kind() == SyntaxKind::COLON);
339        let follows_colon = value_idx
340            .checked_sub(1)
341            .and_then(|i| self.0.children_with_tokens().nth(i))
342            .is_some_and(|c| c.kind() == SyntaxKind::COLON);
343        if follows_colon {
344            self.insert_at(value_idx, vec![fresh_token(SyntaxKind::WHITESPACE, " ")]);
345        } else if !has_colon && flow_context {
346            // A `:` straight after a key that is only a tag or anchor
347            // (`{!}`) would read as part of the property's name.
348            let after_property = self
349                .key()
350                .and_then(|key| trivia::last_non_empty_token(&key))
351                .is_some_and(|t| matches!(t.kind(), SyntaxKind::TAG | SyntaxKind::ANCHOR));
352            let colon = fresh_token(SyntaxKind::COLON, ":");
353            let mut tokens = vec![colon.clone(), fresh_token(SyntaxKind::WHITESPACE, " ")];
354            if after_property {
355                tokens.insert(0, fresh_token(SyntaxKind::WHITESPACE, " "));
356            }
357            self.insert_at(value_idx, tokens);
358            trivia::separate_from_question(&colon);
359        }
360    }
361
362    /// Append `tokens` at the end of `self.0`.
363    fn append(&self, tokens: Vec<rowan::SyntaxToken<Lang>>) {
364        super::super::append_children(&self.0, tokens.into_iter().map(Into::into).collect());
365    }
366
367    /// Give a block explicit key without a value (`? a`) the `:` line a
368    /// value goes on, at the column of the `?`.
369    fn add_explicit_value_indicator(&self) {
370        let has = |kind| has_child_token(&self.0, |k| k == kind);
371        if !has(SyntaxKind::QUESTION) || has(SyntaxKind::COLON) {
372            return;
373        }
374        let value_idx = self.value_index();
375        // The entry's line break, if it has one, now ends the `:` line,
376        // and the key's line gets one of its own. A block key ends its line
377        // itself, and the entry then has none, so the `:` line gets one.
378        // One that ends the input unterminated gets its line break where
379        // the parser puts it, in its last entry.
380        let mut tokens = Vec::new();
381        let key = self.key();
382        let key_ends_line = key.as_ref().is_some_and(trivia::ends_with_line_break);
383        let key_line_end = key
384            .as_ref()
385            .map(trivia::line_end_holder)
386            .filter(|holder| Some(holder) != key.as_ref());
387        match key_line_end {
388            Some(holder) if !key_ends_line => crate::nodes::append_children(
389                &holder,
390                vec![fresh_token(SyntaxKind::NEWLINE, "\n").into()],
391            ),
392            _ if !key_ends_line => tokens.push(fresh_token(SyntaxKind::NEWLINE, "\n")),
393            _ => {}
394        }
395        let indent = self.indent_of_line();
396        if indent > 0 {
397            tokens.push(fresh_token(SyntaxKind::INDENT, &" ".repeat(indent)));
398        }
399        tokens.push(fresh_token(SyntaxKind::COLON, ":"));
400        tokens.push(fresh_token(SyntaxKind::WHITESPACE, " "));
401        self.insert_at(value_idx, tokens);
402        if key_ends_line {
403            self.append(vec![fresh_token(SyntaxKind::NEWLINE, "\n")]);
404        }
405    }
406
407    /// Write the `:` of a key the parser found without one, where it left
408    /// a zero-width COLON.
409    fn write_missing_colon(&self) {
410        let missing = self.0.children_with_tokens().position(|c| {
411            c.as_token()
412                .is_some_and(|t| t.kind() == SyntaxKind::COLON && t.text().is_empty())
413        });
414        if let Some(i) = missing {
415            let colon = fresh_token(SyntaxKind::COLON, ":");
416            replace_children(&self.0, i..i + 1, vec![colon.into()]);
417        }
418    }
419
420    /// Replace this entry's value with a block sequence or mapping from
421    /// another CST. Emits the value on the line after the key, indented to
422    /// this entry's nesting depth + 2, with the source's interior
423    /// indentation shifted to match. Called only from [`set_value`] for
424    /// non-flow parents whose new value is a node-backed block collection.
425    ///
426    /// Uses targeted CST edits (splice the VALUE, detach specific
427    /// surrounding tokens) rather than rebuilding the whole entry, so
428    /// unrelated tokens (leading comments, alignment whitespace) survive.
429    fn set_block_value(&self, source: &SyntaxNode, anchor: Option<&str>) {
430        let block = build_block_value_node(source, self.indent_of_line() + 2, anchor);
431        let value_ends_with_newline = block.ends_with_newline;
432
433        let value_idx = self.value_index();
434        // The WHITESPACE between COLON and VALUE, if any.
435        let ws_between = self
436            .0
437            .children_with_tokens()
438            .nth(value_idx.wrapping_sub(1))
439            .and_then(|c| c.into_token())
440            .filter(|t| t.kind() == SyntaxKind::WHITESPACE);
441        // That whitespace separates the colon from a key-line comment, not
442        // from the value, if the comment comes first in VALUE.
443        let value_ws = ws_between
444            .as_ref()
445            .filter(|_| !self.value_starts_with_comment())
446            .map(|t| t.text().to_string());
447        let kept_ws = || value_ws.clone().unwrap_or_else(|| " ".to_string());
448        // Keep any key-line comment on the key line: after what the new
449        // value puts there, or else first in VALUE, before its line break.
450        // If the new value has a comment of its own there (a block scalar
451        // header's), the old one goes on a line of its own above the entry.
452        let mut displaced_comment = None;
453        let separator = match find_key_line_comment(&self.0) {
454            Some((ws, comment)) if !block.on_key_line => {
455                insert_children(
456                    &block.node,
457                    0,
458                    vec![fresh_token(SyntaxKind::COMMENT, &comment).into()],
459                );
460                Some(ws)
461            }
462            Some(comment) => {
463                if !add_to_first_line(&block.node, comment.clone()) {
464                    displaced_comment = Some(comment.1);
465                }
466                Some(kept_ws())
467            }
468            None => block.on_key_line.then(kept_ws),
469        };
470
471        let start = if ws_between.is_some() {
472            value_idx - 1
473        } else {
474            value_idx
475        };
476        let mut replacement = Vec::new();
477        if let Some(ws) = separator {
478            replacement.push(fresh_token(SyntaxKind::WHITESPACE, &ws).into());
479        }
480        replacement.push(block.node.into());
481        replace_children(&self.0, start..value_idx + 1, replacement);
482
483        self.settle_trailing_newline(value_ends_with_newline);
484        if let Some(comment) = displaced_comment {
485            insert_comment_line_above(&self.0, &comment);
486        }
487    }
488
489    /// Keep exactly one line break ending the entry once its VALUE has
490    /// been replaced: the value's own if it ends with one, else the entry's.
491    fn settle_trailing_newline(&self, value_ends_with_newline: bool) {
492        // The old terminator at the entry's level goes: a value ending in a
493        // line break has its own, and otherwise the line break goes where
494        // the parser puts it, in the last entry of a block value.
495        if let Some(own) = self
496            .0
497            .last_child_or_token()
498            .filter(|c| c.kind() == SyntaxKind::NEWLINE)
499        {
500            own.detach();
501        }
502        if !value_ends_with_newline {
503            trivia::terminate(&self.0);
504        }
505    }
506
507    /// Replace the value of this entry in place, preserving the key and surrounding whitespace.
508    ///
509    /// Comments inside the old value are removed with it; the comment on
510    /// the key's line stays with the entry. An anchor on the old value is
511    /// kept on the new one, in place of any anchor of its own, so aliases
512    /// to it still resolve.
513    ///
514    /// Aliases elsewhere to anchors deeper in the old value are kept
515    /// resolving: the first of each becomes a copy of the anchored node.
516    pub fn set_value(&self, new_value: impl crate::AsYaml, flow_context: bool) {
517        // An alias cannot carry an anchor; the first alias to the old one
518        // gets a copy of the old value instead.
519        let old = self.value();
520        let kept = kept_anchor_token(&old, &new_value);
521        let anchor = kept.as_ref().map(|t| t.text().to_string());
522        crate::nodes::kept_aliases::keeping_aliases(&old, kept, || {
523            self.replace_value_under(new_value, flow_context, anchor.as_deref());
524            Some(self.value())
525        });
526    }
527
528    /// Replace the value as [`set_value`](Self::set_value) does, putting
529    /// `anchor` on the new value. Without one, a node value keeps the
530    /// anchor before it where it was copied from.
531    pub(crate) fn replace_value_under(
532        &self,
533        new_value: impl crate::AsYaml,
534        flow_context: bool,
535        anchor: Option<&str>,
536    ) {
537        // A block entry without a final line break ends the input, which
538        // stays unterminated.
539        let open = !flow_context && !trivia::ends_line(&self.0);
540        self.replace_value(new_value, flow_context, anchor);
541        if open {
542            trivia::unterminate_last(&self.0);
543        }
544        crate::nodes::replaced_value::settle_lines_after_block_scalar(&self.0);
545        if let Some(content) = self.value().last_child() {
546            trivia::drop_space_before_empty(&content);
547        }
548    }
549
550    /// Replace the value with `new_value`, under `old_anchor` if set.
551    fn replace_value(
552        &self,
553        new_value: impl crate::AsYaml,
554        flow_context: bool,
555        old_anchor: Option<&str>,
556    ) {
557        use crate::as_yaml::YamlKind;
558
559        // An alias cannot carry an anchor.
560        let old_anchor = old_anchor.filter(|_| new_value.kind() != YamlKind::Alias);
561        if !flow_context {
562            self.add_explicit_value_indicator();
563        }
564        self.write_missing_colon();
565
566        // A block collection or block scalar coming from another CST goes
567        // through set_block_value, which does targeted CST edits to swap
568        // the VALUE and adjust the surrounding tokens (drop the WHITESPACE
569        // between COLON and VALUE, drop the redundant trailing NEWLINE,
570        // rescue any inline comment on the old value line). Flow context
571        // (parent mapping is flow-style) means block content isn't valid
572        // there, so we fall through to the default path.
573        let built = (!flow_context)
574            .then(|| built_block_node(&new_value))
575            .flatten();
576        let block_source = new_value
577            .value_node()
578            .filter(|_| !flow_context && !new_value.is_inline())
579            .or(built)
580            .filter(|_| {
581                matches!(
582                    new_value.kind(),
583                    YamlKind::Mapping | YamlKind::Sequence | YamlKind::Scalar | YamlKind::Tagged(_)
584                )
585            });
586        if let Some(source) = block_source {
587            self.set_block_value(&source, old_anchor);
588            return;
589        }
590
591        // Build the new VALUE. Without an old anchor to keep, a node-backed
592        // inline value preceded by an `&anchor` in its parent VALUE keeps
593        // that anchor, so aliases pointing at the moved value still resolve.
594        // A key-line comment that goes away with the old value (inside the
595        // old VALUE, or at MAPPING_ENTRY level before a block VALUE) is
596        // embedded inside the new inline VALUE so it stays on the key line
597        // and doesn't swallow anything. One outside an inline VALUE, as in
598        // a flow mapping, stays where it is.
599        let anchor = old_anchor
600            .map(str::to_string)
601            .or_else(|| new_value.value_node().and_then(|n| source_anchor_text(&n)));
602        let old_was_block = value_is_block(&self.value());
603        let trailing_comment = (old_was_block || has_comment(&self.value()))
604            .then(|| find_key_line_comment(&self.0))
605            .flatten();
606        let mut value_builder = GreenNodeBuilder::new();
607        value_builder.start_node(SyntaxKind::VALUE.into());
608        if let Some(a) = anchor.as_deref() {
609            value_builder.token(SyntaxKind::ANCHOR.into(), a);
610            value_builder.token(SyntaxKind::WHITESPACE.into(), " ");
611        }
612        // A block scalar rendered here has to clear this entry's own
613        // column, so hand it that rather than the document's.
614        let flow = flow_context.then(|| {
615            self.0
616                .parent()
617                .map_or(FlowQuoting::Minimal, |m| crate::nodes::flow_quoting(&m))
618        });
619        let value_ends_with_newline =
620            build_in(&new_value, &mut value_builder, self.indent_of_line(), flow);
621        if let Some((ws, comment)) = trailing_comment
622            .as_ref()
623            .filter(|_| !value_ends_with_newline)
624        {
625            value_builder.token(SyntaxKind::WHITESPACE.into(), ws);
626            value_builder.token(SyntaxKind::COMMENT.into(), comment);
627        }
628        value_builder.finish_node();
629        let new_value_node = SyntaxNode::new_root_mut(value_builder.finish());
630        if old_anchor.is_some() {
631            drop_tagged_anchor(&new_value_node);
632        }
633        // A block scalar's body follows its header line, which is where
634        // the parser puts a comment on the key line.
635        if let Some((ws, comment)) = trailing_comment.filter(|_| value_ends_with_newline) {
636            crate::nodes::insert_header_comment(&new_value_node, &ws, &comment);
637        }
638
639        self.splice_inline_value(&new_value_node, old_was_block, flow_context);
640        if value_ends_with_newline {
641            self.settle_trailing_newline(true);
642        }
643    }
644
645    /// Put `new_value_node`, a VALUE that starts on the key line, in place
646    /// of this entry's value, which is a block value if `old_was_block`.
647    fn splice_inline_value(
648        &self,
649        new_value_node: &SyntaxNode,
650        old_was_block: bool,
651        flow_context: bool,
652    ) {
653        if old_was_block {
654            self.rebuild_with_inline_value(new_value_node);
655            return;
656        }
657
658        // Targeted splice: replace the VALUE child in place; siblings and
659        // surrounding tokens (comments, alignment WS) survive untouched.
660        let value_idx = self.value_index();
661        // A block scalar's body ends with the entry's line break.
662        let old_ends_line = match crate::nodes::ending_block_scalar(&self.0) {
663            Some(scalar) => trivia::ends_line(&scalar),
664            None => trivia::ends_with_line_break(&self.0),
665        };
666        let old_omitted = self.value().text_range().is_empty();
667        let new_omitted = new_value_node.text_range().is_empty();
668        // The space before a key-line comment that was all the old
669        // value had now goes before the new value, which carries the
670        // comment with its own.
671        let mut at = value_idx..value_idx + 1;
672        let mut replacement = vec![new_value_node.clone().into()];
673        let space_before = value_idx
674            .checked_sub(1)
675            .and_then(|i| self.0.children_with_tokens().nth(i))
676            .is_some_and(|c| c.kind() == SyntaxKind::WHITESPACE);
677        if self.value_starts_with_comment() && space_before {
678            at.start -= 1;
679            replacement.insert(0, fresh_token(SyntaxKind::WHITESPACE, " ").into());
680        }
681        let value_idx = at.start + replacement.len() - 1;
682        replace_children(&self.0, at, replacement);
683        if old_omitted && !new_omitted {
684            self.separate_filled_value(value_idx, flow_context);
685        }
686        if old_ends_line {
687            trivia::terminate(&self.0);
688        }
689    }
690
691    /// Replace this entry's block value with `new_value_node` (an inline
692    /// VALUE) that sits on the same line as the key. Uses targeted CST
693    /// edits: splice the VALUE, ensure a single WHITESPACE sits between
694    /// COLON and it, rescue any inline comment on the key line so it
695    /// survives, and append a trailing NEWLINE if the entry doesn't
696    /// already end with one.
697    fn rebuild_with_inline_value(&self, new_value_node: &SyntaxNode) {
698        let value_idx = self.value_index();
699
700        // Right after the COLON of an old block VALUE there is only trailing
701        // whitespace and possibly a key-line comment (`k:  # note\n  - block`),
702        // never a value separator. Detach them: the new inline
703        // `new_value_node` already carries the rescued comment inside
704        // itself. Leaving the old tokens would duplicate the comment and -
705        // since comments extend to end of line - swallow the new value.
706        // Whitespace before the COLON (after `?`, or in `k :`) stays.
707        let stale: Vec<_> = self
708            .0
709            .children_with_tokens()
710            .take(value_idx)
711            .skip_while(|c| c.kind() != SyntaxKind::COLON)
712            .skip(1)
713            .map_while(|c| {
714                c.into_token()
715                    .filter(|t| matches!(t.kind(), SyntaxKind::WHITESPACE | SyntaxKind::COMMENT))
716            })
717            .collect();
718        for tok in stale {
719            tok.detach();
720        }
721
722        // Swap VALUE (its index may have moved if we detached above).
723        let value_idx = self.value_index();
724        replace_children(
725            &self.0,
726            value_idx..value_idx + 1,
727            vec![new_value_node.clone().into()],
728        );
729
730        // Ensure a single WHITESPACE sits between COLON and VALUE - the
731        // old block VALUE typically had none.
732        let prev_is_ws = value_idx.checked_sub(1).is_some_and(|p| {
733            self.0
734                .children_with_tokens()
735                .nth(p)
736                .and_then(|c| c.into_token())
737                .is_some_and(|t| t.kind() == SyntaxKind::WHITESPACE)
738        });
739        if !prev_is_ws {
740            self.insert_at(value_idx, vec![fresh_token(SyntaxKind::WHITESPACE, " ")]);
741        }
742
743        // Inline values don't own a trailing NEWLINE; ensure the entry has
744        // one.
745        trivia::terminate(&self.0);
746    }
747
748    /// Detach this entry from its parent mapping, effectively removing it.
749    ///
750    /// The entry node is detached from the tree; the `MappingEntry` value is
751    /// consumed. To retrieve the removed entry from a mapping (and get back a
752    /// `MappingEntry` you can inspect), use [`Mapping::remove`](super::Mapping::remove) instead.
753    pub fn discard(self) {
754        crate::nodes::kept_aliases::keeping_aliases(&self.0, None, || {
755            self.0.detach();
756            None
757        });
758    }
759
760    /// Remove this entry from its parent mapping.
761    ///
762    /// This is a convenience method that calls [`discard`](Self::discard)
763    /// internally. It's useful when you have a [`MappingEntry`] (e.g., from
764    /// [`find_all_entries_by_key`](super::Mapping::find_all_entries_by_key)) and want
765    /// to remove it without retrieving it from the mapping again.
766    ///
767    /// Consumes `self` and detaches the entry from the parent mapping.
768    pub fn remove(self) {
769        self.discard();
770    }
771
772    crate::nodes::ast_node_spans!("mapping entry");
773}