use std::collections::{BTreeMap, BTreeSet};
use oneyaml_core::{Path, Span};
use oneyaml_syntax::tree::{Fragment, Inserted, Node as TreeNode, SyntaxKind};
use super::comments::{self as cm, Key};
#[derive(Default)]
struct Before {
owners: BTreeMap<TreeId, Key>,
values: BTreeMap<TreeId, Option<String>>,
walked: BTreeSet<TreeId>,
}
use super::edit::{Put, map_tree};
use super::place::{self, Coll};
use super::{CommentId, CommentOwner, CommentRule, Document, EditError, NodeId, TreeId};
fn unsupported(reason: &'static str) -> EditError {
EditError::Unsupported {
at: Path::new(),
reason,
}
}
fn internal(what: &str) -> EditError {
debug_assert!(false, "comment edit: {what}");
EditError::Internal
}
fn climb(t: TreeNode<'_>) -> TreeNode<'_> {
let mut at = t;
while let Some(p) = at.parent().filter(|p| {
matches!(
p.kind(),
SyntaxKind::Scalar | SyntaxKind::Alias | SyntaxKind::Error
)
}) {
at = p;
}
at
}
pub(super) fn after_chain(t: TreeNode<'_>) -> Vec<Put> {
let mut x = climb(t);
let mut out = vec![Put::After(x.id())];
while x.next_sibling().is_none() {
match x.parent() {
Some(p) if p.kind() != SyntaxKind::Stream => {
x = p;
out.push(Put::After(x.id()));
}
_ => break,
}
}
out
}
pub(super) fn after_line(root: TreeNode<'_>, e: Option<TreeNode<'_>>) -> Vec<Put> {
match e {
None => match root.first_child() {
Some(b) if b.kind() == SyntaxKind::ByteOrderMark => vec![Put::After(b.id())],
Some(f) => vec![Put::Before(f.id())],
None => vec![Put::Append(root.id())],
},
Some(e) if e.kind() == SyntaxKind::LineBreak => {
let mut out = Vec::new();
if let Some(q) = cm::before_indent(e).filter(|q| !cm::ends_line(*q)) {
out.push(Put::LineAfter(climb(q).id()));
}
out.push(Put::LineAfter(e.id()));
out
}
Some(e) => vec![Put::LineAfter(climb(e).id())],
}
}
fn blank_follows(root: TreeNode<'_>, e: Option<TreeNode<'_>>) -> bool {
let mut at = match e {
Some(e) => place::next_token(e),
None => cm::first_token(root),
};
while let Some(t) = at.filter(|t| cm::blankish(*t)) {
at = place::next_token(t);
}
at.is_some_and(|t| t.kind() == SyntaxKind::LineBreak)
}
fn rest_blank(t: TreeNode<'_>) -> bool {
let mut at = place::next_token(t);
while let Some(x) = at.filter(|x| cm::blankish(*x)) {
at = place::next_token(x);
}
at.is_none_or(|x| x.kind() == SyntaxKind::LineBreak)
}
fn writable(t: TreeNode<'_>) -> bool {
!cm::ends_line(t) && t.kind() != SyntaxKind::BlockScalarHeader
}
fn line_last(s: TreeNode<'_>) -> Option<TreeNode<'_>> {
let mut at = cm::first_token(s);
let mut last = None;
while let Some(t) = at {
match t.kind() {
SyntaxKind::LineBreak => break,
SyntaxKind::Comment => return None,
_ if cm::visible(t) => {
if cm::ends_line(t) {
return None;
}
last = Some(t);
}
_ => {}
}
at = place::next_token(t);
}
last.filter(|t| writable(*t))
}
fn line_has_comment(s: TreeNode<'_>) -> bool {
let mut at = cm::first_token(s);
while let Some(t) = at {
match t.kind() {
SyntaxKind::LineBreak => return false,
SyntaxKind::Comment => return true,
_ if cm::ends_line(t) => return false,
_ => {}
}
at = place::next_token(t);
}
false
}
fn trailing_points(x: TreeNode<'_>) -> Vec<TreeId> {
let start = cm::entry_start(x);
let mut out = Vec::new();
if let Some(p) = line_last(start) {
out.push(p.id());
}
let key = !cm::is_value(x) && x.parent().and_then(Coll::of).is_some_and(Coll::is_map);
let end = if key { cm::value_of(x) } else { Some(x) };
let last = end
.and_then(cm::last_token)
.filter(|t| writable(*t) && rest_blank(*t));
if let Some(t) = last {
if !out.contains(&t.id()) {
out.push(t.id());
}
}
out
}
fn lines_text(
pad: &str,
raws: &[String],
nl: &str,
lead: bool,
last_break: bool,
blank: bool,
) -> String {
let mut text = String::new();
if lead {
text.push_str(nl);
}
let n = raws.len();
for (i, r) in raws.iter().enumerate() {
text.push_str(pad);
text.push_str(r);
if last_break || i.saturating_add(1) < n {
text.push_str(nl);
}
}
if blank {
text.push_str(nl);
}
text
}
impl Document {
fn checked_raw(&self, text: &str) -> Result<String, EditError> {
let raw = cm::raw_of(text)?;
let invalid = || EditError::InvalidComment {
text: text.to_owned(),
};
let t = self.synth(&raw).map_err(|_| invalid())?;
let mut kids = t.root().children();
match (kids.next(), kids.next()) {
(Some(k), None) if k.kind() == SyntaxKind::Comment && k.text() == raw.as_str() => {
Ok(raw)
}
_ => Err(invalid()),
}
}
fn checked_raws<I>(&self, lines: I) -> Result<Vec<String>, EditError>
where
I: IntoIterator,
I::Item: AsRef<str>,
{
lines
.into_iter()
.map(|l| self.checked_raw(l.as_ref()))
.collect()
}
pub(super) fn trivia_fragment(&self, text: &str) -> Result<Fragment, EditError> {
self.read_trivia(text)?
.ok_or_else(|| internal("comment lines did not read as trivia"))
}
pub(super) fn carried_fragment(&self, text: &str) -> Result<Fragment, EditError> {
self.read_trivia(text)?
.ok_or(EditError::NotWellFormed { at: Path::new() })
}
fn read_trivia(&self, text: &str) -> Result<Option<Fragment>, EditError> {
let t = self.synth(text)?;
let kids: Vec<TreeNode<'_>> = t.root().children().collect();
let trivia = kids.iter().all(|k| {
matches!(
k.kind(),
SyntaxKind::Whitespace | SyntaxKind::Comment | SyntaxKind::LineBreak
)
});
match (kids.first(), kids.last()) {
(Some(a), Some(b)) if trivia => t
.extract(a.id(), b.id())
.map(Some)
.map_err(|e| map_tree(e, Span::empty(0))),
_ => Ok(None),
}
}
fn try_puts(
&mut self,
puts: &[Put],
frag: &Fragment,
b: &mut Before,
) -> Result<Inserted, EditError> {
let mut last = EditError::NotWellFormed { at: Path::new() };
for p in puts {
self.border_put(b, *p);
match self.t_insert(*p, frag.clone()) {
Ok(i) => return Ok(i),
Err(e @ (EditError::NotWellFormed { .. } | EditError::ReadOnly { .. })) => last = e,
Err(e) => return Err(e),
}
}
Err(last)
}
fn border_put(&self, b: &mut Before, put: Put) {
let at = match put {
Put::After(x) | Put::Before(x) | Put::LineAfter(x) => self.tree().get(x).ok(),
Put::Append(p) => self.tree().get(p).ok().and_then(cm::last_token),
};
if let Some(n) = at {
self.border(b, n);
}
}
fn comments_in(&self, ins: Inserted) -> Vec<TreeId> {
let mut out = Vec::new();
let mut at = self.tree().get(ins.first).ok();
while let Some(n) = at {
if n.kind() == SyntaxKind::Comment {
out.push(n.id());
}
if n.id() == ins.last {
break;
}
at = n.next_sibling();
}
out
}
fn insert_after_line(
&mut self,
e: Option<TreeId>,
pad: &str,
raws: &[String],
blank: bool,
b: &mut Before,
) -> Result<Vec<TreeId>, EditError> {
let puts = {
let e = match e {
Some(e) => Some(self.tree().get(e).map_err(|_| EditError::StaleNode)?),
None => None,
};
after_line(self.tree().root(), e)
};
let frag = self.trivia_fragment(&lines_text(pad, raws, self.nl(), false, true, blank))?;
let ins = self.try_puts(&puts, &frag, b)?;
Ok(self.comments_in(ins))
}
fn insert_lines_above(
&mut self,
start: TreeId,
pad: &str,
raws: &[String],
b: &mut Before,
) -> Result<Vec<TreeId>, EditError> {
let e = {
let s = self.tree().get(start).map_err(|_| EditError::StaleNode)?;
cm::before_indent(s).map(|e| e.id())
};
self.insert_after_line(e, pad, raws, false, b)
}
fn insert_lines_below(
&mut self,
sibling: TreeId,
pad: &str,
raws: &[String],
b: &mut Before,
) -> Result<Vec<TreeId>, EditError> {
let nl = self.nl();
let frag = self.trivia_fragment(&lines_text(pad, raws, nl, false, true, false))?;
self.border_put(b, Put::LineAfter(sibling));
match self.t_insert(Put::LineAfter(sibling), frag) {
Ok(i) => return Ok(self.comments_in(i)),
Err(EditError::NotWellFormed { .. } | EditError::ReadOnly { .. }) => {}
Err(e) => return Err(e),
}
let puts = {
let s = self.tree().get(sibling).map_err(|_| EditError::StaleNode)?;
after_chain(s)
};
let frag = self.trivia_fragment(&lines_text(pad, raws, nl, true, false, false))?;
let ins = self.try_puts(&puts, &frag, b)?;
Ok(self.comments_in(ins))
}
fn insert_trailing(
&mut self,
p: TreeId,
raw: &str,
b: &mut Before,
) -> Result<TreeId, EditError> {
let puts = {
let t = self.tree().get(p).map_err(|_| EditError::StaleNode)?;
after_chain(t)
};
let frag = self.trivia_fragment(&format!(" {raw}"))?;
let ins = self.try_puts(&puts, &frag, b)?;
self.comments_in(ins)
.first()
.copied()
.ok_or_else(|| internal("no comment inserted"))
}
fn drop_comments(&mut self, ids: &[TreeId]) -> Result<(), EditError> {
enum Cut {
Range(TreeId, TreeId),
ToLineEnd(TreeId, TreeId),
Lines(TreeId, TreeId, String),
Shifted(TreeId, TreeId, String),
}
if ids.is_empty() {
return Ok(());
}
let cuts = {
let tree = self.tree();
let set: BTreeSet<TreeId> = ids.iter().copied().collect();
let mut nodes = Vec::with_capacity(ids.len());
for c in &set {
nodes.push(tree.get(*c).map_err(|_| EditError::StaleNode)?);
}
nodes.sort_by_key(|n| n.preorder_index());
let is_break = |x: &TreeNode<'_>| x.kind() == SyntaxKind::LineBreak;
let mut used = BTreeSet::new();
let mut cuts = Vec::new();
for n in nodes {
if !used.insert(n.id()) {
continue;
}
let ws = n
.prev_sibling()
.filter(|w| w.kind() == SyntaxKind::Whitespace);
if !cm::line_start(n) {
cuts.push(Cut::Range(ws.unwrap_or(n).id(), n.id()));
continue;
}
let first = match ws {
Some(w) if cm::line_start(w) => w,
_ => n,
};
let Some(mut end) = n.next_sibling().filter(is_break) else {
cuts.push(Cut::ToLineEnd(first.id(), n.id()));
continue;
};
let (mut kept, mut pending) = (String::new(), String::new());
let mut last_c = n;
let mut at = end.next_sibling();
loop {
let mut line = String::new();
while let Some(w) = at.filter(|w| w.kind() == SyntaxKind::Whitespace) {
line.push_str(&w.text());
at = w.next_sibling();
}
match at {
Some(b) if is_break(&b) => {
pending.push_str(&line);
pending.push_str(&b.text());
at = b.next_sibling();
}
Some(c)
if c.kind() == SyntaxKind::Comment
&& set.contains(&c.id())
&& !used.contains(&c.id()) =>
{
let Some(b) = c.next_sibling().filter(is_break) else {
break;
};
used.insert(c.id());
kept.push_str(&pending);
pending.clear();
last_c = c;
end = b;
at = b.next_sibling();
}
_ => break,
}
}
let at_stream = n.parent().is_some_and(|p| p.kind() == SyntaxKind::Stream);
let before = first.prev_sibling().filter(is_break);
if let (true, Some(lb)) = (at_stream, before) {
let joined = format!("{}{kept}", lb.text());
let stays = end.text();
if let Some(text) = joined.strip_suffix(stays.as_ref()) {
cuts.push(Cut::Shifted(lb.id(), last_c.id(), text.to_owned()));
continue;
}
}
cuts.push(Cut::Lines(first.id(), end.id(), kept));
}
cuts
};
for cut in cuts.into_iter().rev() {
match cut {
Cut::Shifted(a, b, text) if text.is_empty() => self.t_remove(a, b)?,
Cut::Shifted(a, b, text) => {
let frag = self.trivia_fragment(&text)?;
self.t_replace(a, b, frag, false)?;
}
Cut::Range(a, b) => self.t_remove(a, b)?,
Cut::ToLineEnd(a, b) => self.t_remove_to_line_end(a, b, None)?,
Cut::Lines(a, b, kept) if kept.is_empty() => self.t_remove(a, b)?,
Cut::Lines(a, b, kept) => {
let frag = self.trivia_fragment(&kept)?;
self.t_replace(a, b, frag, false)?;
}
}
}
Ok(())
}
fn before(&self, seen: &[(TreeNode<'_>, Option<Key>)], except: &BTreeSet<TreeId>) -> Before {
Before {
owners: seen
.iter()
.filter(|(c, _)| !self.has_explicit(c.id()) && !except.contains(&c.id()))
.filter_map(|(c, k)| Some((c.id(), (*k)?)))
.collect(),
..Before::default()
}
}
fn border(&self, b: &mut Before, t: TreeNode<'_>) {
if b.walked.contains(&t.id()) {
return;
}
let (first, last) = (cm::first_token(t), cm::last_token(t));
let mut toks: Vec<TreeNode<'_>> = first.into_iter().chain(last).collect();
if t.kind() == SyntaxKind::Comment {
b.walked.insert(t.id());
}
let mut at = first.and_then(place::prev_token);
while let Some(x) = at {
if cm::visible(x) {
toks.push(x);
break;
}
if x.kind() == SyntaxKind::Comment && !b.walked.insert(x.id()) {
break;
}
at = place::prev_token(x);
}
let mut at = last.and_then(place::next_token);
while let Some(x) = at {
if cm::visible(x) {
toks.push(x);
break;
}
if x.kind() == SyntaxKind::Comment && !b.walked.insert(x.id()) {
break;
}
at = place::next_token(x);
}
for tok in toks.into_iter().filter(|x| cm::visible(*x)) {
if let Some(s) = tok.parent().filter(|p| p.kind() == SyntaxKind::Scalar) {
b.values
.entry(s.id())
.or_insert_with(|| s.value().map(std::borrow::Cow::into_owned));
}
}
}
fn border_all(&self, b: &mut Before, ids: &[TreeId]) {
for c in ids {
if let Ok(n) = self.tree().get(*c) {
self.border(b, n);
}
}
}
fn check_after(&self, b: &Before, new: &[TreeId], want: Option<Key>) -> Result<(), EditError> {
let tree = self.tree();
for (id, was) in &b.values {
let now = tree
.get(*id)
.ok()
.and_then(|n| n.value().map(std::borrow::Cow::into_owned));
if now != *was {
return Err(unsupported(
"a comment edit that would change the value of the scalar next to it \
(a kept `|+` block scalar reads the lines after it)",
));
}
}
if let Some(n) = new.first().and_then(|c| tree.get(*c).ok()) {
let recorded = |x: TreeNode<'_>| {
x.parent()
.filter(|p| p.kind() == SyntaxKind::Scalar)
.is_none_or(|p| b.values.contains_key(&p.id()))
};
let ok = |step: fn(TreeNode<'_>) -> Option<TreeNode<'_>>| {
let mut at = step(n);
while let Some(x) = at {
if cm::visible(x) {
return recorded(x);
}
if x.kind() == SyntaxKind::Comment && b.walked.contains(&x.id()) {
return true;
}
at = step(x);
}
true
};
if !ok(place::prev_token) || !ok(place::next_token) {
return Err(internal("new comment lines next to an unrecorded scalar"));
}
}
let fresh: BTreeSet<TreeId> = new.iter().copied().collect();
let seeds = new
.iter()
.chain(b.owners.keys())
.filter_map(|id| tree.get(*id).ok());
for (c, a) in self.decide_runs(seeds) {
let expect = if fresh.contains(&c.id()) {
want
} else {
b.owners.get(&c.id()).copied()
};
let Some(expect) = expect else { continue };
let got = cm::owner_of(&a).map(|(o, _)| (o, a.document().map(TreeNode::id)));
if got != Some(expect) {
let rule = cm::rule_of(a.rule()).ok_or_else(|| internal("unknown rule"))?;
return Err(EditError::CommentReattaches { rule });
}
}
Ok(())
}
fn finish_comment_edit(&mut self, removed: &[TreeId]) {
self.forget_explicit(removed);
self.commit();
}
pub(super) fn has_comments_to_clear(&self, id: NodeId) -> Result<bool, EditError> {
let x = self.comment_target(id)?;
let f = self.find_comments(x)?;
Ok(!(f.leading.is_empty() && f.trailing.is_empty() && f.footer.is_empty()))
}
fn comment_target(&self, id: NodeId) -> Result<TreeNode<'_>, EditError> {
let x = self.node(id)?.tree_node();
if !x.kind().is_yaml_node() {
return Err(EditError::ReadOnly { at: Path::new() });
}
self.check_region(&[x], x.parent())?;
Ok(x)
}
fn root_tree(&self) -> Option<TreeNode<'_>> {
self.doc_node()?
.children()
.find(|c| c.kind().is_yaml_node())
}
fn key_of(&self, owner: CommentOwner) -> Key {
(owner, self.doc)
}
pub fn set_leading<I>(&mut self, id: NodeId, lines: I) -> Result<Vec<CommentId>, EditError>
where
I: IntoIterator,
I::Item: AsRef<str>,
{
self.prepare()?;
let raws = self.checked_raws(lines)?;
self.set_leading_raws(id, raws)
}
fn set_leading_raws(
&mut self,
id: NodeId,
raws: Vec<String>,
) -> Result<Vec<CommentId>, EditError> {
let (start, pad, old, mut before) = {
let x = self.comment_target(id)?;
let start = cm::entry_start(x);
if !raws.is_empty() && !cm::line_start(start) {
return Err(unsupported(
"a leading comment of an entry or node that does not start its line",
));
}
let f = self.find_comments(x)?;
let old: Vec<TreeId> = f.leading.iter().map(|h| h.node.id()).collect();
let mut before = self.before(&f.seen, &old.iter().copied().collect());
self.border(&mut before, start);
self.border_all(&mut before, &old);
(start.id(), cm::line_prefix(start), old, before)
};
if old.is_empty() && raws.is_empty() {
return Ok(Vec::new());
}
let want = Some(self.key_of(CommentOwner::Leading(id)));
let new = self.atomic(|d| {
d.drop_comments(&old)?;
let new = if raws.is_empty() {
Vec::new()
} else {
d.insert_lines_above(start, &pad, &raws, &mut before)?
};
d.check_after(&before, &new, want)?;
Ok(new)
})?;
self.finish_comment_edit(&old);
Ok(new.into_iter().map(CommentId::of).collect())
}
pub fn set_trailing(
&mut self,
id: NodeId,
text: Option<&str>,
) -> Result<Option<CommentId>, EditError> {
self.prepare()?;
let raw = text.map(|t| self.checked_raw(t)).transpose()?;
let (keep, drop, mut before) = {
let x = self.comment_target(id)?;
if cm::is_value(x) {
return Err(unsupported(
"a same-line comment of a mapping value (its entry owns it: use the key)",
));
}
let f = self.find_comments(x)?;
let keep = match raw {
Some(_) => f
.trailing
.iter()
.find(|h| !h.explicit)
.map(|h| (h.node.id(), h.node.span())),
None => None,
};
let drop: Vec<TreeId> = f
.trailing
.iter()
.map(|h| h.node.id())
.filter(|c| keep.is_none_or(|(k, _)| k != *c))
.collect();
let mut except: BTreeSet<TreeId> = drop.iter().copied().collect();
except.extend(keep.map(|(k, _)| k));
let mut before = self.before(&f.seen, &except);
self.border_all(&mut before, &drop);
if let Some((k, _)) = keep {
self.border_all(&mut before, &[k]);
}
(keep, drop, before)
};
let Some(raw) = raw else {
if drop.is_empty() {
return Ok(None);
}
self.atomic(|d| {
d.drop_comments(&drop)?;
d.check_after(&before, &[], None)
})?;
self.finish_comment_edit(&drop);
return Ok(None);
};
let want = Some(self.key_of(CommentOwner::Trailing(id)));
let new = self.atomic(|d| {
d.drop_comments(&drop)?;
let new = match keep {
Some((k, span)) => d
.tree_mut()?
.replace_token(k, &raw)
.map_err(|e| map_tree(e, span))?,
None => d.new_trailing(id, &raw, &mut before, want)?,
};
d.check_after(&before, &[new], want)?;
Ok(new)
})?;
let mut gone = drop;
gone.extend(keep.map(|(k, _)| k));
self.finish_comment_edit(&gone);
Ok(Some(CommentId::of(new)))
}
fn new_trailing(
&mut self,
id: NodeId,
raw: &str,
before: &mut Before,
want: Option<Key>,
) -> Result<TreeId, EditError> {
let (points, taken) = {
let x = self.tree_node(id)?;
(trailing_points(x), line_has_comment(cm::entry_start(x)))
};
let mut last = None;
for p in points {
let r = self.atomic(|d| {
let c = d.insert_trailing(p, raw, before)?;
d.check_after(before, &[c], want)?;
Ok(c)
});
match r {
Ok(c) => return Ok(c),
Err(
e @ (EditError::CommentReattaches { .. }
| EditError::NotWellFormed { .. }
| EditError::ReadOnly { .. }),
) => last = Some(e),
Err(e) => return Err(e),
}
}
Err(last.unwrap_or_else(|| {
if taken {
unsupported(
"the entry's line already ends with a comment another owner has \
(re-own it with attach_comment, or clear it first)",
)
} else {
unsupported("no line of the entry ends with its own content (a same-line comment)")
}
}))
}
pub fn push_footer(&mut self, id: NodeId, text: &str) -> Result<CommentId, EditError> {
self.prepare()?;
let raw = self.checked_raw(text)?;
if let Some(r) = self.push_footer_indexed(id, &raw)? {
return Ok(r);
}
let (above, at, pad, mut before) = {
let x = self.comment_target(id)?;
let kind = Coll::of(x)
.ok_or_else(|| unsupported("a footer of a node that is not a collection"))?;
let f = self.find_comments(x)?;
let mut before = self.before(&f.seen, &BTreeSet::new());
if kind.is_flow() {
let close = x.last_child().filter(|l| {
matches!(
l.kind(),
SyntaxKind::FlowSequenceClose | SyntaxKind::FlowMappingClose
)
});
let close = close.ok_or_else(|| {
unsupported("a footer of a single-pair or unclosed flow collection")
})?;
if !cm::line_start(close) {
return Err(unsupported(
"a flow collection's footer before a closing bracket that does not start its line",
));
}
self.border(&mut before, close);
(true, close.id(), cm::line_prefix(close), before)
} else {
let first =
place::first_entry(x)?.ok_or_else(|| internal("an empty block collection"))?;
let pad = " ".repeat(place::col_of(first.first));
let end = x.span().end;
let (from, to) = (x.span().start, end);
let inside = |o: &CommentOwner| match o {
CommentOwner::Footer(y) if *y == id => true,
CommentOwner::Footer(y) => self.node(*y).is_ok_and(|n| {
let s = n.span();
from <= s.start && s.end <= to
}),
_ => false,
};
let deep = f
.seen
.iter()
.filter(|(c, k)| {
k.is_some_and(|(o, _)| inside(&o))
&& !self.has_explicit(c.id())
&& c.span().start >= end
})
.map(|(c, _)| *c)
.chain(f.footer.iter().map(|h| h.node))
.filter(|c| c.span().start >= end)
.max_by_key(|c| c.preorder_index());
let sibling = match deep {
Some(c) => c,
None => {
let t = cm::last_token(x)
.ok_or_else(|| internal("a collection without tokens"))?;
if cm::ends_line(t) {
climb(t)
} else {
cm::comment_after(t).unwrap_or_else(|| climb(t))
}
}
};
self.border(&mut before, sibling);
(false, sibling.id(), pad, before)
}
};
let want = Some(self.key_of(CommentOwner::Footer(id)));
let raws = [raw];
let new = self.atomic(|d| {
let new = if above {
d.insert_lines_above(at, &pad, &raws, &mut before)?
} else {
d.insert_lines_below(at, &pad, &raws, &mut before)?
};
d.check_after(&before, &new, want)?;
new.first()
.copied()
.ok_or_else(|| internal("no comment inserted"))
})?;
self.finish_comment_edit(&[]);
Ok(CommentId::of(new))
}
fn push_footer_indexed(
&mut self,
id: NodeId,
raw: &str,
) -> Result<Option<CommentId>, EditError> {
let (at, pad, mut before) = {
let Ok(x) = self.comment_target(id) else {
return Ok(None);
};
let Some(kind) = Coll::of(x) else {
return Ok(None);
};
if kind.is_flow() || self.holds_error_regions() {
return Ok(None);
}
let Ok(Some(first)) = place::first_entry(x) else {
return Ok(None);
};
let pad = " ".repeat(place::col_of(first.first));
let tree = self.tree();
let own = self.doc_node();
let end = x.span().end;
let me = CommentOwner::Footer(id);
let mut deep = None;
let mut at = tree
.last_footer_comment(x)
.filter(|c| c.span().start >= end);
while let Some(c) = at {
if !self.has_explicit(c.id()) {
deep = Some(c);
break;
}
if self.explicit_owner(c.id()).is_none() {
let mine = tree.comment_attachment(c).is_some_and(|a| {
a.document() == own && cm::owner_of(&a).is_some_and(|(o, _)| o == me)
});
if mine {
deep = Some(c);
break;
}
}
at = cm::prev_comment_in_run(c).filter(|c| c.span().start >= end);
}
for (c, o) in self.explicit_owned_by(&[x.id()]) {
let Ok(c) = tree.get(c) else { continue };
if o != me
|| self.explicit_owner(c.id()) != Some(me)
|| c.span().start < end
|| tree.comment_document(c) != Some(own)
{
continue;
}
if deep.is_none_or(|d: TreeNode<'_>| d.span().start < c.span().start) {
deep = Some(c);
}
}
let sibling = match deep {
Some(c) => c,
None => {
let Some(t) = cm::last_token(x) else {
return Ok(None);
};
if cm::ends_line(t) {
climb(t)
} else {
cm::comment_after(t).unwrap_or_else(|| climb(t))
}
}
};
let mut before = Before::default();
self.border_indexed(&mut before, sibling);
(sibling.id(), pad, before)
};
let want = self.key_of(CommentOwner::Footer(id));
let raws = [raw.to_owned()];
let new = self.atomic(|d| {
let new = d.insert_lines_below(at, &pad, &raws, &mut before)?;
d.check_after_indexed(&before, &new, want)?;
new.first()
.copied()
.ok_or_else(|| internal("no comment inserted"))
})?;
self.finish_comment_edit(&[]);
Ok(Some(CommentId::of(new)))
}
fn border_indexed(&self, b: &mut Before, t: TreeNode<'_>) {
let tree = self.tree();
let (first, last) = (cm::first_token(t), cm::last_token(t));
let mut toks: Vec<TreeNode<'_>> = first.into_iter().chain(last).collect();
if t.kind() == SyntaxKind::Comment {
b.walked.insert(t.id());
}
toks.extend(first.and_then(|f| tree.prev_content_token(f)));
toks.extend(last.and_then(|l| tree.next_content_token(l)));
for tok in toks.into_iter().filter(|x| cm::visible(*x)) {
if let Some(s) = tok.parent().filter(|p| p.kind() == SyntaxKind::Scalar) {
b.values
.entry(s.id())
.or_insert_with(|| s.value().map(std::borrow::Cow::into_owned));
}
}
}
fn check_after_indexed(&self, b: &Before, new: &[TreeId], want: Key) -> Result<(), EditError> {
let tree = self.tree();
for (id, was) in &b.values {
let now = tree
.get(*id)
.ok()
.and_then(|n| n.value().map(std::borrow::Cow::into_owned));
if now != *was {
return Err(unsupported(
"a comment edit that would change the value of the scalar next to it \
(a kept `|+` block scalar reads the lines after it)",
));
}
}
let recorded = |x: Option<TreeNode<'_>>| {
x.and_then(|x| x.parent())
.filter(|p| p.kind() == SyntaxKind::Scalar)
.is_none_or(|p| b.values.contains_key(&p.id()))
};
for c in new.iter().filter_map(|c| tree.get(*c).ok()) {
if !recorded(tree.prev_content_token(c)) || !recorded(tree.next_content_token(c)) {
return Err(internal("new comment lines next to an unrecorded scalar"));
}
let a = tree
.comment_attachment(c)
.ok_or_else(|| internal("a new comment without an owner"))?;
let got = cm::owner_of(&a).map(|(o, _)| (o, a.document().map(TreeNode::id)));
if got != Some(want) {
let rule = cm::rule_of(a.rule()).ok_or_else(|| internal("unknown rule"))?;
return Err(EditError::CommentReattaches { rule });
}
}
Ok(())
}
pub fn clear_comments(&mut self, id: NodeId) -> Result<Vec<String>, EditError> {
self.prepare()?;
let (all, texts, before) = {
let x = self.comment_target(id)?;
let f = self.find_comments(x)?;
let mut hits: Vec<TreeNode<'_>> = f
.leading
.iter()
.chain(&f.trailing)
.chain(&f.footer)
.map(|h| h.node)
.collect();
hits.sort_by_key(|h| h.preorder_index());
let ids: Vec<TreeId> = hits.iter().map(|h| h.id()).collect();
let texts: Vec<String> = hits.iter().map(|h| h.text().into_owned()).collect();
let mut before = self.before(&f.seen, &ids.iter().copied().collect());
self.border_all(&mut before, &ids);
(ids, texts, before)
};
if all.is_empty() {
return Ok(Vec::new());
}
self.atomic(|d| {
d.drop_comments(&all)?;
d.check_after(&before, &[], None)
})?;
self.finish_comment_edit(&all);
Ok(texts)
}
pub fn set_blank_lines_before(&mut self, id: NodeId, n: usize) -> Result<(), EditError> {
self.prepare()?;
let (start, before) = {
let x = self.comment_target(id)?;
let f = self.find_comments(x)?;
(
cm::entry_start(x).id(),
self.before(&f.seen, &BTreeSet::new()),
)
};
self.set_blank_above(start, n, before)
}
pub fn set_comment_blank_lines(&mut self, id: CommentId, n: usize) -> Result<(), EditError> {
self.prepare()?;
self.check_own_comment(id)?;
let c = id.tree_id();
let before = {
let tree = self.tree();
let node = tree.get(c).map_err(|_| EditError::StaleNode)?;
let holder = node.parent();
self.check_region(&[], holder)?;
let seen: Vec<(TreeNode<'_>, Option<Key>)> = self
.decide_runs([node])
.into_iter()
.map(|(n, a)| {
let key = cm::owner_of(&a).map(|(o, _)| (o, a.document().map(TreeNode::id)));
(n, key)
})
.collect();
self.before(&seen, &BTreeSet::new())
};
self.set_blank_above(c, n, before)
}
fn set_blank_above(
&mut self,
s: TreeId,
n: usize,
mut before: Before,
) -> Result<(), EditError> {
type Line = (Vec<TreeId>, TreeId);
let (lines, end, after_scalar): (Vec<Line>, Option<TreeId>, bool) = {
let sn = self.tree().get(s).map_err(|_| EditError::StaleNode)?;
if !cm::line_start(sn) {
if n == 0 {
return Ok(());
}
return Err(unsupported(
"blank lines above an entry, node, or comment that does not start its line",
));
}
self.border(&mut before, sn);
let e = cm::before_indent(sn);
let scalar_end =
|t: TreeNode<'_>| t.kind() != SyntaxKind::LineBreak && cm::ends_line(t);
let mut after_scalar = e.is_some_and(scalar_end);
let mut lines = Vec::new();
let mut at = e;
while let Some(b) = at.filter(|b| b.kind() == SyntaxKind::LineBreak) {
let mut ws = Vec::new();
let mut u = place::prev_token(b);
while let Some(t) = u.filter(|t| cm::blankish(*t)) {
if t.kind() == SyntaxKind::Whitespace {
ws.push(t.id());
}
u = place::prev_token(t);
}
match u {
Some(t) if t.kind() == SyntaxKind::LineBreak => {
lines.push((ws, b.id()));
at = Some(t);
}
None => {
lines.push((ws, b.id()));
break;
}
Some(t) if cm::ends_line(t) => {
lines.push((ws, b.id()));
after_scalar = scalar_end(t);
break;
}
Some(_) => break,
}
}
(lines, e.map(|e| e.id()), after_scalar)
};
let current = lines.len();
if current == n {
return Ok(());
}
let scalar_content = || {
unsupported(
"blank lines right after a block scalar (a kept `|+` scalar reads them as its \
content)",
)
};
if n > current {
let add = n.saturating_sub(current);
let total = self.tree().root().span().end;
let max = self.settings().get_limits().get_max_document_size();
let fits = add
.checked_mul(self.nl().len())
.and_then(|b| total.checked_add(b))
.is_some_and(|t| t <= max && isize::try_from(t).is_ok());
if !fits {
return Err(super::edit::too_large(Span::new(total, total)));
}
}
let r = self.atomic(|d| {
if n < current {
let drop = current.saturating_sub(n);
for (ws, b) in lines.iter().take(drop) {
let first = ws.last().copied();
let siblings = first.is_some_and(|f| {
let t = d.tree();
let mut at = t.get(f).ok();
while let Some(x) = at {
if x.id() == *b {
return true;
}
if !ws.contains(&x.id()) {
return false;
}
at = x.next_sibling();
}
false
});
match first {
Some(f) if siblings => d.t_remove(f, *b)?,
_ => {
for w in ws {
d.t_remove(*w, *w)?;
}
d.t_remove(*b, *b)?;
}
}
}
} else {
let add = n.saturating_sub(current);
let puts = {
let e = match end {
Some(e) => Some(d.tree().get(e).map_err(|_| EditError::StaleNode)?),
None => None,
};
after_line(d.tree().root(), e)
};
let frag = d.trivia_fragment(&d.nl().repeat(add))?;
d.try_puts(&puts, &frag, &mut before)?;
}
d.check_after(&before, &[], None)
});
match r {
Err(EditError::NotWellFormed { .. }) if after_scalar => return Err(scalar_content()),
Err(e) => return Err(e),
Ok(()) => {}
}
self.finish_comment_edit(&[]);
Ok(())
}
pub fn set_header<I>(&mut self, lines: I) -> Result<Vec<CommentId>, EditError>
where
I: IntoIterator,
I::Item: AsRef<str>,
{
self.prepare()?;
let raws = self.checked_raws(lines)?;
self.set_header_raws(raws)
}
fn set_header_raws(&mut self, raws: Vec<String>) -> Result<Vec<CommentId>, EditError> {
let (old, mut before) = {
if let Some(r) = self.root_tree() {
self.check_region(&[r], r.parent())?;
}
let f = self.find_doc_comments()?;
let old: Vec<TreeId> = f.leading.iter().map(|h| h.node.id()).collect();
let mut before = self.before(&f.seen, &old.iter().copied().collect());
self.border_all(&mut before, &old);
(old, before)
};
if old.is_empty() && raws.is_empty() {
return Ok(Vec::new());
}
let want = Some(self.key_of(CommentOwner::DocumentHeader));
let new = self.atomic(|d| {
d.drop_comments(&old)?;
let new = if raws.is_empty() {
Vec::new()
} else {
d.insert_header(&raws, &mut before)?
};
d.check_after(&before, &new, want)?;
Ok(new)
})?;
self.finish_comment_edit(&old);
Ok(new.into_iter().map(CommentId::of).collect())
}
fn insert_header(
&mut self,
raws: &[String],
before: &mut Before,
) -> Result<Vec<TreeId>, EditError> {
enum At {
End(bool),
Above(TreeId),
Top(Option<TreeId>, bool),
}
let at = match self.doc_node() {
None => {
let text = self.tree().root().text();
At::End(!text.is_empty() && !text.ends_with(['\n', '\r']))
}
Some(d) => {
let ft = cm::first_token(d).ok_or_else(|| internal("a document without tokens"))?;
let started = d.first_child().is_some_and(|f| {
matches!(
f.kind(),
SyntaxKind::DocumentStartMarker
| SyntaxKind::YamlDirective
| SyntaxKind::TagDirective
| SyntaxKind::ReservedDirective
)
});
if started {
At::Above(ft.id())
} else {
let mut ignored = Vec::new();
let e = match cm::comments_back(ft, false, &mut ignored) {
None => None,
Some(v) => {
let mut at = Some(v);
while let Some(t) = at.filter(|t| !cm::ends_line(*t)) {
at = place::next_token(t);
}
Some(at.ok_or_else(|| {
internal("content before a document ends no line")
})?)
}
};
let blank = blank_follows(self.tree().root(), e);
At::Top(e.map(|e| e.id()), !blank)
}
}
};
match at {
At::End(lead) => {
let root = self.tree().root().id();
let text = lines_text("", raws, self.nl(), lead, true, false);
let frag = self.trivia_fragment(&text)?;
self.border_put(before, Put::Append(root));
let ins = self.t_insert(Put::Append(root), frag)?;
Ok(self.comments_in(ins))
}
At::Above(t) => self.insert_lines_above(t, "", raws, before),
At::Top(e, blank) => self.insert_after_line(e, "", raws, blank, before),
}
}
pub fn set_footer<I>(&mut self, lines: I) -> Result<Vec<CommentId>, EditError>
where
I: IntoIterator,
I::Item: AsRef<str>,
{
self.prepare()?;
let raws = self.checked_raws(lines)?;
self.set_footer_raws(raws)
}
fn set_footer_raws(&mut self, raws: Vec<String>) -> Result<Vec<CommentId>, EditError> {
let (old, mut before) = {
if let Some(r) = self.root_tree() {
self.check_region(&[r], r.parent())?;
}
let f = self.find_doc_comments()?;
let old: Vec<TreeId> = f.footer.iter().map(|h| h.node.id()).collect();
let mut before = self.before(&f.seen, &old.iter().copied().collect());
self.border_all(&mut before, &old);
(old, before)
};
if old.is_empty() && raws.is_empty() {
return Ok(Vec::new());
}
if !raws.is_empty() && self.root_tree().is_none() {
return Err(EditError::CommentReattaches {
rule: CommentRule::HeaderBlankLine,
});
}
let want = Some(self.key_of(CommentOwner::DocumentFooter));
let new = self.atomic(|d| {
d.drop_comments(&old)?;
let new = if raws.is_empty() {
Vec::new()
} else {
let s = d.footer_sibling()?;
d.insert_lines_below(s, "", &raws, &mut before)?
};
d.check_after(&before, &new, want)?;
Ok(new)
})?;
self.finish_comment_edit(&old);
Ok(new.into_iter().map(CommentId::of).collect())
}
fn footer_sibling(&self) -> Result<TreeId, EditError> {
let d = self
.doc_node()
.ok_or_else(|| internal("a footer without a document"))?;
let r = self
.root_tree()
.ok_or_else(|| internal("a footer without a root"))?;
let mut last = cm::last_token(r).ok_or_else(|| internal("a root without tokens"))?;
let mut at = place::next_token(r);
while let Some(t) = at {
if cm::visible(t) || cm::in_other_document(t, d) {
break;
}
if t.kind() != SyntaxKind::Whitespace {
last = t;
}
at = place::next_token(t);
}
Ok(climb(last).id())
}
}