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