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}