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}