Skip to main content

yaml_edit/nodes/
mod.rs

1//! AST node types for YAML.
2//!
3//! # CST invariants
4//!
5//! yaml-edit is a lossless editor: every mutation produces text that
6//! re-parses into the same tree shape (see [`crate::debug::roundtrip_ok`]
7//! and [`crate::debug::validate_tree`]). To keep that property, all
8//! mutation helpers in this module must uphold the following invariants
9//! on the concrete syntax tree.
10//!
11//! ## Where whitespace lives
12//!
13//! For a block-style mapping entry `key:\n  - a`, the NEWLINE after
14//! `key:` and the INDENT before the SEQUENCE both live inside the parent
15//! VALUE, *not* inside the child SEQUENCE. The SEQUENCE_ENTRYs
16//! themselves carry only the DASH, WHITESPACE, and content:
17//!
18//! ```text
19//! MAPPING_ENTRY
20//!   KEY ...
21//!   COLON ":"
22//!   VALUE
23//!     NEWLINE "\n"
24//!     INDENT "  "
25//!     SEQUENCE
26//!       SEQUENCE_ENTRY
27//!         DASH "-"
28//!         WHITESPACE " "
29//!         SCALAR ...
30//! ```
31//!
32//! Consequence: when constructing a fresh block SEQUENCE or MAPPING to
33//! splice under a key, do **not** prepend its own leading INDENT for the
34//! first entry - the parent VALUE already carries one. Duplicating it
35//! renders as doubled indentation and violates the no-stacked-INDENT
36//! check in [`crate::debug::validate_tree`].
37//!
38//! Comment and blank lines before a block collection's first entry live
39//! in the parent as well, so removing the first entry moves the lines
40//! after it out of the collection.
41//!
42//! For flow-style values (`{a: 1}`, `[1, 2]`) all separators live
43//! *inside* the flow SEQUENCE / MAPPING: the parent VALUE has no direct
44//! NEWLINE child. `value_is_block` in `mapping.rs` distinguishes the two
45//! by looking for a NEWLINE token as a direct child of VALUE.
46//!
47//! The space after the colon belongs to MAPPING_ENTRY, before VALUE,
48//! including for block values that start on the key line with an anchor,
49//! tag or block scalar indicator (`key: &a\n  - a`, `key: |\n  text`).
50//!
51//! ## Entry termination
52//!
53//! Block-style MAPPING_ENTRY and SEQUENCE_ENTRY nodes are terminated by
54//! a NEWLINE, the entry's own last token, after the comment ending its
55//! line if there is one. A mapping entry keeps that comment in its VALUE,
56//! a sequence entry after its item:
57//!
58//! ```text
59//! SEQUENCE
60//!   SEQUENCE_ENTRY
61//!     DASH "-"
62//!     WHITESPACE " "
63//!     SCALAR ("a")
64//!     WHITESPACE "  "
65//!     COMMENT "# note"
66//!     NEWLINE "\n"
67//!   SEQUENCE_ENTRY ...
68//! ```
69//!
70//! An explicit-key entry (`? key\n: value  # c\n`) is no different: its
71//! VALUE holds the comment and the entry the NEWLINE ending the value's
72//! line. One without a `:` line (`? key  # c\n`) has the comment after
73//! its KEY, then its empty VALUE and the NEWLINE. An entry that ends in a
74//! block node, a nested block collection or a block scalar, has its line
75//! ended by that node. See [`trivia`].
76//!
77//! Two carve-outs:
78//! - The **last** entry of a block collection may lack a terminator
79//!   (unterminated sources like `a: 1\nb: 2` are legitimate YAML and
80//!   must roundtrip).
81//! - An explicit-key entry without a value whose key is a block node
82//!   (`? - a\n`) ends in the zero-width `SCALAR { NULL "" }` of its value
83//!   (see below), after the NEWLINE ending the key.
84//!
85//! An empty value never holds the lines after it. Where the parser looks
86//! past the end of the line for a value and finds none, the line break
87//! and whatever follows it are left to the entry and the collection, as
88//! after any other value: `key:\n` is `VALUE { SCALAR { NULL "" } }`
89//! followed by the entry's NEWLINE, and the INDENT, blank lines and
90//! comment lines after it belong to the collection.
91//!
92//! The lines after a nested block collection's last entry, blank lines
93//! and comment lines with their INDENT, belong to the collection that
94//! holds the next entry, as the lines between its own entries do, or to
95//! the root collection at the end of the document. A nested block
96//! collection always ends in an entry:
97//!
98//! ```text
99//! # m:\n  a: 1\n    # c\n\nz: 1\n
100//! MAPPING
101//!   MAPPING_ENTRY ("m")
102//!     VALUE
103//!       ...
104//!       MAPPING
105//!         MAPPING_ENTRY ("a: 1\n")
106//!   INDENT "    "
107//!   COMMENT "# c"
108//!   NEWLINE "\n"
109//!   NEWLINE "\n"
110//!   MAPPING_ENTRY ("z") ...
111//! ```
112//!
113//! When inserting a new sibling entry after an existing one, mutation
114//! helpers must first ensure the predecessor is terminated
115//! ([`trivia::terminate`]).
116//!
117//! ## Document-level trivia
118//!
119//! Trivia outside the root node lives in DOCUMENT: a leading BOM, blank
120//! lines and comment lines before the root, and the NEWLINE and blank
121//! lines after a scalar or flow-collection root. The file root holds
122//! only what is outside every document: the lines before a `---` that
123//! follows a comment, or before a directive, the lines after a `...`
124//! and a stream of nothing but comments. A BOM must stay first in the
125//! stream, so `---` and directives are inserted after it.
126//!
127//! ## Implicit-null values
128//!
129//! Every `MAPPING_ENTRY` and `SEQUENCE_ENTRY` holds a value node, even
130//! when the source omits it. The parser emits a zero-width
131//! `SCALAR { NULL "" }` at every "missing value" position:
132//!
133//! ```text
134//! Source form                     CST shape
135//! ------------------------------- ----------------------------------------
136//! key:\n     (block mapping)      VALUE { SCALAR { NULL "" } }, NEWLINE
137//! key:       (block mapping, EOF) VALUE { SCALAR { NULL "" } }
138//! {key}      (flow mapping)       MAPPING_ENTRY { KEY { SCALAR "key" },
139//!                                                 VALUE { SCALAR { NULL "" } } }
140//! {key:}     (flow mapping)       MAPPING_ENTRY { KEY { SCALAR "key" },
141//!                                                 COLON,
142//!                                                 VALUE { SCALAR { NULL "" } } }
143//! {,}        (flow mapping)       MAPPING_ENTRY { KEY   { SCALAR { NULL "" } },
144//!                                                 VALUE { SCALAR { NULL "" } },
145//!                                                 COMMA }
146//! {: v}      (flow mapping)       KEY { SCALAR { NULL "" } }, COLON, VALUE ...
147//! ? key\n    (explicit key)       MAPPING_ENTRY { QUESTION, KEY ...,
148//!                                                 VALUE { SCALAR { NULL "" } },
149//!                                                 NEWLINE }
150//! {? : v}    (flow explicit key)  KEY { QUESTION, WHITESPACE,
151//!                                       SCALAR { NULL "" } }, COLON, ...
152//! [{]:\n     (missing `:`, error) KEY ..., COLON "",
153//!                                 VALUE { SCALAR { NULL "" } }, NEWLINE
154//! - \n       (block sequence)     SEQUENCE_ENTRY { DASH, WHITESPACE,
155//!                                                  SCALAR { NULL "" }, NEWLINE }
156//! [a, , c]   (flow sequence)      SEQUENCE_ENTRY { SCALAR { NULL "" }, COMMA, ... }
157//! - !e\n     (tag, no content)    TAGGED_NODE { TAG, SCALAR { NULL "" } }
158//! ```
159//!
160//! The invariant: **every KEY, every VALUE, every SEQUENCE_ENTRY holds
161//! exactly one scalar or collection node.** Mappings wrap the value in a
162//! `VALUE` node; sequences hold the scalar directly as a
163//! `SEQUENCE_ENTRY` child (matching how each shape wraps its non-null
164//! values -- sequences never had a wrapper, mappings always did).
165//!
166//! Error recovery keeps to the same shapes. A node with nothing to read
167//! (`[}`, `{?`) is the zero-width null, and a token no entry can start,
168//! like the `]` of `[]:\n]:`, goes in an ERROR node beside the entries,
169//! as content after the end of a document does. What is left of a block
170//! entry's line after its node (`a: :`, `- [a] ]`) stays in the entry's
171//! slot, as more nodes, rather than opening another entry on that line.
172//!
173//! The zero-width `NULL ""` token renders as nothing, so round-trips are
174//! lossless. This is distinct from a programmatically-written null,
175//! which uses `SCALAR { NULL "null" }` (textual, four characters) so
176//! the value is visible in the output.
177//!
178//! Consequences for mutation helpers:
179//!
180//! - Iterating `SEQUENCE_ENTRY` children with `matches!(kind, SCALAR |
181//!   MAPPING | SEQUENCE | ALIAS | TAGGED_NODE)` finds the value in
182//!   every entry, including implicit-null ones. `len` / `get` /
183//!   `set` / `remove` therefore share the same set of indexes.
184//! - `Scalar::is_null()` returns `true` for the zero-width form.
185//! - `set(i, real_value)` on an implicit-null entry may need to insert
186//!   a separating WHITESPACE that the original didn't have (the value
187//!   was zero-width so no separator was needed). See `Sequence::set`
188//!   for the `after_dash` pattern used in block sequences.
189//!
190//! ## No stacked INDENTs, no double trailing NEWLINEs
191//!
192//! Two adjacent INDENT tokens as direct children of the same node
193//! concatenate at render time and produce visibly-wrong indentation
194//! (this was the shape of issue #38). Two adjacent NEWLINE tokens at the
195//! tail of a MAPPING_ENTRY or SEQUENCE_ENTRY render as a stray blank
196//! line (issue #18).
197//!
198//! Both are checked by [`crate::debug::validate_tree`]; mutation helpers
199//! must not produce either. Note the check for double NEWLINE is scoped
200//! to *entries*, not their parent MAPPING / SEQUENCE containers: bare
201//! NEWLINEs between entries (blank-line separators) are valid formatting
202//! and appear at the container level.
203//!
204//! ## Flow separators live on the previous entry
205//!
206//! Inside a flow mapping or sequence, the COMMA between two entries is
207//! stored after the *previous* entry's KEY / VALUE / SCALAR, not inside
208//! the entry it precedes, along with anything before it and the rest of
209//! its line: white space, a comment and the NEWLINE. An entry without a
210//! comma owns the rest of its line the same way. Comment and blank lines
211//! after that, and the INDENT of the next line, belong to the collection,
212//! as between block entries:
213//!
214//! ```text
215//! # {a: 1, # c\n # d\n b: 2\n}
216//! MAPPING
217//!   LEFT_BRACE "{"
218//!   MAPPING_ENTRY ("a: 1, # c\n")
219//!   INDENT " "
220//!   COMMENT "# d"
221//!   NEWLINE "\n"
222//!   INDENT " "
223//!   MAPPING_ENTRY ("b: 2\n")
224//!   RIGHT_BRACE "}"
225//! ```
226//!
227//! Inserters that append a new entry must add a `, ` suffix to the entry
228//! they follow, not a `,` prefix on themselves. See
229//! [`trivia::FlowSeparator`].
230//!
231//! ## Prefer targeted splices over whole-node rebuilds
232//!
233//! Reconstructing an entire MAPPING or SEQUENCE from a builder discards
234//! anchors, tags, comments, quoting styles, and any tokens the fix
235//! didn't know about. Mutation helpers should locate the specific
236//! `NodeOrToken` range they need to change and use `splice_children`,
237//! preserving the surrounding tokens.
238
239use crate::lex::SyntaxKind;
240
241/// YAML language type for rowan.
242#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
243pub enum Lang {}
244
245impl rowan::Language for Lang {
246    type Kind = SyntaxKind;
247
248    fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind {
249        // SAFETY: SyntaxKind is #[repr(u16)] with contiguous discriminants
250        // from ROOT = 0 through EOF, so every value in that range is a valid
251        // variant. The bound is checked in every build, not just debug: a
252        // stray value here would otherwise be undefined behaviour rather
253        // than a panic.
254        assert!(
255            raw.0 <= SyntaxKind::EOF as u16,
256            "raw SyntaxKind value {} is out of range (max {})",
257            raw.0,
258            SyntaxKind::EOF as u16,
259        );
260        unsafe { std::mem::transmute::<u16, SyntaxKind>(raw.0) }
261    }
262
263    fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind {
264        kind.into()
265    }
266}
267
268pub type SyntaxNode = rowan::SyntaxNode<Lang>;
269pub type SyntaxToken = rowan::SyntaxToken<Lang>;
270
271/// Detach `anchor` with the whitespace separating it from what follows it
272/// on the same line, or else the space before it, so `k: &a 1` becomes
273/// `k: 1` and `k: &a` followed by a block becomes `k:`.
274pub(crate) fn detach_anchor(anchor: &SyntaxToken) {
275    let separator = anchor
276        .next_token()
277        .filter(|t| t.kind() == SyntaxKind::WHITESPACE)
278        .or_else(|| {
279            anchor
280                .prev_token()
281                .filter(|t| t.kind() == SyntaxKind::WHITESPACE)
282        });
283    if let Some(separator) = separator {
284        separator.detach();
285    }
286    anchor.detach();
287}
288
289/// Build a standalone `SyntaxToken` of `kind` with `text`, ready to
290/// splice into a parent's child list via `splice_children`.
291pub(crate) fn fresh_token(kind: SyntaxKind, text: &str) -> SyntaxToken {
292    let mut builder = rowan::GreenNodeBuilder::new();
293    builder.start_node(SyntaxKind::ROOT.into());
294    builder.token(kind.into(), text);
295    builder.finish_node();
296    SyntaxNode::new_root_mut(builder.finish())
297        .first_token()
298        .expect("just built a token")
299}
300
301/// Put `ws` and `comment` at the end of the first line of `value`, a
302/// mutable node holding a block scalar: on its header line, where the
303/// parser keeps a comment following the indicator.
304pub(crate) fn insert_header_comment(value: &SyntaxNode, ws: &str, comment: &str) {
305    let header_end = value
306        .descendants_with_tokens()
307        .find(|c| c.kind() == SyntaxKind::NEWLINE)
308        .expect("a block scalar's header ends with a line break");
309    let parent = header_end.parent().expect("a token has a parent");
310    let idx = header_end.index();
311    insert_children(
312        &parent,
313        idx,
314        vec![
315            fresh_token(SyntaxKind::WHITESPACE, ws).into(),
316            fresh_token(SyntaxKind::COMMENT, comment).into(),
317        ],
318    );
319}
320
321/// Whether `key` ends in an anchor or alias name, which would take in a
322/// `:` written right after it: `*a:` is an alias to `a:`.
323pub(crate) fn ends_in_property_name(key: &SyntaxNode) -> bool {
324    key.last_token()
325        .is_some_and(|t| matches!(t.kind(), SyntaxKind::ANCHOR | SyntaxKind::REFERENCE))
326}
327
328/// Put a space between the key of `entry` and the `:` right after it if
329/// the key [ends in a property name](ends_in_property_name).
330pub(crate) fn separate_key_from_colon(entry: &SyntaxNode) {
331    let Some(key) = entry_key(entry).filter(ends_in_property_name) else {
332        return;
333    };
334    if key
335        .next_sibling_or_token()
336        .is_some_and(|n| n.kind() == SyntaxKind::COLON)
337    {
338        let i = key.index() + 1;
339        insert_children(
340            entry,
341            i,
342            vec![fresh_token(SyntaxKind::WHITESPACE, " ").into()],
343        );
344    }
345}
346
347/// The node at the place of `node` in an immutable copy of its tree.
348///
349/// A mutable rowan tree works out each node's offset by walking up to the
350/// root, so every step of a traversal costs the depth it is at. A pass that
351/// only reads a whole tree runs on a copy to stay linear in its size.
352pub(crate) fn immutable_copy(node: &SyntaxNode) -> SyntaxNode {
353    let path: Vec<(usize, rowan::TextRange)> = node
354        .ancestors()
355        .take_while(|n| n.parent().is_some())
356        .map(|n| (n.index(), n.text_range()))
357        .collect();
358    let root = node.ancestors().last().unwrap_or_else(|| node.clone());
359    let missing = "the copy has the same shape as the tree";
360    path.iter().rev().fold(
361        SyntaxNode::new_root(root.green().into_owned()),
362        |parent, &(index, range)| {
363            // Find the child by its range rather than counting through the
364            // ones before it, then step past any empty siblings beside it.
365            let mut child = parent.child_or_token_at_range(range).expect(missing);
366            while child.index() < index {
367                child = child.next_sibling_or_token().expect(missing);
368            }
369            while child.index() > index {
370                child = child.prev_sibling_or_token().expect(missing);
371            }
372            child.into_node().expect(missing)
373        },
374    )
375}
376
377/// The first direct child node of `parent` with the given `kind`, if any.
378fn child_of_kind(parent: &SyntaxNode, kind: SyntaxKind) -> Option<SyntaxNode> {
379    parent.children().find(|n| n.kind() == kind)
380}
381
382/// Does `parent` have a direct child token whose kind satisfies `pred`?
383///
384/// Flow/block style and several syntactic properties are decided by the
385/// presence of a marker token (`{`, `[`, `|`, `:`) directly under a node,
386/// never a nested one, so the search deliberately stays one level deep.
387pub(crate) fn has_child_token(parent: &SyntaxNode, pred: impl Fn(SyntaxKind) -> bool) -> bool {
388    parent
389        .children_with_tokens()
390        .any(|el| el.as_token().is_some_and(|t| pred(t.kind())))
391}
392
393/// The first direct child token of `parent` with the given `kind`, if any.
394pub(crate) fn child_token(parent: &SyntaxNode, kind: SyntaxKind) -> Option<SyntaxToken> {
395    parent
396        .children_with_tokens()
397        .filter_map(|el| el.into_token())
398        .find(|t| t.kind() == kind)
399}
400
401/// Replace the children of `parent` in `range` with `children`.
402///
403/// rowan 0.16's `SyntaxNode::splice_children` detaches the range while
404/// iterating over it, so it stops after the first element and leaves the
405/// rest in place. Use this whenever the range can hold more than one child.
406pub(crate) fn replace_children(
407    parent: &SyntaxNode,
408    range: std::ops::Range<usize>,
409    mut children: Vec<rowan::NodeOrToken<SyntaxNode, SyntaxToken>>,
410) {
411    match_line_breaks(parent, &mut children);
412    for i in range.clone().rev() {
413        match parent
414            .children_with_tokens()
415            .nth(i)
416            .expect("range within the parent's children")
417        {
418            rowan::NodeOrToken::Node(n) => n.detach(),
419            rowan::NodeOrToken::Token(t) => t.detach(),
420        }
421    }
422    parent.splice_children(range.start..range.start, children);
423}
424
425/// Insert `children` into `parent`'s child list at `at`.
426pub(crate) fn insert_children(
427    parent: &SyntaxNode,
428    at: usize,
429    children: Vec<rowan::NodeOrToken<SyntaxNode, SyntaxToken>>,
430) {
431    replace_children(parent, at..at, children);
432}
433
434/// Append `children` at the end of `parent`'s child list.
435pub(crate) fn append_children(
436    parent: &SyntaxNode,
437    children: Vec<rowan::NodeOrToken<SyntaxNode, SyntaxToken>>,
438) {
439    insert_children(parent, parent.children_with_tokens().count(), children);
440}
441
442/// Rewrite the `\n` line breaks in `elements`, about to go into the tree
443/// `node` is in, to the document's own line break.
444///
445/// Builders and fresh tokens always write `\n`; in a document using `\r\n`
446/// or `\r` they would mix line endings. The document's line break is its
447/// first one, as for the lexer's mixed line ending check. A document
448/// without one keeps `\n`.
449fn match_line_breaks(
450    node: &SyntaxNode,
451    elements: &mut [rowan::NodeOrToken<SyntaxNode, SyntaxToken>],
452) {
453    let tokens = |e: &rowan::NodeOrToken<SyntaxNode, SyntaxToken>| {
454        let tokens: Vec<_> = match e {
455            rowan::NodeOrToken::Node(n) => n
456                .descendants_with_tokens()
457                .filter_map(|c| c.into_token())
458                .collect(),
459            rowan::NodeOrToken::Token(t) => vec![t.clone()],
460        };
461        tokens
462            .into_iter()
463            .filter(|t| t.text().contains('\n') && !t.text().contains('\r'))
464            .collect::<Vec<_>>()
465    };
466    let to_rewrite: Vec<_> = elements.iter().map(tokens).collect();
467    if to_rewrite.iter().all(Vec::is_empty) {
468        return;
469    }
470    let root = node.ancestors().last().expect("a node is its own ancestor");
471    let Some(line_break) = root
472        .descendants_with_tokens()
473        .filter_map(|c| c.into_token())
474        .find(|t| t.kind() == SyntaxKind::NEWLINE)
475        .map(|t| t.text().to_string())
476        .filter(|b| b != "\n")
477    else {
478        return;
479    };
480    for (element, tokens) in elements.iter_mut().zip(to_rewrite) {
481        for token in tokens {
482            let fresh = fresh_token(token.kind(), &token.text().replace('\n', &line_break));
483            match element {
484                rowan::NodeOrToken::Token(_) => *element = fresh.into(),
485                rowan::NodeOrToken::Node(_) => {
486                    let parent = token.parent().expect("a token in a node has a parent");
487                    let i = token.index();
488                    parent.splice_children(i..i + 1, vec![fresh.into()]);
489                }
490            }
491        }
492    }
493}
494
495/// Lay out the lines after `entry`, a block collection entry just
496/// inserted, so that a block scalar its value ends in does not take them
497/// in.
498pub(crate) fn settle_lines_after_inserted_entry(entry: &SyntaxNode) {
499    move_blank_lines_ahead_of_kept_scalar(entry);
500    replaced_value::settle_lines_after_block_scalar(entry);
501}
502
503/// Move the blank lines straight after `entry`, a block collection entry
504/// just inserted, to in front of its line, if its value ends in a kept
505/// (`|+`, `>+`) block scalar.
506///
507/// Such a scalar takes every blank line after it into its value, so the
508/// new entry goes after the blank lines rather than before them, leaving
509/// the text around it as it was.
510fn move_blank_lines_ahead_of_kept_scalar(entry: &SyntaxNode) {
511    if !ends_with_kept_block_scalar(entry) {
512        return;
513    }
514    let Some(line_end) = entry
515        .last_token()
516        .filter(|t| t.kind() == SyntaxKind::NEWLINE)
517    else {
518        return;
519    };
520    let mut lines = Vec::new();
521    let mut next = line_end.next_token();
522    loop {
523        let space = next
524            .clone()
525            .filter(|t| matches!(t.kind(), SyntaxKind::INDENT | SyntaxKind::WHITESPACE));
526        let Some(line_break) = space
527            .as_ref()
528            .map_or(next.clone(), |t| t.next_token())
529            .filter(|t| t.kind() == SyntaxKind::NEWLINE)
530        else {
531            break;
532        };
533        next = line_break.next_token();
534        lines.extend(space);
535        lines.push(line_break);
536    }
537    if lines.is_empty() {
538        return;
539    }
540    let moved: Vec<_> = lines
541        .into_iter()
542        .map(|t| {
543            t.detach();
544            fresh_token(t.kind(), t.text()).into()
545        })
546        .collect();
547
548    // The lines go in front of the outermost element that starts the
549    // entry's line, within the document.
550    let first = entry.first_token().expect("an inserted entry has tokens");
551    let line_start = std::iter::successors(Some(first), |t| {
552        t.prev_token()
553            .filter(|p| !matches!(p.kind(), SyntaxKind::NEWLINE | SyntaxKind::BOM))
554    })
555    .last()
556    .expect("successors yields its first item");
557    let mut at = rowan::NodeOrToken::Token(line_start.clone());
558    while let Some(parent) = at
559        .parent()
560        .filter(|p| p.kind() != SyntaxKind::DOCUMENT && p.first_token() == Some(line_start.clone()))
561    {
562        at = rowan::NodeOrToken::Node(parent);
563    }
564    let parent = at.parent().expect("an inserted entry is in a document");
565    let index = at.index();
566    insert_children(&parent, index, moved);
567}
568
569/// Whether the last node `entry` holds is a kept (`|+`, `>+`) block scalar.
570fn ends_with_kept_block_scalar(entry: &SyntaxNode) -> bool {
571    ending_block_scalar(entry).is_some_and(|scalar| {
572        scalar_node::block_scalar_indicators(&scalar).0 == scalar_node::Chomping::Keep
573    })
574}
575
576/// The block scalar that is the last node `entry` holds, if any.
577pub(crate) fn ending_block_scalar(entry: &SyntaxNode) -> Option<SyntaxNode> {
578    let tokens: Vec<_> = entry
579        .descendants_with_tokens()
580        .filter_map(|c| c.into_token())
581        .collect();
582    tokens
583        .iter()
584        .rev()
585        .filter(|t| !t.text().is_empty())
586        .find_map(|t| {
587            let parent = t.parent()?;
588            let layout = matches!(
589                t.kind(),
590                SyntaxKind::NEWLINE | SyntaxKind::WHITESPACE | SyntaxKind::INDENT
591            );
592            match parent.kind() {
593                SyntaxKind::SCALAR => Some(Some(parent)),
594                _ if layout => None,
595                _ => Some(None),
596            }
597        })
598        .flatten()
599        .filter(is_block_scalar)
600}
601
602/// The `children_with_tokens()` index of `parent`'s `index`-th child node of
603/// the given kind.
604pub(crate) fn nth_entry_index(
605    parent: &SyntaxNode,
606    kind: SyntaxKind,
607    index: usize,
608) -> Option<usize> {
609    parent
610        .children_with_tokens()
611        .enumerate()
612        .filter(|(_, c)| c.as_node().is_some_and(|n| n.kind() == kind))
613        .map(|(i, _)| i)
614        .nth(index)
615}
616
617/// How strings written into the flow collection `collection` are quoted.
618///
619/// If the string scalars among its keys and items are all double-quoted,
620/// as in JSON, new strings are too; if any is plain or single-quoted, they
621/// are quoted only where needed. A collection without string scalars of its
622/// own goes by the nearest enclosing flow collection that has some, and
623/// failing that quotes only where needed.
624pub(crate) fn flow_quoting(collection: &SyntaxNode) -> crate::as_yaml::FlowQuoting {
625    use crate::as_yaml::FlowQuoting;
626    let is_flow = |n: &SyntaxNode| starts_a_flow_collection(n) || mapping::is_flow_pair(n);
627    let own_quoting = |n: SyntaxNode| {
628        let mut strings = n
629            .children()
630            .flat_map(|entry| entry.children())
631            .flat_map(|c| match c.kind() {
632                SyntaxKind::KEY | SyntaxKind::VALUE => c.children().collect(),
633                _ => vec![c],
634            })
635            .filter(|c| c.kind() == SyntaxKind::SCALAR)
636            .filter_map(|c| c.first_token())
637            .filter(|t| t.kind() == SyntaxKind::STRING)
638            .peekable();
639        strings.peek()?;
640        Some(if strings.all(|t| t.text().starts_with('"')) {
641            FlowQuoting::Double
642        } else {
643            FlowQuoting::Minimal
644        })
645    };
646    collection
647        .ancestors()
648        .filter(|n| matches!(n.kind(), SyntaxKind::MAPPING | SyntaxKind::SEQUENCE))
649        .take_while(is_flow)
650        .find_map(own_quoting)
651        .unwrap_or(FlowQuoting::Minimal)
652}
653
654/// Emit `key`'s content for use inside a `KEY` node, of a flow mapping
655/// quoting strings as `flow` says if it is set.
656///
657/// A block scalar or block collection, tagged or not, is written in flow
658/// form instead: a block collection cannot be an implicit key, and a block
659/// scalar does not read back as the same key, as the entry's `:` lands
660/// inside the block. `indent` is the column of the entry, which a
661/// multi-line key's continuation lines are shifted to clear.
662pub(crate) fn build_key_content(
663    builder: &mut rowan::GreenNodeBuilder,
664    key: &impl crate::AsYaml,
665    indent: usize,
666    flow: Option<crate::as_yaml::FlowQuoting>,
667) {
668    if let Some(quoting) = flow {
669        // Flow context already rules out block scalars.
670        key.build_flow_content(builder, indent, quoting);
671        return;
672    }
673    if let Some(key) = mapping::flow_form(key, 2, crate::as_yaml::FlowQuoting::Minimal) {
674        return build_key_content(builder, &key, indent, None);
675    }
676    // flow_form leaves raw values that claim to be inline alone, but a
677    // string with line breaks still renders as a block scalar, and a set as
678    // a block mapping below its tag. In flow context both fit on one line.
679    let spans_lines = key.value_node().is_none() && {
680        let mut probe = rowan::GreenNodeBuilder::new();
681        probe.start_node(SyntaxKind::ROOT.into());
682        key.build_content(&mut probe, 0, false);
683        probe.finish_node();
684        SyntaxNode::new_root(probe.finish())
685            .text()
686            .contains_char('\n')
687    };
688    key.build_content(builder, indent, spans_lines);
689}
690
691/// Whether `key`, as [`build_key_content`] writes it, spans several lines,
692/// which in a block mapping only an explicit key may do.
693pub(crate) fn key_spans_lines(key: &impl crate::AsYaml) -> bool {
694    let mut builder = rowan::GreenNodeBuilder::new();
695    builder.start_node(SyntaxKind::ROOT.into());
696    build_key_content(&mut builder, key, 0, None);
697    builder.finish_node();
698    SyntaxNode::new_root(builder.finish())
699        .text()
700        .contains_char('\n')
701}
702
703/// The `KEY` child of a `MAPPING_ENTRY`.
704pub(crate) fn entry_key(entry: &SyntaxNode) -> Option<SyntaxNode> {
705    child_of_kind(entry, SyntaxKind::KEY)
706}
707
708/// The `VALUE` child of a `MAPPING_ENTRY` or `SEQUENCE_ENTRY`.
709pub(crate) fn entry_value(entry: &SyntaxNode) -> Option<SyntaxNode> {
710    child_of_kind(entry, SyntaxKind::VALUE)
711}
712
713/// The node a KEY, VALUE or other holder wraps, as a
714/// [`YamlNode`](crate::as_yaml::YamlNode).
715pub(crate) fn held_node(holder: &SyntaxNode) -> Option<crate::as_yaml::YamlNode> {
716    holder
717        .children()
718        .next()
719        .and_then(crate::as_yaml::YamlNode::from_syntax)
720}
721
722/// A macro to create AST node wrappers.
723macro_rules! ast_node {
724    ($ast:ident, $kind:ident, $doc:expr) => {
725        #[doc = $doc]
726        #[doc = ""]
727        #[doc = "**Note:** This type uses interior mutability through the rowan library."]
728        #[doc = "Mutation methods work even when called through `&self`. See the crate-level"]
729        #[doc = "documentation for details on the mutability model."]
730        #[derive(Clone, PartialEq, Eq, Hash)]
731        pub struct $ast(pub(crate) SyntaxNode);
732
733        impl std::fmt::Debug for $ast {
734            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
735                f.debug_struct(stringify!($ast))
736                    .field("syntax", &self.0)
737                    .finish()
738            }
739        }
740
741        impl AstNode for $ast {
742            type Language = Lang;
743
744            fn can_cast(kind: SyntaxKind) -> bool {
745                kind == SyntaxKind::$kind
746            }
747
748            fn cast(syntax: SyntaxNode) -> Option<Self> {
749                if Self::can_cast(syntax.kind()) {
750                    Some(Self(syntax))
751                } else {
752                    None
753                }
754            }
755
756            fn syntax(&self) -> &SyntaxNode {
757                &self.0
758            }
759        }
760
761        impl std::fmt::Display for $ast {
762            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
763                write!(f, "{}", self.0.text())
764            }
765        }
766    };
767}
768
769pub(crate) use ast_node;
770/// Emit the `byte_range` / `start_position` / `end_position` trio for a
771/// wrapper whose `self.0` exposes `text_range()`. `$what` names the node
772/// in the generated docs ("sequence", "comment", ...).
773macro_rules! ast_node_spans {
774    ($what:expr) => {
775        #[doc = concat!("Get the byte offset range of this ", $what, " in the source text.")]
776        #[doc = ""]
777        #[doc = "Returns the start and end byte offsets as a `TextPosition`."]
778        pub fn byte_range(&self) -> crate::TextPosition {
779            self.0.text_range().into()
780        }
781
782        #[doc = concat!("Get the line and column where this ", $what, " starts.")]
783        #[doc = ""]
784        #[doc = "Requires the original source text to calculate line/column from byte"]
785        #[doc = "offsets. Line and column numbers are 1-indexed."]
786        #[doc = ""]
787        #[doc = "# Arguments"]
788        #[doc = ""]
789        #[doc = "* `source_text` - The original YAML source text"]
790        pub fn start_position(&self, source_text: &str) -> crate::LineColumn {
791            crate::byte_offset_to_line_column(source_text, self.byte_range().start as usize)
792        }
793
794        #[doc = concat!("Get the line and column where this ", $what, " ends.")]
795        #[doc = ""]
796        #[doc = "Requires the original source text to calculate line/column from byte"]
797        #[doc = "offsets. Line and column numbers are 1-indexed."]
798        #[doc = ""]
799        #[doc = "# Arguments"]
800        #[doc = ""]
801        #[doc = "* `source_text` - The original YAML source text"]
802        pub fn end_position(&self, source_text: &str) -> crate::LineColumn {
803            crate::byte_offset_to_line_column(source_text, self.byte_range().end as usize)
804        }
805    };
806}
807
808pub(crate) use ast_node_spans;
809
810// Node modules
811pub mod alias_node;
812pub mod comment;
813pub mod directive;
814pub mod document;
815mod empty_collection;
816pub mod entry;
817mod kept_aliases;
818pub mod mapping;
819mod predicates;
820mod replaced_value;
821pub mod scalar_node;
822pub mod sequence;
823pub mod tagged_node;
824pub(crate) mod trivia;
825
826pub(crate) use predicates::{
827    in_flow_collection, is_block_scalar, is_quoted_scalar, starts_a_flow_collection,
828};
829
830// Re-exports
831pub use alias_node::Alias;
832pub use comment::Comment;
833pub use directive::Directive;
834pub use document::Document;
835pub use entry::{Entry, OccupiedEntry, VacantEntry};
836pub use mapping::{Mapping, MappingEntry};
837pub use scalar_node::{Scalar, ScalarConversionError};
838pub use sequence::Sequence;
839pub use tagged_node::TaggedNode;
840
841#[cfg(test)]
842mod tests {
843    use super::*;
844
845    #[test]
846    fn test_replace_children_removes_whole_range() {
847        let mut builder = rowan::GreenNodeBuilder::new();
848        builder.start_node(SyntaxKind::ROOT.into());
849        for text in ["a", "b", "c", "d", "e"] {
850            builder.token(SyntaxKind::STRING.into(), text);
851        }
852        builder.finish_node();
853        let root = SyntaxNode::new_root_mut(builder.finish());
854
855        replace_children(
856            &root,
857            1..4,
858            vec![fresh_token(SyntaxKind::STRING, "X").into()],
859        );
860        assert_eq!(root.to_string(), "aXe");
861    }
862}