use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;
use oneyaml_core::Span;
use oneyaml_syntax::tree as syntax;
use oneyaml_syntax::tree::{Node as TreeNode, SyntaxKind};
use super::place::{self, Coll};
use super::{Document, EditError, NodeId, NodeRef, TreeId};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct CommentId(TreeId);
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum CommentRule {
HeaderBeforeStart,
HeaderBlankLine,
FirstNodeLeading,
Leading,
Trailing,
CollectionFooter,
DocumentFooter,
Flow,
ShallowLeading,
ExplicitValue,
Inside,
RunOrder,
Explicit,
}
impl CommentRule {
#[must_use]
pub const fn number(self) -> u8 {
match self {
Self::Explicit => 0,
Self::HeaderBeforeStart => 1,
Self::HeaderBlankLine => 2,
Self::FirstNodeLeading => 3,
Self::Leading => 4,
Self::Trailing => 5,
Self::CollectionFooter => 6,
Self::DocumentFooter => 7,
Self::Flow => 9,
Self::ShallowLeading => 10,
Self::ExplicitValue => 11,
Self::Inside => 12,
Self::RunOrder => 13,
}
}
#[must_use]
pub const fn summary(self) -> &'static str {
match self {
Self::HeaderBeforeStart => "document header: before `---`",
Self::HeaderBlankLine => "document header: a blank line before the root",
Self::FirstNodeLeading => "leading: the document's first node",
Self::Leading => "leading: directly above the node",
Self::Trailing => "trailing: after content on its line",
Self::CollectionFooter => {
"collection footer: after the last child, deeper than what follows"
}
Self::DocumentFooter => "document footer: after the root",
Self::Flow => "flow collection: the next item, or the footer",
Self::ShallowLeading => "leading: left of the node below",
Self::ExplicitValue => "leading: the value of an explicit `? ` entry",
Self::Inside => "leading: inside the node (properties, header)",
Self::RunOrder => "run order: the owner of the comment before it",
Self::Explicit => "explicit: attached with Document::attach_comment",
}
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum CommentOwner {
DocumentHeader,
DocumentFooter,
Leading(NodeId),
Trailing(NodeId),
Footer(NodeId),
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct CommentExplanation {
id: CommentId,
span: Span,
text: String,
owner: CommentOwner,
owner_span: Option<Span>,
rule: CommentRule,
blank_lines_before: usize,
}
impl CommentExplanation {
#[must_use]
pub const fn id(&self) -> CommentId {
self.id
}
#[must_use]
pub const fn span(&self) -> Span {
self.span
}
#[must_use]
pub fn text(&self) -> &str {
&self.text
}
#[must_use]
pub const fn owner(&self) -> CommentOwner {
self.owner
}
#[must_use]
pub const fn owner_span(&self) -> Option<Span> {
self.owner_span
}
#[must_use]
pub const fn rule(&self) -> CommentRule {
self.rule
}
#[must_use]
pub const fn blank_lines_before(&self) -> usize {
self.blank_lines_before
}
}
impl core::fmt::Display for CommentExplanation {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let what = match self.owner {
CommentOwner::DocumentHeader => "document header",
CommentOwner::DocumentFooter => "document footer",
CommentOwner::Leading(_) => "leading comment of the node",
CommentOwner::Trailing(_) => "trailing comment of the node",
CommentOwner::Footer(_) => "footer of the collection",
};
write!(
f,
"comment at {}..{}: rule {} ({}): {what}",
self.span.start,
self.span.end,
self.rule.number(),
self.rule.summary()
)?;
if let Some(s) = self.owner_span {
write!(f, " at {}..{}", s.start, s.end)?;
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Comment {
id: CommentId,
span: Span,
raw: String,
blank_lines_before: usize,
explicit: bool,
}
impl Comment {
#[must_use]
pub const fn id(&self) -> CommentId {
self.id
}
#[must_use]
pub const fn span(&self) -> Span {
self.span
}
#[must_use]
pub fn raw(&self) -> &str {
&self.raw
}
#[must_use]
pub fn text(&self) -> &str {
view(&self.raw)
}
#[must_use]
pub const fn blank_lines_before(&self) -> usize {
self.blank_lines_before
}
#[must_use]
pub const fn is_explicit(&self) -> bool {
self.explicit
}
}
pub const NO_COMMENTS: [&str; 0] = [];
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct Comments {
leading: Vec<Comment>,
trailing: Vec<Comment>,
footer: Vec<Comment>,
blank_lines_before: usize,
}
impl Comments {
#[must_use]
pub fn leading(&self) -> &[Comment] {
&self.leading
}
#[must_use]
pub fn trailing(&self) -> &[Comment] {
&self.trailing
}
#[must_use]
pub fn footer(&self) -> &[Comment] {
&self.footer
}
#[must_use]
pub fn blank_lines_before(&self) -> usize {
self.blank_lines_before
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.leading.is_empty() && self.trailing.is_empty() && self.footer.is_empty()
}
}
pub(super) fn view(raw: &str) -> &str {
match raw.strip_prefix('#') {
Some(r) => r.strip_prefix(' ').unwrap_or(r),
None => raw,
}
}
fn comment_char(c: char) -> bool {
matches!(c,
'\t'
| '\u{20}'..='\u{7E}'
| '\u{A0}'..='\u{2027}'
| '\u{202A}'..='\u{D7FF}'
| '\u{E000}'..='\u{FFFD}'
| '\u{10000}'..='\u{10FFFF}')
&& c != '\u{FEFF}'
}
pub(super) fn raw_of(text: &str) -> Result<String, EditError> {
if !text.chars().all(comment_char) {
return Err(EditError::InvalidComment {
text: text.to_owned(),
});
}
Ok(if text.is_empty() {
"#".to_owned()
} else {
format!("# {text}")
})
}
pub(super) fn rule_of(r: syntax::AttachRule) -> Option<CommentRule> {
use syntax::AttachRule as R;
Some(match r {
R::HeaderBeforeStart => CommentRule::HeaderBeforeStart,
R::HeaderBlankLine => CommentRule::HeaderBlankLine,
R::FirstNodeLeading => CommentRule::FirstNodeLeading,
R::Leading => CommentRule::Leading,
R::Trailing => CommentRule::Trailing,
R::CollectionFooter => CommentRule::CollectionFooter,
R::DocumentFooter => CommentRule::DocumentFooter,
R::Flow => CommentRule::Flow,
R::ShallowLeading => CommentRule::ShallowLeading,
R::ExplicitValue => CommentRule::ExplicitValue,
R::Inside => CommentRule::Inside,
R::RunOrder => CommentRule::RunOrder,
_ => return None,
})
}
pub(super) fn owner_of(a: &syntax::Attachment<'_>) -> Option<(CommentOwner, Option<Span>)> {
let node = |o: TreeNode<'_>| (NodeId(o.id()), Some(o.span()));
Some(match a.owner() {
syntax::CommentOwner::DocumentHeader(_) => (CommentOwner::DocumentHeader, None),
syntax::CommentOwner::DocumentFooter(_) => (CommentOwner::DocumentFooter, None),
syntax::CommentOwner::Leading(o) => {
let (id, s) = node(o);
(CommentOwner::Leading(id), s)
}
syntax::CommentOwner::Trailing(o) => {
let (id, s) = node(o);
(CommentOwner::Trailing(id), s)
}
syntax::CommentOwner::Footer(o) => {
let (id, s) = node(o);
(CommentOwner::Footer(id), s)
}
_ => return None,
})
}
fn explain(n: TreeNode<'_>, a: &syntax::Attachment<'_>) -> Option<CommentExplanation> {
let rule = rule_of(a.rule())?;
let (owner, owner_span) = owner_of(a)?;
Some(CommentExplanation {
id: CommentId(n.id()),
span: n.span(),
text: n.text().into_owned(),
owner,
owner_span,
rule,
blank_lines_before: a.blank_lines_before(),
})
}
pub(super) fn visible(t: TreeNode<'_>) -> bool {
t.is_token() && !t.kind().is_trivia() && !t.span().is_empty()
}
pub(super) fn ends_line(t: TreeNode<'_>) -> bool {
t.kind() == SyntaxKind::LineBreak || (t.is_token() && t.text().ends_with(['\n', '\r']))
}
pub(super) fn blankish(t: TreeNode<'_>) -> bool {
matches!(t.kind(), SyntaxKind::Whitespace | SyntaxKind::ByteOrderMark)
|| (t.kind() != SyntaxKind::LineBreak && t.span().is_empty())
}
pub(super) fn before_indent(t: TreeNode<'_>) -> Option<TreeNode<'_>> {
let mut at = place::prev_token(t);
while let Some(x) = at.filter(|x| blankish(*x)) {
at = place::prev_token(x);
}
at
}
pub(super) fn line_start(t: TreeNode<'_>) -> bool {
before_indent(t).is_none_or(ends_line)
}
pub(super) fn line_prefix(t: TreeNode<'_>) -> String {
let mut parts = Vec::new();
let mut at = place::prev_token(t);
while let Some(x) = at.filter(|x| x.kind() == SyntaxKind::Whitespace) {
parts.push(x.text().into_owned());
at = place::prev_token(x);
}
parts.iter().rev().map(String::as_str).collect()
}
pub(super) fn first_token(n: TreeNode<'_>) -> Option<TreeNode<'_>> {
let mut at = n;
loop {
if at.is_token() {
return Some(at);
}
match at.first_child() {
Some(c) => at = c,
None => return place::next_token(at),
}
}
}
pub(super) fn last_token(n: TreeNode<'_>) -> Option<TreeNode<'_>> {
if n.is_token() {
return Some(n);
}
if let Some(after) = place::next_token(n) {
return place::prev_token(after);
}
let mut at = n;
while let Some(c) = at.last_child() {
at = c;
}
if at.is_token() {
Some(at)
} else {
place::prev_token(at)
}
}
fn blank_before(end: Option<TreeNode<'_>>) -> usize {
let Some(mut at) = end else { return 0 };
let mut count = 0usize;
loop {
if at.kind() != SyntaxKind::LineBreak {
return count;
}
let mut u = place::prev_token(at);
while let Some(t) = u.filter(|t| blankish(*t)) {
u = place::prev_token(t);
}
match u {
None => return count.saturating_add(1),
Some(t) if t.kind() == SyntaxKind::LineBreak => {
count = count.saturating_add(1);
at = t;
}
Some(t) if ends_line(t) => return count.saturating_add(1),
Some(_) => return count,
}
}
}
pub(super) fn entry_start(x: TreeNode<'_>) -> TreeNode<'_> {
let indicator = match x.parent().and_then(Coll::of) {
Some(Coll::BlockSeq) => SyntaxKind::SequenceEntry,
Some(c) if c.is_map() => SyntaxKind::MappingKey,
_ => return x,
};
let mut at = x.prev_sibling();
while let Some(s) = at {
if s.kind() == indicator {
return s;
}
if !s.kind().is_trivia() {
break;
}
at = s.prev_sibling();
}
x
}
pub(super) fn is_value(x: TreeNode<'_>) -> bool {
x.parent()
.filter(|p| Coll::of(*p).is_some_and(Coll::is_map))
.is_some_and(|p| !place::is_key(p, x))
}
pub(super) fn value_of(x: TreeNode<'_>) -> Option<TreeNode<'_>> {
let mut at = x.next_sibling();
while let Some(s) = at {
if place::is_item(s) {
return Some(s);
}
at = s.next_sibling();
}
None
}
pub(super) fn comments_back<'t>(
from: TreeNode<'t>,
flow: bool,
out: &mut Vec<TreeNode<'t>>,
) -> Option<TreeNode<'t>> {
let mut at = place::prev_token(from);
while let Some(t) = at {
if visible(t) && !(flow && t.kind() == SyntaxKind::FlowEntry) {
return Some(t);
}
if t.kind() == SyntaxKind::Comment {
out.push(t);
}
at = place::prev_token(t);
}
None
}
fn comments_forward<'t>(from: TreeNode<'t>, out: &mut Vec<TreeNode<'t>>) {
let mut at = place::next_token(from);
while let Some(t) = at {
if visible(t) {
return;
}
if t.kind() == SyntaxKind::Comment {
out.push(t);
}
at = place::next_token(t);
}
}
pub(super) fn prev_comment_in_run(c: TreeNode<'_>) -> Option<TreeNode<'_>> {
let mut at = place::prev_token(c);
while let Some(t) = at {
if visible(t) {
return None;
}
if t.kind() == SyntaxKind::Comment {
return line_start(t).then_some(t);
}
at = place::prev_token(t);
}
None
}
pub(super) fn comment_after(p: TreeNode<'_>) -> Option<TreeNode<'_>> {
let mut at = place::next_token(p);
while let Some(t) = at.filter(|t| blankish(*t)) {
at = place::next_token(t);
}
at.filter(|t| t.kind() == SyntaxKind::Comment)
}
pub(super) type Key = (CommentOwner, Option<TreeId>);
#[derive(Clone, Copy)]
pub(super) struct Hit<'t> {
pub(super) node: TreeNode<'t>,
blank: usize,
pub(super) explicit: bool,
}
#[derive(Default)]
pub(super) struct Found<'t> {
pub(super) leading: Vec<Hit<'t>>,
pub(super) trailing: Vec<Hit<'t>>,
pub(super) footer: Vec<Hit<'t>>,
pub(super) seen: Vec<(TreeNode<'t>, Option<Key>)>,
pub(super) blank_lines_before: usize,
}
fn to_comment(h: &Hit<'_>) -> Comment {
Comment {
id: CommentId(h.node.id()),
span: h.node.span(),
raw: h.node.text().into_owned(),
blank_lines_before: h.blank,
explicit: h.explicit,
}
}
fn to_comments(hits: &[Hit<'_>]) -> Vec<Comment> {
hits.iter().map(to_comment).collect()
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Slot {
Leading,
Trailing,
Footer,
}
impl CommentId {
pub(super) const fn of(id: TreeId) -> Self {
Self(id)
}
pub(super) const fn tree_id(self) -> TreeId {
self.0
}
}
pub(super) type OwnerKey = (u8, Option<TreeId>);
pub(super) type ExplicitUndo = (TreeId, Option<(CommentOwner, OwnerKey)>);
fn owner_key(owner: CommentOwner, doc: Option<TreeId>) -> OwnerKey {
match owner {
CommentOwner::DocumentHeader => (0, doc),
CommentOwner::DocumentFooter => (1, doc),
CommentOwner::Leading(n) => (2, Some(n.0)),
CommentOwner::Trailing(n) => (3, Some(n.0)),
CommentOwner::Footer(n) => (4, Some(n.0)),
}
}
#[derive(Clone, Debug, Default)]
pub(super) struct Explicit {
by_comment: BTreeMap<TreeId, (CommentOwner, OwnerKey)>,
by_owner: BTreeMap<OwnerKey, BTreeSet<TreeId>>,
cursor: Option<TreeId>,
}
impl Explicit {
fn get(&self, c: TreeId) -> Option<CommentOwner> {
self.by_comment.get(&c).map(|(o, _)| *o)
}
pub(super) fn contains(&self, c: TreeId) -> bool {
self.by_comment.contains_key(&c)
}
fn insert(&mut self, c: TreeId, owner: CommentOwner, doc: Option<TreeId>) {
self.remove(c);
let key = owner_key(owner, doc);
self.by_comment.insert(c, (owner, key));
self.by_owner.entry(key).or_default().insert(c);
}
fn remove(&mut self, c: TreeId) -> Option<CommentOwner> {
let (owner, key) = self.by_comment.remove(&c)?;
if let Some(set) = self.by_owner.get_mut(&key) {
set.remove(&c);
if set.is_empty() {
self.by_owner.remove(&key);
}
}
Some(owner)
}
fn restore(&mut self, c: TreeId, prev: Option<(CommentOwner, OwnerKey)>) {
self.remove(c);
if let Some((owner, key)) = prev {
self.by_comment.insert(c, (owner, key));
self.by_owner.entry(key).or_default().insert(c);
}
}
fn of_owner(&self, key: OwnerKey) -> impl Iterator<Item = TreeId> + '_ {
self.by_owner.get(&key).into_iter().flatten().copied()
}
}
impl Document {
pub(super) fn explicit_owner(&self, c: TreeId) -> Option<CommentOwner> {
let o = self.explicit.as_deref()?.get(c)?;
self.owner_live(o).then_some(o)
}
pub(super) fn explicit_raw(&self, c: TreeId) -> Option<CommentOwner> {
self.explicit.as_deref()?.get(c)
}
pub(super) fn explicit_owned_by(&self, nodes: &[TreeId]) -> Vec<(TreeId, CommentOwner)> {
let Some(e) = self.explicit.as_deref() else {
return Vec::new();
};
if e.by_comment.is_empty() {
return Vec::new();
}
let mut out = Vec::new();
for n in nodes {
for kind in [2u8, 3, 4] {
for c in e.of_owner((kind, Some(*n))) {
if let Some(o) = e.get(c) {
out.push((c, o));
}
}
}
}
out
}
pub(super) fn any_explicit(&self) -> bool {
self.explicit
.as_deref()
.is_some_and(|e| !e.by_comment.is_empty())
}
pub(super) fn has_explicit(&self, c: TreeId) -> bool {
self.explicit.as_deref().is_some_and(|e| e.contains(c))
}
fn log_explicit(&mut self, c: TreeId) {
if self.tree().in_group() {
let prev = self
.explicit
.as_deref()
.and_then(|e| e.by_comment.get(&c).copied());
self.explicit_log.push((c, prev));
}
}
fn explicit_insert(&mut self, c: TreeId, owner: CommentOwner, doc: Option<TreeId>) {
self.log_explicit(c);
self.explicit_mut().insert(c, owner, doc);
}
fn explicit_remove(&mut self, c: TreeId) {
self.log_explicit(c);
self.explicit_mut().remove(c);
}
pub(super) fn undo_explicit(&mut self, from: usize) {
let ops = self
.explicit_log
.split_off(from.min(self.explicit_log.len()));
if ops.is_empty() {
return;
}
let e = self.explicit_mut();
for (c, prev) in ops.into_iter().rev() {
e.restore(c, prev);
}
}
pub(super) fn explicit_log_len(&self) -> usize {
self.explicit_log.len()
}
pub(super) fn settle_explicit_log(&mut self) {
if !self.tree().in_group() {
self.explicit_log.clear();
}
}
fn explicit_mut(&mut self) -> &mut Explicit {
Arc::make_mut(self.explicit.get_or_insert_with(Arc::default))
}
pub(super) fn forget_explicit(&mut self, gone: &[TreeId]) {
for c in gone {
if self.has_explicit(*c) {
self.explicit_remove(*c);
}
}
}
pub(super) fn prune_explicit(&mut self) {
const PRUNE: usize = 4;
let Some(e) = self.explicit.as_deref() else {
return;
};
if self.tree().in_group() {
return;
}
if e.by_comment.is_empty() {
return;
}
let from = e.cursor;
let mut batch: Vec<(TreeId, CommentOwner, OwnerKey)> = match from {
Some(c) => e
.by_comment
.range((std::ops::Bound::Excluded(c), std::ops::Bound::Unbounded))
.take(PRUNE)
.map(|(k, (o, key))| (*k, *o, *key))
.collect(),
None => Vec::new(),
};
if batch.len() < PRUNE {
let more = PRUNE.saturating_sub(batch.len());
batch.extend(
e.by_comment
.iter()
.take(more)
.map(|(k, (o, key))| (*k, *o, *key)),
);
}
let tree = self.tree();
let alive = |o: CommentOwner, key: OwnerKey| match o {
CommentOwner::DocumentHeader | CommentOwner::DocumentFooter => {
key.1.is_none_or(|d| tree.get(d).is_ok())
}
CommentOwner::Leading(n) | CommentOwner::Trailing(n) => {
tree.get(n.0).is_ok_and(|x| x.kind().is_yaml_node())
}
CommentOwner::Footer(n) => tree.get(n.0).is_ok_and(|x| Coll::of(x).is_some()),
};
let dead: Vec<TreeId> = batch
.iter()
.filter(|(c, o, key)| tree.get(*c).is_err() || !alive(*o, *key))
.map(|(c, _, _)| *c)
.collect();
let next = batch.last().map(|(c, _, _)| *c);
if dead.is_empty() && next == from {
return;
}
let e = self.explicit_mut();
for c in dead {
e.remove(c);
}
e.cursor = next;
}
pub(super) fn set_explicit(&mut self, c: TreeId, owner: CommentOwner, doc: Option<TreeId>) {
self.explicit_insert(c, owner, doc);
}
pub(super) fn owner_is_live(&self, owner: CommentOwner) -> bool {
self.owner_live(owner)
}
fn owner_live(&self, owner: CommentOwner) -> bool {
match owner {
CommentOwner::DocumentHeader => true,
CommentOwner::DocumentFooter => self.doc_node().is_some(),
CommentOwner::Leading(n) | CommentOwner::Trailing(n) => self
.node(n)
.is_ok_and(|r| r.tree_node().kind().is_yaml_node()),
CommentOwner::Footer(n) => self
.node(n)
.is_ok_and(|r| Coll::of(r.tree_node()).is_some()),
}
}
fn owner_span(&self, owner: CommentOwner) -> Option<Span> {
match owner {
CommentOwner::Leading(n) | CommentOwner::Trailing(n) | CommentOwner::Footer(n) => {
self.node(n).ok().map(NodeRef::span)
}
_ => None,
}
}
fn explained(&self, n: TreeNode<'_>, a: &syntax::Attachment<'_>) -> Option<CommentExplanation> {
let mut e = explain(n, a)?;
if let Some(o) = self.explicit_owner(n.id()) {
e.owner = o;
e.owner_span = self.owner_span(o);
e.rule = CommentRule::Explicit;
}
Some(e)
}
#[must_use]
pub fn comment_ids(&self) -> Vec<CommentId> {
self.attachments()
.into_iter()
.map(|(n, _)| CommentId(n.id()))
.collect()
}
pub fn explain_comment(&self, id: CommentId) -> Result<CommentExplanation, EditError> {
let (n, a) = self.own_comment(id)?;
self.explained(n, &a).ok_or_else(internal)
}
fn own_comment(
&self,
id: CommentId,
) -> Result<(TreeNode<'_>, syntax::Attachment<'_>), EditError> {
let tree = &self.shared.tree;
let n = tree.get(id.0).map_err(|_| EditError::StaleNode)?;
if n.kind() != SyntaxKind::Comment {
return Err(EditError::StaleNode);
}
let a = tree.comment_attachment(n).ok_or(EditError::StaleNode)?;
if a.document() != self.doc_node() {
return Err(EditError::StaleNode);
}
Ok((n, a))
}
pub(super) fn check_own_comment(&self, id: CommentId) -> Result<(), EditError> {
let tree = &self.shared.tree;
let n = tree.get(id.0).map_err(|_| EditError::StaleNode)?;
if n.kind() != SyntaxKind::Comment {
return Err(EditError::StaleNode);
}
if !tree.has_errors() {
let d = n.ancestors().find(|a| a.kind() == SyntaxKind::Document);
if let (Some(d), Some(own)) = (d, self.doc_node()) {
if d == own {
return Ok(());
}
}
}
if tree.comment_document(n) == Some(self.doc_node()) {
Ok(())
} else {
Err(EditError::StaleNode)
}
}
pub fn explain_comments(&self) -> Result<Vec<CommentExplanation>, EditError> {
self.attachments()
.iter()
.map(|(n, a)| self.explained(*n, a).ok_or_else(internal))
.collect()
}
fn attachments(&self) -> Vec<(TreeNode<'_>, syntax::Attachment<'_>)> {
let tree = &self.shared.tree;
match (self.doc_node(), self.index) {
(Some(d), _) => tree.document_comment_attachments(d),
(None, None) => tree.comment_attachments(),
(None, Some(_)) => Vec::new(),
}
}
pub fn comments(&self, id: NodeId) -> Result<Comments, EditError> {
let x = self.node(id)?.tree_node();
let f = self.find_comments(x)?;
Ok(Comments {
leading: to_comments(&f.leading),
trailing: to_comments(&f.trailing),
footer: to_comments(&f.footer),
blank_lines_before: f.blank_lines_before,
})
}
pub fn header(&self) -> Result<Vec<Comment>, EditError> {
Ok(to_comments(&self.find_doc_comments()?.leading))
}
pub fn footer(&self) -> Result<Vec<Comment>, EditError> {
Ok(to_comments(&self.find_doc_comments()?.footer))
}
pub(super) fn decide_runs<'s>(
&'s self,
seeds: impl IntoIterator<Item = TreeNode<'s>>,
) -> Vec<(TreeNode<'s>, syntax::Attachment<'s>)> {
let tree = self.tree();
let mut done = BTreeSet::new();
let mut out = Vec::new();
for s in seeds {
if s.kind() != SyntaxKind::Comment || done.contains(&s.id()) {
continue;
}
let mut top = s;
while let Some(p) = top.parent().filter(|p| p.kind() != SyntaxKind::Stream) {
top = p;
}
let decided = if top.kind() == SyntaxKind::Document {
tree.comment_run_attachments(s)
} else {
let is_doc = |n: &TreeNode<'_>| n.kind() == SyntaxKind::Document;
let mut before = top.prev_sibling();
while let Some(b) = before.filter(|b| !is_doc(b)) {
before = b.prev_sibling();
}
let mut after = top.next_sibling();
while let Some(a) = after.filter(|a| !is_doc(a)) {
after = a.next_sibling();
}
tree.comment_attachments_between(before, after)
};
for (c, a) in decided {
if done.insert(c.id()) {
out.push((c, a));
}
}
}
out
}
fn classify<'s>(
&'s self,
decided: Vec<(TreeNode<'s>, syntax::Attachment<'s>)>,
found: &mut Found<'s>,
wanted: &[(CommentOwner, Slot)],
) -> Result<(), EditError> {
let own = self.doc_node().map(TreeNode::id);
let keys: Vec<(OwnerKey, Slot)> = wanted
.iter()
.map(|(o, s)| (owner_key(*o, own), *s))
.collect();
let pick = |o: CommentOwner, doc: Option<TreeId>| {
let k = owner_key(o, doc);
keys.iter().find(|(x, _)| *x == k).map(|(_, s)| *s)
};
for (c, a) in decided {
let doc = a.document().map(TreeNode::id);
let heuristic = owner_of(&a).ok_or_else(internal)?.0;
found.seen.push((c, Some((heuristic, doc))));
let (owner, explicit) = match self.explicit_owner(c.id()) {
Some(o) if doc == own => (o, true),
_ => (heuristic, false),
};
let hit = Hit {
node: c,
blank: a.blank_lines_before(),
explicit,
};
match pick(owner, doc) {
Some(Slot::Leading) => found.leading.push(hit),
Some(Slot::Trailing) => found.trailing.push(hit),
Some(Slot::Footer) => found.footer.push(hit),
None => {}
}
}
for list in [&mut found.leading, &mut found.trailing, &mut found.footer] {
list.sort_by_key(|h| h.node.preorder_index());
}
Ok(())
}
pub(super) fn holds_error_regions(&self) -> bool {
if self.doc.is_none() {
return self.tree().has_errors();
}
let i = self.index.unwrap_or(0);
self.shared.error_docs.binary_search(&i).is_ok()
}
fn collect_runs<'s>(
&'s self,
mut seeds: Vec<TreeNode<'s>>,
found: &mut Found<'s>,
wanted: &[(CommentOwner, Slot)],
) -> Result<(), EditError> {
let tree = self.tree();
if self.holds_error_regions() {
return self.classify(self.attachments(), found, wanted);
}
let own = self.doc_node().map(TreeNode::id);
if let Some(e) = self.explicit.as_deref() {
for (o, _) in wanted {
let k = owner_key(*o, own);
seeds.extend(e.of_owner(k).filter_map(|c| tree.get(c).ok()));
}
}
let decided = self.decide_runs(seeds);
self.classify(decided, found, wanted)
}
pub(super) fn find_comments<'s>(&'s self, x: TreeNode<'s>) -> Result<Found<'s>, EditError> {
let parent = x.parent();
let flow = parent.and_then(Coll::of).is_some_and(Coll::is_flow);
let value = is_value(x);
let key = !value && parent.and_then(Coll::of).is_some_and(Coll::is_map);
let start = entry_start(x);
let mut seeds = Vec::new();
let stop = comments_back(start, flow, &mut seeds);
if start != x {
comments_back(x, flow, &mut seeds);
}
if value {
if let Some(colon) = stop.filter(|s| s.kind() == SyntaxKind::MappingValue) {
comments_back(colon, flow, &mut seeds);
}
}
seeds.extend(
x.children()
.take_while(|c| !place::is_item(*c))
.filter(|c| c.kind() == SyntaxKind::Comment),
);
let mut points = vec![start, x];
let v = if key { value_of(x) } else { Some(x) };
if key {
let mut at = x.next_sibling();
while let Some(s) = at.filter(|s| !place::is_item(*s)) {
if s.kind() == SyntaxKind::MappingValue {
points.push(s);
}
at = s.next_sibling();
}
}
if let Some(v) = v {
points.push(v);
points.extend(
v.children()
.take_while(|c| !place::is_item(*c))
.filter(|c| !c.kind().is_trivia()),
);
if flow {
let mut at = v.next_sibling();
while let Some(s) = at.filter(|s| s.kind().is_trivia()) {
at = s.next_sibling();
}
points.extend(at.filter(|s| s.kind() == SyntaxKind::FlowEntry));
}
}
seeds.extend(points.into_iter().filter_map(comment_after));
match Coll::of(x) {
Some(c) if c.is_flow() => {
if let Some(close) = x.last_child().filter(|l| {
matches!(
l.kind(),
SyntaxKind::FlowSequenceClose | SyntaxKind::FlowMappingClose
)
}) {
comments_back(close, true, &mut seeds);
}
}
Some(_) => comments_forward(x, &mut seeds),
None => {}
}
let me = NodeId(x.id());
let mut found = Found {
blank_lines_before: if line_start(start) {
blank_before(before_indent(start))
} else {
0
},
..Found::default()
};
let wanted = [
(CommentOwner::Leading(me), Slot::Leading),
(CommentOwner::Trailing(me), Slot::Trailing),
(CommentOwner::Footer(me), Slot::Footer),
];
self.collect_runs(seeds, &mut found, &wanted)?;
Ok(found)
}
pub(super) fn find_doc_comments(&self) -> Result<Found<'_>, EditError> {
let tree = self.tree();
let mut found = Found::default();
let Some(d) = self.doc_node() else {
if self.index.is_none() {
for (c, a) in tree.comment_attachments() {
found.seen.push((c, owner_of(&a).map(|o| (o.0, None))));
found.leading.push(Hit {
node: c,
blank: a.blank_lines_before(),
explicit: false,
});
}
}
return Ok(found);
};
let mut seeds = Vec::new();
let root = d.children().find(|c| c.kind().is_yaml_node());
let stop = root.and_then(first_token);
if let Some(ft) = first_token(d) {
let mut at = place::prev_token(ft);
while let Some(t) = at {
if visible(t) && t.ancestors().any(|a| a.kind() == SyntaxKind::Document) {
break;
}
if t.kind() == SyntaxKind::Comment {
seeds.push(t);
}
at = place::prev_token(t);
}
let mut at = Some(ft);
while let Some(t) = at {
if Some(t) == stop || (root.is_none() && in_other_document(t, d)) {
break;
}
if t.kind() == SyntaxKind::Comment {
seeds.push(t);
}
at = place::next_token(t);
}
}
if let Some(r) = root {
let mut at = place::next_token(r);
while let Some(t) = at {
if in_other_document(t, d) {
break;
}
if t.kind() == SyntaxKind::Comment {
seeds.push(t);
}
at = place::next_token(t);
}
}
let wanted = [
(CommentOwner::DocumentHeader, Slot::Leading),
(CommentOwner::DocumentFooter, Slot::Footer),
];
let picked = self.collect_runs(seeds, &mut found, &wanted);
debug_assert!(picked.is_ok(), "unknown comment owner");
picked?;
Ok(found)
}
pub fn attach_comment(&mut self, id: CommentId, owner: CommentOwner) -> Result<(), EditError> {
self.check_own_comment(id)?;
let unsupported = |reason| EditError::Unsupported {
at: oneyaml_core::Path::new(),
reason,
};
match owner {
CommentOwner::DocumentHeader => {}
CommentOwner::DocumentFooter => {
if self.doc_node().is_none() {
return Err(unsupported(
"an input without documents has no document footer",
));
}
}
CommentOwner::Leading(n) | CommentOwner::Trailing(n) => {
if !self.node(n)?.tree_node().kind().is_yaml_node() {
return Err(unsupported("an error region owns no comments"));
}
}
CommentOwner::Footer(n) => {
if Coll::of(self.node(n)?.tree_node()).is_none() {
return Err(unsupported("only a collection has a footer"));
}
}
}
let doc = self.doc_node().map(TreeNode::id);
self.explicit_insert(id.0, owner, doc);
self.prune_explicit();
Ok(())
}
pub fn detach_comment(&mut self, id: CommentId) -> Result<Option<CommentOwner>, EditError> {
self.check_own_comment(id)?;
let live = self.explicit_owner(id.0);
if self.has_explicit(id.0) {
self.explicit_remove(id.0);
}
Ok(live)
}
}
pub(super) fn in_other_document(t: TreeNode<'_>, d: TreeNode<'_>) -> bool {
t.ancestors()
.find(|a| a.kind() == SyntaxKind::Document)
.is_some_and(|a| a != d)
}
impl<'d> NodeRef<'d> {
pub fn comments(self) -> Result<Comments, EditError> {
self.document().comments(self.id())
}
#[must_use]
pub fn key(self) -> Option<NodeRef<'d>> {
let n = self.tree_node();
let parent = n.parent()?;
if !is_value(n) {
return None;
}
let e = place::entry_of(parent, n).ok()?;
e.key.map(|k| NodeRef::new(self.document(), k))
}
}
fn internal() -> EditError {
debug_assert!(false, "comment attachment: unknown rule or owner");
EditError::Internal
}