use super::empty_collection::{detach_empty_collection_placeholder_newline, render_empty};
use super::{
append_children, fresh_token, has_child_token, insert_children, is_block_scalar,
replace_children, starts_a_flow_collection, SyntaxNode, SyntaxToken,
};
use crate::lex::SyntaxKind;
pub(crate) fn line_break(entry: &SyntaxNode) -> Option<String> {
last_non_empty_token(entry)
.filter(|t| t.kind() == SyntaxKind::NEWLINE)
.map(|t| t.text().to_string())
}
pub(crate) fn ends_with_line_break(node: &SyntaxNode) -> bool {
last_non_empty_token(node).is_some_and(|t| t.kind() == SyntaxKind::NEWLINE)
}
pub(crate) fn ends_line(node: &SyntaxNode) -> bool {
last_non_empty_token(node).is_some_and(|t| match t.kind() {
SyntaxKind::NEWLINE => true,
SyntaxKind::INDENT => t
.prev_token()
.is_some_and(|p| p.kind() == SyntaxKind::NEWLINE),
_ => false,
})
}
pub(crate) fn drop_space_before_empty(content: &SyntaxNode) {
if !content.text().is_empty() {
return;
}
let ends_line = content
.last_token()
.and_then(|t| t.next_token())
.map_or(true, |t| t.kind() == SyntaxKind::NEWLINE);
let space = content
.first_token()
.and_then(|t| t.prev_token())
.filter(|t| t.kind() == SyntaxKind::WHITESPACE);
if let Some(space) = space.filter(|_| ends_line) {
space.detach();
}
}
pub(crate) fn last_non_empty_token(node: &SyntaxNode) -> Option<SyntaxToken> {
node.descendants_with_tokens()
.filter_map(|c| c.into_token())
.filter(|t| !t.text().is_empty())
.last()
}
pub(crate) enum Slot {
End,
After(SyntaxNode),
Before(SyntaxNode),
}
pub(crate) fn entry_column(collection: &SyntaxNode) -> usize {
let mut entries = collection.children().filter(|n| {
matches!(
n.kind(),
SyntaxKind::MAPPING_ENTRY | SyntaxKind::SEQUENCE_ENTRY
)
});
entries
.nth(1)
.and_then(|second| second.prev_sibling_or_token())
.and_then(|c| c.into_token())
.filter(|t| t.kind() == SyntaxKind::INDENT)
.map_or_else(
|| crate::column::node_column(collection),
|t| t.text().len(),
)
}
pub(crate) fn attach_entry(collection: &SyntaxNode, slot: Slot, entry: &SyntaxNode) {
let kind = entry.kind();
let column = entry_column(collection);
let indent =
|| (column > 0).then(|| fresh_token(SyntaxKind::INDENT, &" ".repeat(column)).into());
let mut first = false;
let mut open_end = false;
let (at, elements) = match slot {
Slot::Before(next) => {
let prev = next
.siblings(rowan::Direction::Prev)
.skip(1)
.find(|n| n.kind() == kind);
if let Some(prev) = prev {
terminate(&prev);
}
let mut elements = vec![entry.clone().into()];
elements.extend(indent());
(next.index(), elements)
}
Slot::After(prev) => {
open_end = !ends_with_line_break(&prev);
terminate(&prev);
let mut elements: Vec<_> = indent().into_iter().collect();
elements.push(entry.clone().into());
(prev.index() + 1, elements)
}
Slot::End => {
let mut elements = Vec::new();
if last_entry(collection, kind).is_some() {
elements.extend(indent());
} else {
first = true;
}
elements.push(entry.clone().into());
let (at, opened) = end_line(collection, end_slot_index(collection));
open_end = opened;
(at, elements)
}
};
insert_children(collection, at, elements);
if first {
detach_empty_collection_placeholder_newline(collection, entry);
}
super::settle_lines_after_inserted_entry(entry);
if open_end {
unterminate_last(entry);
}
}
fn last_entry(collection: &SyntaxNode, kind: SyntaxKind) -> Option<SyntaxNode> {
std::iter::successors(collection.last_child(), |n| n.prev_sibling()).find(|n| n.kind() == kind)
}
fn end_slot_index(collection: &SyntaxNode) -> usize {
let count = collection.green().children().len();
let lifted_comment_indent = collection
.last_child_or_token()
.is_some_and(|c| c.kind() == SyntaxKind::INDENT);
count - usize::from(lifted_comment_indent)
}
fn child_at(node: &SyntaxNode, i: usize) -> Option<rowan::SyntaxElement<super::Lang>> {
let from_end = node.green().children().len().checked_sub(i + 1)?;
if from_end < i {
std::iter::successors(node.last_child_or_token(), |c| c.prev_sibling_or_token())
.nth(from_end)
} else {
node.children_with_tokens().nth(i)
}
}
fn end_line(collection: &SyntaxNode, at: usize) -> (usize, bool) {
let before = at.checked_sub(1).and_then(|i| child_at(collection, i));
match before {
Some(rowan::NodeOrToken::Node(entry))
if matches!(
entry.kind(),
SyntaxKind::MAPPING_ENTRY | SyntaxKind::SEQUENCE_ENTRY
) =>
{
let open = !ends_with_line_break(&entry);
terminate(&entry);
(entry.index() + 1, open)
}
Some(rowan::NodeOrToken::Token(t)) if t.kind() == SyntaxKind::COMMENT => {
if t.next_sibling_or_token()
.is_some_and(|c| c.kind() == SyntaxKind::NEWLINE)
{
return (at + 1, false);
}
insert_children(
collection,
at,
vec![fresh_token(SyntaxKind::NEWLINE, "\n").into()],
);
(at + 1, true)
}
_ => (at, false),
}
}
pub(crate) fn attach_flow_entry(collection: &SyntaxNode, slot: Slot, entry: &SyntaxNode) {
let kind = entry.kind();
let next = match slot {
Slot::After(prev) => std::iter::successors(prev.next_sibling(), |n| n.next_sibling())
.find(|n| n.kind() == kind),
Slot::Before(next) => Some(next),
Slot::End => None,
};
let at = match (next, last_entry(collection, kind)) {
(Some(next), _) => next.index(),
(None, Some(last)) => last.index() + 1,
(None, None) => collection
.children_with_tokens()
.position(|c| {
matches!(
c.kind(),
SyntaxKind::RIGHT_BRACE | SyntaxKind::RIGHT_BRACKET
)
})
.unwrap_or_else(|| collection.children_with_tokens().count()),
};
let mut entries_before: Vec<SyntaxNode> = collection
.children_with_tokens()
.take(at)
.filter_map(|c| c.into_node())
.filter(|n| n.kind() == kind)
.collect();
let has_following_entry = collection
.children_with_tokens()
.skip(at)
.any(|c| c.as_node().is_some_and(|n| n.kind() == kind));
if has_following_entry {
if let Some(prev) = entries_before.last() {
FlowSeparator::ensure(prev);
}
FlowSeparator::ensure(entry);
} else if let Some(last) = entries_before.pop() {
let mut elements = FlowSeparator::hand_over_end(&last, entries_before.last(), entry);
elements.push(entry.clone().into());
insert_children(collection, at, elements);
return;
}
insert_children(collection, at, vec![entry.clone().into()]);
}
pub(crate) fn detach_entry(collection: &SyntaxNode, entry: &SyntaxNode) {
let kind = entry.kind();
let is_last = !std::iter::successors(entry.next_sibling(), |n| n.next_sibling())
.any(|n| n.kind() == kind);
let prev = std::iter::successors(entry.prev_sibling(), |n| n.prev_sibling())
.find(|n| n.kind() == kind);
let terminated = ends_with_line_break(entry);
let root = collection
.ancestors()
.last()
.filter(|root| root.text_range().end() == entry.text_range().end());
let flow = starts_a_flow_collection(collection);
let mut trivia = Vec::new();
if !flow || terminated {
match line_indentation_before(entry) {
Some(indentation) => trivia.extend(indentation),
None if flow && terminated => trivia.extend(
std::iter::successors(entry.next_sibling_or_token(), |c| c.next_sibling_or_token())
.take_while(|c| {
matches!(c.kind(), SyntaxKind::INDENT | SyntaxKind::WHITESPACE)
}),
),
None => {}
}
if entry
.next_sibling_or_token()
.is_some_and(|c| c.kind() == SyntaxKind::NEWLINE)
{
let first =
std::iter::successors(entry.prev_sibling_or_token(), |c| c.prev_sibling_or_token())
.all(|c| c.kind() == SyntaxKind::INDENT);
match collection.parent() {
Some(holder) if first && !flow && holder.kind() != SyntaxKind::DOCUMENT => {
trivia.extend(blank_lines_before(collection, true))
}
_ => trivia.extend(blank_lines_before(entry, false)),
}
}
}
entry.detach();
for c in trivia {
c.detach();
}
let drained = !collection.children().any(|n| n.kind() == kind);
if flow {
let last = collection.children().filter(|n| n.kind() == kind).last();
if let Some(last) = last.filter(|_| is_last) {
if !has_comma(entry) {
FlowSeparator::strip(&last);
} else if let Some(separator) = FlowSeparator::of(entry) {
FlowSeparator::swap(&last, &separator);
}
}
} else {
let leading_indent = collection
.first_child_or_token()
.and_then(|c| c.into_token())
.filter(|t| t.kind() == SyntaxKind::INDENT && !crate::yaml::indents_comment_line(t))
.filter(|t| {
t.next_token()
.is_some_and(|n| n.kind() != SyntaxKind::NEWLINE)
});
if let Some(indent) = leading_indent {
indent.detach();
}
if !drained {
if prev.is_none() {
crate::yaml::move_leading_lines_out(collection);
}
if is_last {
move_trailing_lines_out(collection);
}
}
if let Some(prev) = &prev {
super::replaced_value::settle_lines_after_block_scalar(prev);
}
}
if drained {
render_empty(collection, line_break(entry).as_deref());
}
if is_last && !terminated && !flow {
unterminate_last(root.as_ref().unwrap_or(collection));
}
}
fn move_trailing_lines_out(collection: &SyntaxNode) {
let mut lines: Vec<_> = std::iter::successors(collection.last_child_or_token(), |c| {
c.prev_sibling_or_token()
})
.map_while(|c| c.into_token())
.collect();
lines.reverse();
let Some((host, at)) = lines_after_entry_slot(collection) else {
return;
};
for t in &lines {
t.detach();
}
insert_children(&host, at, fresh_copies(&lines));
}
fn lines_after_entry_slot(node: &SyntaxNode) -> Option<(SyntaxNode, usize)> {
let holders: Vec<_> = node
.ancestors()
.filter(|n| {
matches!(
n.kind(),
SyntaxKind::MAPPING_ENTRY | SyntaxKind::SEQUENCE_ENTRY
)
})
.collect();
let holder = holders
.iter()
.find(|e| e.next_sibling().is_some_and(|n| n.kind() == e.kind()))
.or(holders.last())?;
Some((holder.parent()?, holder.index() + 1))
}
fn fresh_copies(tokens: &[SyntaxToken]) -> Vec<rowan::SyntaxElement<super::Lang>> {
tokens
.iter()
.map(|t| fresh_token(t.kind(), t.text()).into())
.collect()
}
pub(crate) fn end_former_block_scalar(
scalar: &SyntaxNode,
line_break: &SyntaxToken,
blank_lines: &[SyntaxToken],
) {
let child = scalar.ancestors().find(|n| {
n.parent().is_some_and(|p| {
matches!(
p.kind(),
SyntaxKind::MAPPING_ENTRY | SyntaxKind::SEQUENCE_ENTRY | SyntaxKind::DOCUMENT
)
})
});
let Some(child) = child else {
let mut lines = vec![line_break.clone()];
lines.extend_from_slice(blank_lines);
append_children(scalar, fresh_copies(&lines));
return;
};
let host = child.parent().expect("found by its parent");
insert_children(
&host,
child.index() + 1,
fresh_copies(std::slice::from_ref(line_break)),
);
let (host, at) = match lines_after_entry_slot(&child) {
Some(slot) if host.kind() != SyntaxKind::DOCUMENT => slot,
_ => (host, child.index() + 2),
};
insert_children(&host, at, fresh_copies(blank_lines));
}
fn line_indentation_before(entry: &SyntaxNode) -> Option<Vec<rowan::SyntaxElement<super::Lang>>> {
let mut before =
std::iter::successors(entry.prev_sibling_or_token(), |c| c.prev_sibling_or_token())
.peekable();
let mut indentation = Vec::new();
while let Some(c) =
before.next_if(|c| matches!(c.kind(), SyntaxKind::INDENT | SyntaxKind::WHITESPACE))
{
indentation.push(c);
}
let starts_line = match before.next()? {
rowan::NodeOrToken::Node(n) => ends_with_line_break(&n),
rowan::NodeOrToken::Token(t) => t.kind() == SyntaxKind::NEWLINE,
};
starts_line.then_some(indentation)
}
fn blank_lines_before(
node: &SyntaxNode,
starts_in_line: bool,
) -> Vec<rowan::SyntaxElement<super::Lang>> {
let mut before =
std::iter::successors(node.prev_sibling_or_token(), |c| c.prev_sibling_or_token())
.skip_while(|c| c.kind() == SyntaxKind::INDENT)
.peekable();
let mut newlines = Vec::new();
while let Some(newline) = before.next_if(|c| c.kind() == SyntaxKind::NEWLINE) {
newlines.push(newline);
}
let ends_line = match before.next() {
Some(rowan::NodeOrToken::Node(n)) => !ends_with_line_break(&n),
Some(rowan::NodeOrToken::Token(_)) => true,
None => starts_in_line,
};
if ends_line {
newlines.pop();
}
newlines
}
pub(crate) fn terminate(entry: &SyntaxNode) {
terminate_with(entry, "\n");
}
pub(crate) fn terminate_with(entry: &SyntaxNode, line_break: &str) {
if !ends_with_line_break(entry) {
terminate_last_line(entry, line_break);
}
}
pub(crate) fn unterminate_last(node: &SyntaxNode) {
let Some(last) = last_non_empty_token(node) else {
return;
};
let in_scalar = last
.parent_ancestors()
.take_while(|n| n != node)
.any(|n| n.kind() == SyntaxKind::SCALAR);
if last.kind() == SyntaxKind::NEWLINE && !in_scalar {
last.detach();
}
}
pub(crate) fn terminate_last_line(node: &SyntaxNode, line_break: &str) {
let newline = vec![fresh_token(SyntaxKind::NEWLINE, line_break).into()];
append_children(&line_end_holder(node), newline);
}
pub(crate) fn line_end_holder(node: &SyntaxNode) -> SyntaxNode {
let mut cur = node.clone();
while let Some(last) = last_line_node(&cur) {
if !holds_line_end(&last) {
break;
}
cur = last;
}
cur
}
fn last_line_node(node: &SyntaxNode) -> Option<SyntaxNode> {
let last = node.last_child_or_token()?.into_node()?;
if last.kind() == SyntaxKind::VALUE && !has_child_token(node, |k| k == SyntaxKind::COLON) {
return node.children().find(|c| c.kind() == SyntaxKind::KEY);
}
Some(last)
}
fn holds_line_end(node: &SyntaxNode) -> bool {
match node.kind() {
SyntaxKind::MAPPING | SyntaxKind::SEQUENCE => !starts_a_flow_collection(node),
SyntaxKind::MAPPING_ENTRY | SyntaxKind::SEQUENCE_ENTRY => true,
SyntaxKind::SCALAR => is_block_scalar(node),
SyntaxKind::KEY | SyntaxKind::VALUE | SyntaxKind::TAGGED_NODE => {
node.last_child().is_some_and(|last| holds_line_end(&last))
}
_ => false,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct FlowSeparator(Vec<(SyntaxKind, String)>);
impl FlowSeparator {
fn comma_space() -> Self {
Self(vec![
(SyntaxKind::COMMA, ",".to_string()),
(SyntaxKind::WHITESPACE, " ".to_string()),
])
}
pub(crate) fn of(entry: &SyntaxNode) -> Option<Self> {
let tokens = separator_tokens(entry)?;
Some(Self(
tokens
.iter()
.map(|t| (t.kind(), t.text().to_string()))
.collect(),
))
}
fn build(&self) -> Vec<rowan::SyntaxElement<super::Lang>> {
self.0
.iter()
.map(|(kind, text)| fresh_token(*kind, text).into())
.collect()
}
pub(crate) fn swap(entry: &SyntaxNode, separator: &Self) -> bool {
let Some(own) = separator_tokens(entry) else {
return false;
};
let end = entry.children_with_tokens().count();
replace_children(entry, end - own.len()..end, separator.build());
separate_comma_from_indicator(entry);
true
}
pub(crate) fn ensure(entry: &SyntaxNode) {
if !has_comma(entry) {
append_children(entry, Self::comma_space().build());
separate_comma_from_indicator(entry);
}
}
pub(crate) fn insert_comma(entry: &SyntaxNode) {
if has_comma(entry) {
return;
}
let at = entry
.children_with_tokens()
.filter(|c| c.as_node().is_some())
.last()
.map_or(0, |n| n.index() + 1);
let mut tokens = vec![fresh_token(SyntaxKind::COMMA, ",").into()];
let spaced = entry.children_with_tokens().nth(at).is_some_and(|c| {
matches!(
c.kind(),
SyntaxKind::WHITESPACE | SyntaxKind::NEWLINE | SyntaxKind::INDENT
)
});
if !spaced {
tokens.push(fresh_token(SyntaxKind::WHITESPACE, " ").into());
}
insert_children(entry, at, tokens);
separate_comma_from_indicator(entry);
}
pub(crate) fn strip(entry: &SyntaxNode) {
let Some(comma) = entry
.children_with_tokens()
.filter_map(|c| c.into_token())
.filter(|t| t.kind() == SyntaxKind::COMMA)
.last()
else {
return;
};
let after_entry =
std::iter::successors(entry.next_sibling_or_token(), |c| c.next_sibling_or_token())
.map_while(|c| c.into_token())
.take_while(|t| {
matches!(
t.kind(),
SyntaxKind::WHITESPACE | SyntaxKind::NEWLINE | SyntaxKind::INDENT
)
});
let mut tail = vec![comma.clone()];
let mut next = comma.next_sibling_or_token();
while let Some(c) = next {
let Some(t) = c.into_token() else { break };
next = t.next_sibling_or_token();
tail.push(t);
}
tail.extend(after_entry);
let only_whitespace = tail[1..].iter().all(|t| {
matches!(
t.kind(),
SyntaxKind::WHITESPACE | SyntaxKind::NEWLINE | SyntaxKind::INDENT
)
});
if !only_whitespace {
tail.truncate(1);
}
for t in tail {
t.detach();
}
let content_end = entry.children().last().map_or(0, |n| n.index() + 1);
let lines: Vec<_> = entry
.children_with_tokens()
.skip(content_end)
.skip_while(|c| c.kind() != SyntaxKind::NEWLINE)
.skip(1)
.collect();
for c in &lines {
c.detach();
}
let parent = entry.parent().expect("a flow entry is in a collection");
insert_children(
&parent,
entry.index() + 1,
lines
.iter()
.map(|c| fresh_token(c.kind(), &c.to_string()).into())
.collect(),
);
}
fn hand_over_end(
last: &SyntaxNode,
before_last: Option<&SyntaxNode>,
new_entry: &SyntaxNode,
) -> Vec<rowan::SyntaxElement<super::Lang>> {
let Some(separator) = separator_tokens(last) else {
let indentation: Vec<_> =
std::iter::successors(last.next_sibling_or_token(), |c| c.next_sibling_or_token())
.take_while(|c| matches!(c.kind(), SyntaxKind::INDENT | SyntaxKind::WHITESPACE))
.collect();
for c in &indentation {
c.detach();
}
if has_comma(last) {
append_children(new_entry, vec![fresh_token(SyntaxKind::COMMA, ",").into()]);
return indentation;
}
let mut separator = indentation;
separator.push(fresh_token(SyntaxKind::COMMA, ",").into());
separator.push(fresh_token(SyntaxKind::WHITESPACE, " ").into());
append_children(last, separator);
return Vec::new();
};
let usual = before_last
.filter(|e| has_comma(e))
.and_then(Self::of)
.unwrap_or_else(Self::comma_space);
for t in &separator {
t.detach();
}
append_children(new_entry, separator.into_iter().map(Into::into).collect());
append_children(last, usual.build());
separate_comma_from_indicator(last);
if !ends_with_line_break(last) {
return Vec::new();
}
let indentation = line_indentation_before(last).unwrap_or_default();
indentation
.iter()
.rev()
.map(|c| fresh_token(c.kind(), &c.to_string()).into())
.collect()
}
}
fn separate_comma_from_indicator(entry: &SyntaxNode) {
let comma = entry
.children_with_tokens()
.filter_map(|c| c.into_token())
.find(|t| t.kind() == SyntaxKind::COMMA);
if let Some(comma) = comma {
separate_from_question(&comma);
}
}
pub(crate) fn separate_from_question(token: &SyntaxToken) {
let question = std::iter::successors(token.prev_token(), |t| t.prev_token())
.find(|t| !t.text().is_empty())
.filter(|t| t.kind() == SyntaxKind::QUESTION);
if let Some(question) = question {
insert_children(
&question.parent().expect("a token has a parent"),
question.index() + 1,
vec![fresh_token(SyntaxKind::WHITESPACE, " ").into()],
);
}
}
pub(crate) fn has_comma(entry: &SyntaxNode) -> bool {
has_child_token(entry, |k| k == SyntaxKind::COMMA)
}
fn separator_tokens(entry: &SyntaxNode) -> Option<Vec<SyntaxToken>> {
let children: Vec<_> = entry.children_with_tokens().collect();
let start = children
.iter()
.rposition(|c| {
!matches!(
c.kind(),
SyntaxKind::COMMA
| SyntaxKind::WHITESPACE
| SyntaxKind::NEWLINE
| SyntaxKind::INDENT
) || c.as_node().is_some()
})
.map_or(0, |i| i + 1);
let separator: Vec<SyntaxToken> = children[start..]
.iter()
.filter_map(|c| c.as_token().cloned())
.collect();
let comma_in_separator = separator.iter().any(|t| t.kind() == SyntaxKind::COMMA);
if comma_in_separator != has_comma(entry) {
return None;
}
let before = children[..start]
.iter()
.rev()
.filter_map(|c| match c {
rowan::NodeOrToken::Node(n) => n.last_token(),
rowan::NodeOrToken::Token(t) => Some(t.clone()),
})
.next();
if before.is_some_and(|t| t.kind() == SyntaxKind::COMMENT) {
return None;
}
Some(separator)
}