use crate::lex::SyntaxKind;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Lang {}
impl rowan::Language for Lang {
type Kind = SyntaxKind;
fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind {
assert!(
raw.0 <= SyntaxKind::EOF as u16,
"raw SyntaxKind value {} is out of range (max {})",
raw.0,
SyntaxKind::EOF as u16,
);
unsafe { std::mem::transmute::<u16, SyntaxKind>(raw.0) }
}
fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind {
kind.into()
}
}
pub type SyntaxNode = rowan::SyntaxNode<Lang>;
pub type SyntaxToken = rowan::SyntaxToken<Lang>;
pub(crate) fn detach_anchor(anchor: &SyntaxToken) {
let separator = anchor
.next_token()
.filter(|t| t.kind() == SyntaxKind::WHITESPACE)
.or_else(|| {
anchor
.prev_token()
.filter(|t| t.kind() == SyntaxKind::WHITESPACE)
});
if let Some(separator) = separator {
separator.detach();
}
anchor.detach();
}
pub(crate) fn fresh_token(kind: SyntaxKind, text: &str) -> SyntaxToken {
let mut builder = rowan::GreenNodeBuilder::new();
builder.start_node(SyntaxKind::ROOT.into());
builder.token(kind.into(), text);
builder.finish_node();
SyntaxNode::new_root_mut(builder.finish())
.first_token()
.expect("just built a token")
}
pub(crate) fn insert_header_comment(value: &SyntaxNode, ws: &str, comment: &str) {
let header_end = value
.descendants_with_tokens()
.find(|c| c.kind() == SyntaxKind::NEWLINE)
.expect("a block scalar's header ends with a line break");
let parent = header_end.parent().expect("a token has a parent");
let idx = header_end.index();
insert_children(
&parent,
idx,
vec![
fresh_token(SyntaxKind::WHITESPACE, ws).into(),
fresh_token(SyntaxKind::COMMENT, comment).into(),
],
);
}
pub(crate) fn ends_in_property_name(key: &SyntaxNode) -> bool {
key.last_token()
.is_some_and(|t| matches!(t.kind(), SyntaxKind::ANCHOR | SyntaxKind::REFERENCE))
}
pub(crate) fn separate_key_from_colon(entry: &SyntaxNode) {
let Some(key) = entry_key(entry).filter(ends_in_property_name) else {
return;
};
if key
.next_sibling_or_token()
.is_some_and(|n| n.kind() == SyntaxKind::COLON)
{
let i = key.index() + 1;
insert_children(
entry,
i,
vec![fresh_token(SyntaxKind::WHITESPACE, " ").into()],
);
}
}
pub(crate) fn immutable_copy(node: &SyntaxNode) -> SyntaxNode {
let path: Vec<(usize, rowan::TextRange)> = node
.ancestors()
.take_while(|n| n.parent().is_some())
.map(|n| (n.index(), n.text_range()))
.collect();
let root = node.ancestors().last().unwrap_or_else(|| node.clone());
let missing = "the copy has the same shape as the tree";
path.iter().rev().fold(
SyntaxNode::new_root(root.green().into_owned()),
|parent, &(index, range)| {
let mut child = parent.child_or_token_at_range(range).expect(missing);
while child.index() < index {
child = child.next_sibling_or_token().expect(missing);
}
while child.index() > index {
child = child.prev_sibling_or_token().expect(missing);
}
child.into_node().expect(missing)
},
)
}
fn child_of_kind(parent: &SyntaxNode, kind: SyntaxKind) -> Option<SyntaxNode> {
parent.children().find(|n| n.kind() == kind)
}
pub(crate) fn has_child_token(parent: &SyntaxNode, pred: impl Fn(SyntaxKind) -> bool) -> bool {
parent
.children_with_tokens()
.any(|el| el.as_token().is_some_and(|t| pred(t.kind())))
}
pub(crate) fn child_token(parent: &SyntaxNode, kind: SyntaxKind) -> Option<SyntaxToken> {
parent
.children_with_tokens()
.filter_map(|el| el.into_token())
.find(|t| t.kind() == kind)
}
pub(crate) fn replace_children(
parent: &SyntaxNode,
range: std::ops::Range<usize>,
mut children: Vec<rowan::NodeOrToken<SyntaxNode, SyntaxToken>>,
) {
match_line_breaks(parent, &mut children);
for i in range.clone().rev() {
match parent
.children_with_tokens()
.nth(i)
.expect("range within the parent's children")
{
rowan::NodeOrToken::Node(n) => n.detach(),
rowan::NodeOrToken::Token(t) => t.detach(),
}
}
parent.splice_children(range.start..range.start, children);
}
pub(crate) fn insert_children(
parent: &SyntaxNode,
at: usize,
children: Vec<rowan::NodeOrToken<SyntaxNode, SyntaxToken>>,
) {
replace_children(parent, at..at, children);
}
pub(crate) fn append_children(
parent: &SyntaxNode,
children: Vec<rowan::NodeOrToken<SyntaxNode, SyntaxToken>>,
) {
insert_children(parent, parent.children_with_tokens().count(), children);
}
fn match_line_breaks(
node: &SyntaxNode,
elements: &mut [rowan::NodeOrToken<SyntaxNode, SyntaxToken>],
) {
let tokens = |e: &rowan::NodeOrToken<SyntaxNode, SyntaxToken>| {
let tokens: Vec<_> = match e {
rowan::NodeOrToken::Node(n) => n
.descendants_with_tokens()
.filter_map(|c| c.into_token())
.collect(),
rowan::NodeOrToken::Token(t) => vec![t.clone()],
};
tokens
.into_iter()
.filter(|t| t.text().contains('\n') && !t.text().contains('\r'))
.collect::<Vec<_>>()
};
let to_rewrite: Vec<_> = elements.iter().map(tokens).collect();
if to_rewrite.iter().all(Vec::is_empty) {
return;
}
let root = node.ancestors().last().expect("a node is its own ancestor");
let Some(line_break) = root
.descendants_with_tokens()
.filter_map(|c| c.into_token())
.find(|t| t.kind() == SyntaxKind::NEWLINE)
.map(|t| t.text().to_string())
.filter(|b| b != "\n")
else {
return;
};
for (element, tokens) in elements.iter_mut().zip(to_rewrite) {
for token in tokens {
let fresh = fresh_token(token.kind(), &token.text().replace('\n', &line_break));
match element {
rowan::NodeOrToken::Token(_) => *element = fresh.into(),
rowan::NodeOrToken::Node(_) => {
let parent = token.parent().expect("a token in a node has a parent");
let i = token.index();
parent.splice_children(i..i + 1, vec![fresh.into()]);
}
}
}
}
}
pub(crate) fn settle_lines_after_inserted_entry(entry: &SyntaxNode) {
move_blank_lines_ahead_of_kept_scalar(entry);
replaced_value::settle_lines_after_block_scalar(entry);
}
fn move_blank_lines_ahead_of_kept_scalar(entry: &SyntaxNode) {
if !ends_with_kept_block_scalar(entry) {
return;
}
let Some(line_end) = entry
.last_token()
.filter(|t| t.kind() == SyntaxKind::NEWLINE)
else {
return;
};
let mut lines = Vec::new();
let mut next = line_end.next_token();
loop {
let space = next
.clone()
.filter(|t| matches!(t.kind(), SyntaxKind::INDENT | SyntaxKind::WHITESPACE));
let Some(line_break) = space
.as_ref()
.map_or(next.clone(), |t| t.next_token())
.filter(|t| t.kind() == SyntaxKind::NEWLINE)
else {
break;
};
next = line_break.next_token();
lines.extend(space);
lines.push(line_break);
}
if lines.is_empty() {
return;
}
let moved: Vec<_> = lines
.into_iter()
.map(|t| {
t.detach();
fresh_token(t.kind(), t.text()).into()
})
.collect();
let first = entry.first_token().expect("an inserted entry has tokens");
let line_start = std::iter::successors(Some(first), |t| {
t.prev_token()
.filter(|p| !matches!(p.kind(), SyntaxKind::NEWLINE | SyntaxKind::BOM))
})
.last()
.expect("successors yields its first item");
let mut at = rowan::NodeOrToken::Token(line_start.clone());
while let Some(parent) = at
.parent()
.filter(|p| p.kind() != SyntaxKind::DOCUMENT && p.first_token() == Some(line_start.clone()))
{
at = rowan::NodeOrToken::Node(parent);
}
let parent = at.parent().expect("an inserted entry is in a document");
let index = at.index();
insert_children(&parent, index, moved);
}
fn ends_with_kept_block_scalar(entry: &SyntaxNode) -> bool {
ending_block_scalar(entry).is_some_and(|scalar| {
scalar_node::block_scalar_indicators(&scalar).0 == scalar_node::Chomping::Keep
})
}
pub(crate) fn ending_block_scalar(entry: &SyntaxNode) -> Option<SyntaxNode> {
let tokens: Vec<_> = entry
.descendants_with_tokens()
.filter_map(|c| c.into_token())
.collect();
tokens
.iter()
.rev()
.filter(|t| !t.text().is_empty())
.find_map(|t| {
let parent = t.parent()?;
let layout = matches!(
t.kind(),
SyntaxKind::NEWLINE | SyntaxKind::WHITESPACE | SyntaxKind::INDENT
);
match parent.kind() {
SyntaxKind::SCALAR => Some(Some(parent)),
_ if layout => None,
_ => Some(None),
}
})
.flatten()
.filter(is_block_scalar)
}
pub(crate) fn nth_entry_index(
parent: &SyntaxNode,
kind: SyntaxKind,
index: usize,
) -> Option<usize> {
parent
.children_with_tokens()
.enumerate()
.filter(|(_, c)| c.as_node().is_some_and(|n| n.kind() == kind))
.map(|(i, _)| i)
.nth(index)
}
pub(crate) fn flow_quoting(collection: &SyntaxNode) -> crate::as_yaml::FlowQuoting {
use crate::as_yaml::FlowQuoting;
let is_flow = |n: &SyntaxNode| starts_a_flow_collection(n) || mapping::is_flow_pair(n);
let own_quoting = |n: SyntaxNode| {
let mut strings = n
.children()
.flat_map(|entry| entry.children())
.flat_map(|c| match c.kind() {
SyntaxKind::KEY | SyntaxKind::VALUE => c.children().collect(),
_ => vec![c],
})
.filter(|c| c.kind() == SyntaxKind::SCALAR)
.filter_map(|c| c.first_token())
.filter(|t| t.kind() == SyntaxKind::STRING)
.peekable();
strings.peek()?;
Some(if strings.all(|t| t.text().starts_with('"')) {
FlowQuoting::Double
} else {
FlowQuoting::Minimal
})
};
collection
.ancestors()
.filter(|n| matches!(n.kind(), SyntaxKind::MAPPING | SyntaxKind::SEQUENCE))
.take_while(is_flow)
.find_map(own_quoting)
.unwrap_or(FlowQuoting::Minimal)
}
pub(crate) fn build_key_content(
builder: &mut rowan::GreenNodeBuilder,
key: &impl crate::AsYaml,
indent: usize,
flow: Option<crate::as_yaml::FlowQuoting>,
) {
if let Some(quoting) = flow {
key.build_flow_content(builder, indent, quoting);
return;
}
if let Some(key) = mapping::flow_form(key, 2, crate::as_yaml::FlowQuoting::Minimal) {
return build_key_content(builder, &key, indent, None);
}
let spans_lines = key.value_node().is_none() && {
let mut probe = rowan::GreenNodeBuilder::new();
probe.start_node(SyntaxKind::ROOT.into());
key.build_content(&mut probe, 0, false);
probe.finish_node();
SyntaxNode::new_root(probe.finish())
.text()
.contains_char('\n')
};
key.build_content(builder, indent, spans_lines);
}
pub(crate) fn key_spans_lines(key: &impl crate::AsYaml) -> bool {
let mut builder = rowan::GreenNodeBuilder::new();
builder.start_node(SyntaxKind::ROOT.into());
build_key_content(&mut builder, key, 0, None);
builder.finish_node();
SyntaxNode::new_root(builder.finish())
.text()
.contains_char('\n')
}
pub(crate) fn entry_key(entry: &SyntaxNode) -> Option<SyntaxNode> {
child_of_kind(entry, SyntaxKind::KEY)
}
pub(crate) fn entry_value(entry: &SyntaxNode) -> Option<SyntaxNode> {
child_of_kind(entry, SyntaxKind::VALUE)
}
pub(crate) fn held_node(holder: &SyntaxNode) -> Option<crate::as_yaml::YamlNode> {
holder
.children()
.next()
.and_then(crate::as_yaml::YamlNode::from_syntax)
}
macro_rules! ast_node {
($ast:ident, $kind:ident, $doc:expr) => {
#[doc = $doc]
#[doc = ""]
#[doc = "**Note:** This type uses interior mutability through the rowan library."]
#[doc = "Mutation methods work even when called through `&self`. See the crate-level"]
#[doc = "documentation for details on the mutability model."]
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct $ast(pub(crate) SyntaxNode);
impl std::fmt::Debug for $ast {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct(stringify!($ast))
.field("syntax", &self.0)
.finish()
}
}
impl AstNode for $ast {
type Language = Lang;
fn can_cast(kind: SyntaxKind) -> bool {
kind == SyntaxKind::$kind
}
fn cast(syntax: SyntaxNode) -> Option<Self> {
if Self::can_cast(syntax.kind()) {
Some(Self(syntax))
} else {
None
}
}
fn syntax(&self) -> &SyntaxNode {
&self.0
}
}
impl std::fmt::Display for $ast {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0.text())
}
}
};
}
pub(crate) use ast_node;
macro_rules! ast_node_spans {
($what:expr) => {
#[doc = concat!("Get the byte offset range of this ", $what, " in the source text.")]
#[doc = ""]
#[doc = "Returns the start and end byte offsets as a `TextPosition`."]
pub fn byte_range(&self) -> crate::TextPosition {
self.0.text_range().into()
}
#[doc = concat!("Get the line and column where this ", $what, " starts.")]
#[doc = ""]
#[doc = "Requires the original source text to calculate line/column from byte"]
#[doc = "offsets. Line and column numbers are 1-indexed."]
#[doc = ""]
#[doc = "# Arguments"]
#[doc = ""]
#[doc = "* `source_text` - The original YAML source text"]
pub fn start_position(&self, source_text: &str) -> crate::LineColumn {
crate::byte_offset_to_line_column(source_text, self.byte_range().start as usize)
}
#[doc = concat!("Get the line and column where this ", $what, " ends.")]
#[doc = ""]
#[doc = "Requires the original source text to calculate line/column from byte"]
#[doc = "offsets. Line and column numbers are 1-indexed."]
#[doc = ""]
#[doc = "# Arguments"]
#[doc = ""]
#[doc = "* `source_text` - The original YAML source text"]
pub fn end_position(&self, source_text: &str) -> crate::LineColumn {
crate::byte_offset_to_line_column(source_text, self.byte_range().end as usize)
}
};
}
pub(crate) use ast_node_spans;
pub mod alias_node;
pub mod comment;
pub mod directive;
pub mod document;
mod empty_collection;
pub mod entry;
mod kept_aliases;
pub(crate) use kept_aliases::replace_alias_with_copy;
pub mod mapping;
mod predicates;
mod replaced_value;
pub mod scalar_node;
pub mod sequence;
pub mod tagged_node;
pub(crate) mod trivia;
pub(crate) use predicates::{
in_flow_collection, is_block_scalar, is_quoted_scalar, starts_a_flow_collection,
};
pub use alias_node::Alias;
pub use comment::{Comment, CommentTextError};
pub use directive::Directive;
pub use document::Document;
pub use entry::{Entry, OccupiedEntry, VacantEntry};
pub use mapping::{Mapping, MappingEntry};
pub use scalar_node::{Scalar, ScalarConversionError};
pub use sequence::Sequence;
pub use tagged_node::TaggedNode;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_replace_children_removes_whole_range() {
let mut builder = rowan::GreenNodeBuilder::new();
builder.start_node(SyntaxKind::ROOT.into());
for text in ["a", "b", "c", "d", "e"] {
builder.token(SyntaxKind::STRING.into(), text);
}
builder.finish_node();
let root = SyntaxNode::new_root_mut(builder.finish());
replace_children(
&root,
1..4,
vec![fresh_token(SyntaxKind::STRING, "X").into()],
);
assert_eq!(root.to_string(), "aXe");
}
}