yaml-edit 0.3.5

A lossless parser and editor for YAML files
Documentation
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
//! Empty collections. YAML has no block syntax for a mapping or sequence
//! without entries, so an empty one is written `{}` or `[]`: a block
//! collection that loses its last entry turns into that, and an empty
//! placeholder turns back into a block collection when it gets its first.

use super::{
    append_children, fresh_token, has_child_token, insert_children, replace_children,
    starts_a_flow_collection, trivia, SyntaxNode, SyntaxToken,
};
use crate::lex::SyntaxKind;
use rowan::ast::AstNode;

type Element = rowan::NodeOrToken<SyntaxNode, SyntaxToken>;

/// Write `collection`, a block MAPPING or SEQUENCE left without entries,
/// as `{}` or `[]`, so that it does not read back as null or merge into
/// what follows. Does nothing to a flow collection or one with entries.
///
/// The collection itself becomes the flow-empty, rather than a new node, so
/// handles to it stay attached. As a mapping value it moves up onto the
/// key's line, with its node properties and a comment ending that line:
/// `key: !t {}  # c`. Elsewhere, as a sequence item, explicit key or
/// document root, it stays where it is. A collection with no other
/// around it, at the root of its document or detached from it, keeps the
/// comment lines after its last entry; a nested one has none left, as
/// they move out while its first and last entries go.
///
/// `line_break` is the line break that ended the collection's last line
/// before it was drained, if it had one; the flow-empty line ends with it
/// too.
///
/// The result carries the pending-block marker, so that the collection
/// returns to block style when it gets an entry again.
pub(crate) fn render_empty(collection: &SyntaxNode, line_break: Option<&str>) {
    if brackets(collection.kind()).is_none()
        || starts_a_flow_collection(collection)
        || collection.children().any(|c| {
            matches!(
                c.kind(),
                SyntaxKind::MAPPING_ENTRY | SyntaxKind::SEQUENCE_ENTRY
            )
        })
    {
        return;
    }
    let parent = outer_node(collection).parent();
    assert!(
        !in_collection(collection) || !has_child_token(collection, |k| k == SyntaxKind::COMMENT),
        "comment lines left in a drained nested {:?}",
        collection.kind()
    );
    // A value holding another node besides, which the validator rejects,
    // has no key's line for the collection to move up onto, and neither
    // has one detached from its entry.
    let on_key_line = parent
        .as_ref()
        .is_some_and(|p| p.children().count() == 1 && p.parent().is_some());
    match parent.map(|p| p.kind()) {
        Some(SyntaxKind::VALUE) if on_key_line => render_as_value(collection, line_break),
        Some(
            SyntaxKind::VALUE | SyntaxKind::SEQUENCE_ENTRY | SyntaxKind::KEY | SyntaxKind::DOCUMENT,
        )
        | None => render_in_place(collection, line_break),
        Some(kind) => panic!("unexpected {kind:?} holding a block collection"),
    }
}

/// Whether `node` is inside another mapping or sequence.
fn in_collection(node: &SyntaxNode) -> bool {
    node.ancestors()
        .skip(1)
        .any(|n| matches!(n.kind(), SyntaxKind::MAPPING | SyntaxKind::SEQUENCE))
}

/// The flow-empty brackets for a collection of `kind`, if it is one.
fn brackets(kind: SyntaxKind) -> Option<((SyntaxKind, &'static str), (SyntaxKind, &'static str))> {
    match kind {
        SyntaxKind::MAPPING => Some((
            (SyntaxKind::LEFT_BRACE, "{"),
            (SyntaxKind::RIGHT_BRACE, "}"),
        )),
        SyntaxKind::SEQUENCE => Some((
            (SyntaxKind::LEFT_BRACKET, "["),
            (SyntaxKind::RIGHT_BRACKET, "]"),
        )),
        _ => None,
    }
}

/// Replace the contents of the drained `collection` with flow-empty
/// brackets, returning the comment lines and line breaks it held.
fn replace_with_brackets(collection: &SyntaxNode) -> Vec<SyntaxToken> {
    let leftovers: Vec<_> = collection
        .children_with_tokens()
        .map(|el| match el {
            rowan::NodeOrToken::Token(t) => t,
            rowan::NodeOrToken::Node(n) => {
                panic!("unexpected {:?} left in an empty collection", n.kind())
            }
        })
        .collect();
    if let Some(t) = leftovers.iter().find(|t| {
        !matches!(
            t.kind(),
            SyntaxKind::COMMENT | SyntaxKind::INDENT | SyntaxKind::WHITESPACE | SyntaxKind::NEWLINE
        )
    }) {
        panic!("unexpected {:?} left in an empty collection", t.kind());
    }
    let (open, close) = brackets(collection.kind()).expect("a collection");
    replace_children(
        collection,
        0..leftovers.len(),
        vec![
            fresh_token(open.0, open.1).into(),
            pending_block_marker().into(),
            fresh_token(close.0, close.1).into(),
        ],
    );
    leftovers
}

fn copies(tokens: &[SyntaxToken]) -> Vec<Element> {
    tokens
        .iter()
        .map(|t| fresh_token(t.kind(), t.text()).into())
        .collect()
}

/// [`render_empty`] for a collection that is a sequence item, explicit key,
/// document root, detached, or not alone in a mapping value.
fn render_in_place(collection: &SyntaxNode, line_break: Option<&str>) {
    // Entries take their trailing NEWLINE with them when removed. The
    // comment lines of a collection with none around it stay after the
    // flow-empty line, indented as they were.
    let leftovers = replace_with_brackets(collection);
    let tail_start = leftovers
        .iter()
        .position(|t| t.kind() == SyntaxKind::COMMENT)
        .map(|i| {
            if i > 0 && leftovers[i - 1].kind() != SyntaxKind::NEWLINE {
                i - 1
            } else {
                i
            }
        })
        .unwrap_or(leftovers.len());
    let tail = copies(&leftovers[tail_start..]);

    indent_past_parent(collection);

    if line_break.is_none() && tail.is_empty() {
        return;
    }
    let newline = || fresh_token(SyntaxKind::NEWLINE, line_break.unwrap_or("\n"));
    // The line break follows the node, properties included, as a parsed
    // flow-empty line has it. It ends an explicit-key entry without a `:`.
    let mut lines = vec![newline().into()];
    lines.extend(tail);
    let node = line_node(collection);
    if node.kind() == SyntaxKind::MAPPING_ENTRY {
        append_children(&node, lines);
        return;
    }
    // A collection that is the root of its tree, as only a builder makes
    // one, has no line of its own, nor comment lines.
    let Some(parent) = node.parent() else {
        assert!(lines.len() == 1, "comment lines left in a built collection");
        return;
    };
    insert_children(&parent, node.index() + 1, lines);
}

/// [`render_empty`] for a mapping value: replace the `NEWLINE INDENT
/// COLLECTION` scaffold with an inline ` {}` or ` []`, so the entry
/// renders as `key: {}` instead of an unterminated `key:\n    `. With
/// comment lines before the collection, which cannot share the key's
/// line, the flow-empty takes the collection's place instead.
fn render_as_value(collection: &SyntaxNode, line_break: Option<&str>) {
    // A tag sits between the VALUE and the collection; the rewrite
    // happens inside the TAGGED_NODE so the tag survives.
    let host = collection.parent().expect("a mapping value has a parent");
    let outer = outer_node(collection);
    let value_node = outer.parent().expect("a mapping value has a VALUE");
    let entry_node = value_node
        .parent()
        .filter(|p| p.kind() == SyntaxKind::MAPPING_ENTRY)
        .expect("a VALUE is in a MAPPING_ENTRY");

    let mut properties = Vec::new();
    let mut comment = None;
    let mut comment_lines = false;
    let mut seen_newline = false;
    for el in host.children_with_tokens() {
        let token = match el {
            rowan::NodeOrToken::Node(n) if &n == collection => continue,
            rowan::NodeOrToken::Node(n) => {
                panic!("unexpected {:?} next to a block collection", n.kind())
            }
            rowan::NodeOrToken::Token(t) => t,
        };
        match token.kind() {
            SyntaxKind::TAG | SyntaxKind::ANCHOR => properties.push(token),
            SyntaxKind::COMMENT if seen_newline => comment_lines = true,
            SyntaxKind::COMMENT => comment = Some(token),
            SyntaxKind::NEWLINE => seen_newline = true,
            SyntaxKind::INDENT | SyntaxKind::WHITESPACE => {}
            kind => panic!("unexpected {kind:?} next to a block collection"),
        }
    }

    replace_with_brackets(collection);

    if comment_lines {
        indent_past_parent(collection);
        if let Some(line_break) = line_break {
            trivia::terminate_with(&entry_node, line_break);
        }
        return;
    }

    // Keep the host node and its properties, and replace everything after
    // them (comment, line break, indent and the block collection) with the
    // flow-empty collection.
    let start = properties.last().map_or(0, |p| p.index() + 1);
    let end = host.children_with_tokens().count();
    // Each separator must be its own token: rowan cannot attach one
    // mutable token in two places.
    let ws = || fresh_token(SyntaxKind::WHITESPACE, " ");
    let has_properties = !properties.is_empty();
    let mut replacement: Vec<Element> = Vec::new();
    if has_properties {
        replacement.push(ws().into());
    }
    replacement.push(collection.clone().into());
    let mut comment: Vec<Element> = comment
        .into_iter()
        .flat_map(|c| {
            [
                ws().into(),
                fresh_token(SyntaxKind::COMMENT, c.text()).into(),
            ]
        })
        .collect();
    // A comment after a TAGGED_NODE belongs to the VALUE around it.
    if host.kind() != SyntaxKind::TAGGED_NODE {
        replacement.append(&mut comment);
    }
    replace_children(&host, start..end, replacement);
    if !comment.is_empty() {
        let after_host = outer.index() + 1;
        insert_children(&value_node, after_host, comment);
    }

    // The space separating the colon from its value belongs to the
    // entry, not the value: a parsed `key: {}` puts it between COLON
    // and VALUE. Left inside, a later edit that replaces the whole
    // VALUE took the space with it and wrote `key:value`, which is a
    // plain scalar rather than an entry.
    if !has_properties {
        let index = value_node.index();
        let separated = value_node
            .prev_sibling_or_token()
            .is_some_and(|el| el.kind() == SyntaxKind::WHITESPACE);
        if !separated {
            insert_children(&entry_node, index, vec![ws().into()]);
        }
    }

    if let Some(line_break) = line_break {
        trivia::terminate_with(&entry_node, line_break);
    }
}

/// A block sequence in an explicit key or a mapping value may start on a
/// line of its own at the column of the `?` or the key, but a flow
/// collection there would end the entry. Indent `collection` past that
/// column if it sits there.
fn indent_past_parent(collection: &SyntaxNode) {
    let Some(host) = collection.parent() else {
        return;
    };
    let Some(slot) = host
        .ancestors()
        .find(|n| n.kind() != SyntaxKind::TAGGED_NODE)
    else {
        return;
    };
    let Some(entry) = slot.parent() else {
        return;
    };
    let indicator = match slot.kind() {
        // An implicit key holds a block collection only in input the
        // validator rejects, and has no `?` to indent past.
        SyntaxKind::KEY => entry
            .children_with_tokens()
            .filter_map(|el| el.into_token())
            .find(|t| t.kind() == SyntaxKind::QUESTION),
        SyntaxKind::VALUE => entry.first_token(),
        _ => None,
    };
    let Some(indicator) = indicator else {
        return;
    };
    let Some(prev) = collection
        .prev_sibling_or_token()
        .and_then(|el| el.into_token())
    else {
        return;
    };
    let indent = match prev.kind() {
        SyntaxKind::NEWLINE => None,
        SyntaxKind::INDENT => Some(prev),
        _ => return,
    };
    let column = crate::column::token_column(&indicator);
    if indent
        .as_ref()
        .is_some_and(|t| t.text().chars().count() > column)
    {
        return;
    }
    let new_indent = fresh_token(SyntaxKind::INDENT, &" ".repeat(column + 2));
    let at = collection.index();
    match indent {
        Some(t) => replace_children(&host, t.index()..at, vec![new_indent.into()]),
        None => insert_children(&host, at, vec![new_indent.into()]),
    }
}

/// `collection`, or the outermost TAGGED_NODE around it. Input with more
/// than one tag or anchor on a node, which the validator rejects, nests
/// them.
fn outer_node(collection: &SyntaxNode) -> SyntaxNode {
    collection
        .ancestors()
        .take_while(|n| n == collection || n.kind() == SyntaxKind::TAGGED_NODE)
        .last()
        .expect("ancestors has self")
}

/// The node that carries `collection`'s line: the collection itself, the
/// TAGGED_NODE around it, or for an explicit key the KEY, or the entry if
/// that has no `:`.
fn line_node(collection: &SyntaxNode) -> SyntaxNode {
    let node = outer_node(collection);
    let Some(key) = node.parent().filter(|p| p.kind() == SyntaxKind::KEY) else {
        return node;
    };
    let entry = key.parent().expect("a KEY is in an entry");
    if has_child_token(&entry, |k| k == SyntaxKind::COLON) {
        key
    } else {
        entry
    }
}

/// Detach the line break that ended the line of `empty_collection`, now
/// that `new_entry` is its first entry: the enclosing MAPPING_ENTRY's
/// trailing NEWLINE for a mapping value (a `key:\n    \n` or `key: {}`
/// line, see issue #18), or the NEWLINE after a collection that was a
/// pending-block `{}` or `[]` elsewhere. A root collection takes back the
/// lines after it, which were the document's while it was `{}` or `[]`.
///
/// The freshly built `new_entry` must be checked because it only supplies
/// its own trailing NEWLINE for some shapes; when it does not, the
/// placeholder is still needed to terminate the line.
pub(crate) fn detach_empty_collection_placeholder_newline(
    empty_collection: &SyntaxNode,
    new_entry: &SyntaxNode,
) {
    if !trivia::ends_with_line_break(new_entry) {
        return;
    }
    let node = line_node(empty_collection);
    let parent = node.parent();
    // A value detached from its entry holds the line break itself.
    let entry = parent
        .as_ref()
        .filter(|p| p.kind() == SyntaxKind::VALUE)
        .and_then(|p| p.parent());
    let line_break = match entry {
        Some(entry) => entry.last_child_or_token(),
        None if node.kind() == SyntaxKind::MAPPING_ENTRY => node.last_child_or_token(),
        None => node.next_sibling_or_token(),
    };
    if let Some(line_break) = line_break.filter(|t| t.kind() == SyntaxKind::NEWLINE) {
        line_break.detach();
    }
    // The lines after a root block collection up to `...` are its own. A
    // second root node, which the validator rejects, is not.
    if parent.is_some_and(|p| p.kind() == SyntaxKind::DOCUMENT) {
        let lines: Vec<_> =
            std::iter::successors(node.next_sibling_or_token(), |c| c.next_sibling_or_token())
                .take_while(|c| c.as_token().is_some() && c.kind() != SyntaxKind::DOC_END)
                .collect();
        for c in &lines {
            c.detach();
        }
        super::append_children(empty_collection, lines);
    }
}

/// The zero-width token inside `{}` or `[]` that marks an empty collection
/// as headed for block style; see
/// [`Mapping::new_pending_block`](crate::Mapping::new_pending_block).
fn pending_block_marker() -> SyntaxToken {
    fresh_token(SyntaxKind::WHITESPACE, "")
}

/// Is `collection` an empty mapping or sequence awaiting its first entry?
/// See [`Mapping::new_pending_block`](crate::Mapping::new_pending_block).
pub(crate) fn is_block_placeholder(collection: &SyntaxNode) -> bool {
    !collection.children().any(|c| {
        matches!(
            c.kind(),
            SyntaxKind::MAPPING_ENTRY | SyntaxKind::SEQUENCE_ENTRY
        )
    }) && collection
        .children_with_tokens()
        .filter_map(|c| c.into_token())
        .any(|t| t.kind() == SyntaxKind::WHITESPACE && t.text().is_empty())
}

/// Turn `collection` into a block collection if it is an empty placeholder
/// and nothing forces it to stay flow, before its first entry is added:
/// drop its flow delimiters. As a mapping value, it moves to the line
/// after the key, which keeps its node properties and comment:
/// `key: !t {}  # c` becomes `key: !t  # c\n  `, ready for the entry.
/// Elsewhere the entry takes the place of the brackets.
pub(crate) fn convert_placeholder_to_block(collection: &SyntaxNode) {
    if !is_block_placeholder(collection) || super::sequence::must_render_flow(collection) {
        return;
    }
    let children: Vec<_> = collection.children_with_tokens().collect();
    for child in children {
        child.detach();
    }

    let Some(host) = collection.parent() else {
        return;
    };
    let node = outer_node(collection);
    let Some(value) = node.parent().filter(|p| p.kind() == SyntaxKind::VALUE) else {
        return;
    };
    let pos = collection.index();
    let already_scaffolded = pos >= 1
        && host
            .children_with_tokens()
            .nth(pos - 1)
            .is_some_and(|c| matches!(c.kind(), SyntaxKind::INDENT | SyntaxKind::NEWLINE));
    if already_scaffolded {
        return;
    }
    // A value detached from its entry has no key's line, but moves to a
    // line of its own all the same.
    let entry = value.parent();
    let indent_width = entry
        .as_ref()
        .and_then(|e| e.parent())
        .filter(|m| m.kind() == SyntaxKind::MAPPING)
        .and_then(crate::nodes::Mapping::cast)
        .map_or(2, |m| m.detect_indentation_level() + 2);

    // The comment ending the line moves before the block, where the parser
    // puts it in `key: # c\n  a: 1`.
    let after: Vec<_> = value
        .children_with_tokens()
        .skip(node.index() + 1)
        .collect();
    let comment = after
        .iter()
        .filter_map(|el| el.as_token())
        .find(|t| t.kind() == SyntaxKind::COMMENT)
        .map(|t| fresh_token(SyntaxKind::COMMENT, t.text()));
    for el in after {
        el.detach();
    }
    let space_before = collection
        .prev_sibling_or_token()
        .filter(|el| el.kind() == SyntaxKind::WHITESPACE);
    let has_properties = host
        .children_with_tokens()
        .any(|el| matches!(el.kind(), SyntaxKind::TAG | SyntaxKind::ANCHOR));
    if let Some(space) = space_before {
        space.detach();
    }

    let mut scaffold: Vec<Element> = Vec::new();
    if let Some(comment) = comment {
        if has_properties {
            scaffold.push(fresh_token(SyntaxKind::WHITESPACE, " ").into());
        }
        scaffold.push(comment.into());
    } else if !has_properties {
        // Nothing is left on the key's line after the colon, so the space
        // separating it from the value goes too.
        let index = value.index();
        let separator = entry
            .and_then(|e| e.children_with_tokens().nth(index.wrapping_sub(1)))
            .filter(|el| index > 0 && el.kind() == SyntaxKind::WHITESPACE);
        if let Some(separator) = separator {
            separator.detach();
        }
    }
    scaffold.push(fresh_token(SyntaxKind::NEWLINE, "\n").into());
    scaffold.push(fresh_token(SyntaxKind::INDENT, &" ".repeat(indent_width)).into());
    let pos = collection.index();
    insert_children(&host, pos, scaffold);
}