use std::collections::HashMap;
use oneyaml_syntax::tree::{Node as TreeNode, SyntaxKind, SyntaxTree};
use super::comments::{self as cm, Key};
use super::place::{self, Coll};
use super::{CommentOwner, Document, EditError, NodeId, TreeId};
fn internal(what: &str) -> EditError {
debug_assert!(false, "node edit comments: {what}");
EditError::Internal
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub(super) enum Rel {
Inner(u8, usize),
Fixed(Key),
}
#[derive(Clone, Debug)]
pub(super) struct Expect {
pub(super) text: String,
pub(super) want: Want,
pub(super) old: Option<TreeId>,
pub(super) explicit: Option<Rel>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum Want {
Rel(Rel),
Within(usize),
Rewritten,
Collapsed,
}
pub(super) type Moved = (Option<TreeId>, TreeId, Option<Rel>);
pub(super) type PairCarry = (Window, Vec<Expect>, Vec<(TreeId, Rel)>);
#[derive(Clone, Copy, Debug)]
pub(super) struct Window {
pub(super) a: Option<TreeId>,
pub(super) b: Option<TreeId>,
}
#[derive(Clone, Copy, Default)]
pub(super) struct IdHasher(u64);
impl IdHasher {
fn mix(&mut self, n: u64) {
self.0 = (self.0.rotate_left(26) ^ n).wrapping_mul(0x9E37_79B9_7F4A_7C15);
}
}
impl std::hash::Hasher for IdHasher {
fn finish(&self) -> u64 {
self.0 ^ (self.0 >> 29)
}
fn write(&mut self, bytes: &[u8]) {
for b in bytes {
self.mix(u64::from(*b));
}
}
fn write_u8(&mut self, n: u8) {
self.mix(u64::from(n));
}
fn write_u32(&mut self, n: u32) {
self.mix(u64::from(n));
}
fn write_u64(&mut self, n: u64) {
self.mix(n);
}
fn write_usize(&mut self, n: usize) {
self.mix(n as u64);
}
}
pub(super) type IdMap<V> = HashMap<TreeId, V, std::hash::BuildHasherDefault<IdHasher>>;
pub(super) fn strip_blank_after_comment(text: &str) -> String {
let mut out = String::with_capacity(text.len());
let mut seen = false;
for l in split_keep(text) {
let blank = l.trim_matches([' ', '\t', '\n', '\r']).is_empty();
if blank && seen {
continue;
}
seen |= !blank;
out.push_str(l);
}
out
}
pub(super) fn split_keep(text: &str) -> Vec<&str> {
let mut out = Vec::new();
let b = text.as_bytes();
let mut start = 0usize;
let mut i = 0usize;
while let Some(c) = b.get(i).copied() {
let next = i.saturating_add(1);
let end = match c {
b'\r' if b.get(next) == Some(&b'\n') => Some(next.saturating_add(1)),
b'\r' | b'\n' => Some(next),
_ => None,
};
match end {
Some(e) => {
out.extend(text.get(start..e));
start = e;
i = e;
}
None => i = next,
}
}
out.extend(text.get(start..).filter(|r| !r.is_empty()));
out
}
pub(super) fn owner_node(o: CommentOwner) -> Option<(u8, NodeId)> {
match o {
CommentOwner::Leading(n) => Some((0, n)),
CommentOwner::Trailing(n) => Some((1, n)),
CommentOwner::Footer(n) => Some((2, n)),
_ => None,
}
}
pub(super) fn owner_of_kind(k: u8, n: NodeId) -> CommentOwner {
match k {
0 => CommentOwner::Leading(n),
1 => CommentOwner::Trailing(n),
_ => CommentOwner::Footer(n),
}
}
pub(super) fn pair_nodes(m: TreeNode<'_>, skip: Option<usize>) -> Vec<TreeId> {
let items: Vec<TreeNode<'_>> = m
.children()
.filter(|c| c.kind().is_yaml_node())
.enumerate()
.filter(|(i, _)| skip.is_none_or(|s| *i != s && *i != s.saturating_add(1)))
.map(|(_, c)| c)
.collect();
let mut out = vec![m.id()];
for c in items {
out.extend(
c.descendants()
.filter(|n| n.kind().is_yaml_node() || n.kind() == SyntaxKind::Error)
.map(TreeNode::id),
);
}
out
}
pub(super) fn entry_nodes(key: Option<TreeNode<'_>>, value: TreeNode<'_>) -> Vec<TreeId> {
key.into_iter()
.chain(Some(value))
.flat_map(TreeNode::descendants)
.filter(|n| counted(*n))
.map(TreeNode::id)
.collect()
}
fn counted(n: TreeNode<'_>) -> bool {
n.kind().is_yaml_node() || n.kind() == SyntaxKind::Error
}
pub(super) fn entry_positions(
tree: &SyntaxTree,
key: Option<TreeNode<'_>>,
value: TreeNode<'_>,
targets: impl IntoIterator<Item = TreeId>,
) -> IdMap<usize> {
let mut out = IdMap::default();
let mut targets = targets.into_iter().peekable();
if targets.peek().is_none() {
return out;
}
let part = |p: TreeNode<'_>| (p.preorder_index(), p.descendants().len());
let parts = [key.map(part), Some(part(value))];
let inside = |at: usize| {
parts
.iter()
.flatten()
.any(|(s, n)| *s <= at && at < s.saturating_add(*n))
};
let mut wanted: Vec<(usize, TreeId)> = targets
.filter_map(|t| {
let at = tree.get(t).ok()?.preorder_index();
inside(at).then_some((at, t))
})
.collect();
if wanted.is_empty() {
return out;
}
wanted.sort_unstable_by_key(|w| w.0);
wanted.dedup_by_key(|w| w.0);
let mut next = wanted.iter().peekable();
let mut count = 0usize;
for (p, range) in [key, Some(value)].into_iter().zip(parts) {
let (Some(p), Some((start, _))) = (p, range) else {
continue;
};
for (k, n) in p.descendants().enumerate() {
let at = start.saturating_add(k);
while next.next_if(|w| w.0 < at).is_some() {}
let Some(&&(w, id)) = next.peek() else {
return out;
};
if !counted(n) {
continue;
}
if w == at {
out.insert(id, count);
next.next();
}
count = count.saturating_add(1);
}
}
out
}
pub(super) fn window_comments<'t>(
root: TreeNode<'t>,
a: Option<TreeNode<'t>>,
b: Option<TreeNode<'t>>,
) -> Vec<TreeNode<'t>> {
let mut out = Vec::new();
let mut walked = if root.tree().is_edited() { 0 } else { WALKED };
let mut track: Option<(Vec<TreeNode<'t>>, usize)> = None;
let mut at = match a {
Some(a) => after(a, &mut track),
None => Some(root),
};
while let Some(x) = at {
if Some(x) == b {
break;
}
if x.is_token() {
if x.kind() == SyntaxKind::Comment {
out.push(x);
}
at = after(x, &mut track);
continue;
}
if walked < WALKED {
walked = walked.saturating_add(1);
} else {
let (outer, depth) = track.get_or_insert_with(|| {
let mut outer: Vec<TreeNode<'t>> =
b.into_iter().flat_map(TreeNode::ancestors).collect();
outer.reverse();
(outer, x.ancestors().count())
});
if outer.get(*depth) != Some(&x) {
out.extend(x.descendants().filter(|t| t.kind() == SyntaxKind::Comment));
at = after(x, &mut track);
continue;
}
}
at = match x.first_child() {
Some(c) => {
if let Some((_, depth)) = track.as_mut() {
*depth = depth.saturating_add(1);
}
Some(c)
}
None => after(x, &mut track),
};
}
out
}
const WALKED: usize = 32;
fn after<'t, T>(n: TreeNode<'t>, track: &mut Option<(T, usize)>) -> Option<TreeNode<'t>> {
let mut at = n;
loop {
if let Some(s) = at.next_sibling() {
return Some(s);
}
at = at.parent()?;
if let Some((_, depth)) = track.as_mut() {
*depth = depth.saturating_sub(1);
}
}
}
pub(super) fn visible_before(t: TreeNode<'_>) -> Option<TreeNode<'_>> {
let mut at = place::prev_token(t);
while let Some(x) = at {
if cm::visible(x) {
return Some(x);
}
at = place::prev_token(x);
}
None
}
pub(super) fn visible_after(t: TreeNode<'_>) -> Option<TreeNode<'_>> {
let mut at = place::next_token(t);
while let Some(x) = at {
if cm::visible(x) {
return Some(x);
}
at = place::next_token(x);
}
None
}
fn blank(line: &str) -> bool {
line.chars().all(|c| c == ' ' || c == '\t')
}
pub(super) fn split_lines(text: &str) -> Vec<String> {
let mut out = Vec::new();
let mut rest = text;
while !rest.is_empty() {
match rest.find(['\n', '\r']) {
Some(i) => {
out.push(rest.get(..i).unwrap_or("").to_owned());
let brk = if rest.get(i..).is_some_and(|r| r.starts_with("\r\n")) {
2
} else {
1
};
rest = rest.get(i.saturating_add(brk)..).unwrap_or("");
}
None => {
out.push(rest.to_owned());
rest = "";
}
}
}
out
}
pub(super) fn shift_lines(lines: &[String], from: usize, to: usize) -> Vec<String> {
lines
.iter()
.map(|l| {
if from == to || blank(l) {
return l.clone();
}
let t = l.trim_start_matches(' ');
let lead = l.len().saturating_sub(t.len());
let lead = if to >= from {
lead.saturating_add(to.saturating_sub(from))
} else {
lead.saturating_sub(from.saturating_sub(to))
};
format!("{}{t}", " ".repeat(lead))
})
.collect()
}
pub(super) fn line_head(t: TreeNode<'_>) -> TreeNode<'_> {
match place::prev_token(t).filter(|w| w.kind() == SyntaxKind::Whitespace) {
Some(w) if cm::line_start(w) => w,
_ => t,
}
}
pub(super) fn line_end(t: TreeNode<'_>) -> Option<TreeNode<'_>> {
let mut t = t;
while t.span().is_empty() {
match place::prev_token(t) {
Some(p) => t = p,
None => break,
}
}
if cm::ends_line(t) {
return Some(t);
}
let mut at = place::next_token(t);
while let Some(x) = at {
if cm::ends_line(x) {
return Some(x);
}
at = place::next_token(x);
}
None
}
pub(super) fn siblings<'t>(a: TreeNode<'t>, b: TreeNode<'t>) -> Option<Vec<TreeNode<'t>>> {
if a.parent() != b.parent() {
return None;
}
let mut out = Vec::new();
let mut at = Some(a);
while let Some(x) = at {
out.push(x);
if x == b {
return Some(out);
}
at = x.next_sibling();
}
None
}
#[derive(Clone, Debug, Default)]
pub(super) struct Carry {
pub(super) window: Option<Window>,
pub(super) owned: Vec<(TreeId, String, Rel, Option<Rel>)>,
pub(super) rest: Vec<Expect>,
pub(super) lead: Option<(TreeId, TreeId, String)>,
pub(super) tail: Option<(TreeId, TreeId, String, bool)>,
pub(super) outside_explicit: Vec<(TreeId, Rel)>,
pub(super) kept_lead: usize,
}
impl Document {
pub(super) fn rule_owners(&self, comments: &[TreeNode<'_>]) -> Result<IdMap<Key>, EditError> {
let decided = self.decide_runs(comments.iter().copied());
let mut out = IdMap::with_capacity_and_hasher(decided.len(), Default::default());
for (c, a) in decided {
let (o, _) = cm::owner_of(&a).ok_or_else(|| internal("unknown owner"))?;
out.insert(c.id(), (o, a.document().map(TreeNode::id)));
}
Ok(out)
}
pub(super) fn property_carry(
&self,
n: TreeNode<'_>,
first: TreeNode<'_>,
last: TreeNode<'_>,
) -> Result<PropertyCarry, EditError> {
let start = cm::first_token(first).or_else(|| cm::first_token(n));
let end = cm::last_token(last).or_else(|| cm::last_token(n));
let (a, b) = match (start, end) {
(Some(s), Some(e)) => (visible_before(s), visible_after(e)),
_ => (visible_before(n), visible_after(n)),
};
let comments = window_comments(self.tree().root(), a, b);
let owners = self.rule_owners(&comments)?;
let targets = owners
.values()
.filter_map(|k| owner_node(k.0))
.map(|(_, x)| x.0);
let nodes = entry_positions(self.tree(), None, n, targets);
let mut expect = Vec::with_capacity(comments.len());
for c in &comments {
let key = *owners
.get(&c.id())
.ok_or_else(|| internal("an undecided comment"))?;
expect.push(Expect {
text: c.text().into_owned(),
want: Want::Rel(Self::rel(key, &nodes)),
old: Some(c.id()),
explicit: None,
});
}
let mut lead = None;
if let Some(s) = start {
let mut at = place::prev_token(s);
while let Some(t) = at {
match t.kind() {
SyntaxKind::Whitespace | SyntaxKind::LineBreak => {}
SyntaxKind::Comment => {
let inside = owners
.get(&t.id())
.and_then(|k| owner_node(k.0))
.is_some_and(|(_, o)| nodes.contains_key(&o.0));
if !inside || !cm::line_start(line_head(t)) {
break;
}
lead = Some(t.id());
}
_ => break,
}
at = place::prev_token(t);
}
}
Ok(PropertyCarry {
window: Window {
a: a.map(TreeNode::id),
b: b.map(TreeNode::id),
},
expect,
lead,
})
}
fn rel(key: Key, nodes: &IdMap<usize>) -> Rel {
match owner_node(key.0) {
Some((k, n)) => match nodes.get(&n.0) {
Some(i) => Rel::Inner(k, *i),
None => Rel::Fixed(key),
},
None => Rel::Fixed(key),
}
}
pub(super) fn pair_carry(&self, pair: TreeNode<'_>) -> Result<PairCarry, EditError> {
let first = cm::first_token(pair).ok_or_else(|| internal("a pair without tokens"))?;
let last = cm::last_token(pair).ok_or_else(|| internal("a pair without tokens"))?;
let mut wa = visible_before(first);
while let Some(x) = wa.filter(|x| x.kind() == SyntaxKind::FlowEntry) {
wa = visible_before(x);
}
let mut wb = visible_after(last);
while let Some(x) = wb.filter(|x| x.kind() == SyntaxKind::FlowEntry) {
wb = visible_after(x);
}
let window = Window {
a: wa.map(TreeNode::id),
b: wb.map(TreeNode::id),
};
let comments = window_comments(self.tree().root(), wa, wb);
let owners = self.rule_owners(&comments)?;
let list = pair_nodes(pair, None);
let nodes: IdMap<usize> = list.iter().enumerate().map(|(i, n)| (*n, i)).collect();
let doc = self.doc_node().map(TreeNode::id);
let mut expect = Vec::with_capacity(comments.len());
for c in &comments {
let key = *owners
.get(&c.id())
.ok_or_else(|| internal("an undecided comment"))?;
expect.push(Expect {
text: c.text().into_owned(),
want: Want::Rel(Self::rel(key, &nodes)),
old: Some(c.id()),
explicit: self
.explicit_owner(c.id())
.map(|o| Self::rel((o, doc), &nodes)),
});
}
let in_window: std::collections::HashSet<TreeId> =
comments.iter().map(|c| c.id()).collect();
let mut outside = Vec::new();
for (c, o) in self.explicit_owned_by(&list) {
if !in_window.contains(&c) {
if let Rel::Inner(k, i) = Self::rel((o, doc), &nodes) {
outside.push((c, Rel::Inner(k, i)));
}
}
}
Ok((window, expect, outside))
}
pub(super) fn entry_carry(
&self,
coll: TreeNode<'_>,
e: &place::Entry<'_>,
take_blank: bool,
keep_lead: Option<Key>,
around: Option<TreeNode<'_>>,
) -> Result<Carry, EditError> {
let flow = Coll::of(coll).is_some_and(Coll::is_flow);
let start = cm::first_token(e.first).ok_or_else(|| internal("an entry without tokens"))?;
let end = cm::last_token(e.value).ok_or_else(|| internal("an entry without tokens"))?;
let (wa, wb) = match around {
Some(x) => {
let f = cm::first_token(x).ok_or_else(|| internal("a node without tokens"))?;
let l = cm::last_token(x).ok_or_else(|| internal("a node without tokens"))?;
(visible_before(f), visible_after(l))
}
None => {
let mut before = visible_before(start);
while let Some(x) = before.filter(|x| flow && x.kind() == SyntaxKind::FlowEntry) {
before = visible_before(x);
}
let mut after = visible_after(end);
while let Some(x) = after.filter(|x| flow && x.kind() == SyntaxKind::FlowEntry) {
after = visible_after(x);
}
(before, after)
}
};
let root = self.tree().root();
let comments = window_comments(root, wa, wb);
let owners = self.rule_owners(&comments)?;
let explicit = self.any_explicit();
let nodes: IdMap<usize> = if explicit {
entry_nodes(e.key, e.value)
.into_iter()
.enumerate()
.map(|(i, n)| (n, i))
.collect()
} else {
let targets = owners
.values()
.filter_map(|k| owner_node(k.0))
.map(|(_, n)| n.0);
entry_positions(self.tree(), e.key, e.value, targets)
};
let (s_at, e_at) = (start.preorder_index(), end.preorder_index());
let mut carry = Carry {
window: Some(Window {
a: wa.map(TreeNode::id),
b: wb.map(TreeNode::id),
}),
..Carry::default()
};
let mut seen_lead = false;
let mut seen_other_after = false;
let mut lead_first: Option<TreeNode<'_>> = None;
let mut tail_last: Option<TreeNode<'_>> = None;
for c in &comments {
let key = *owners
.get(&c.id())
.ok_or_else(|| internal("an undecided comment"))?;
let at = c.preorder_index();
let pos = if at < s_at {
0
} else if at > e_at {
2
} else {
1
};
let rel = Self::rel(key, &nodes);
let mine = matches!(rel, Rel::Inner(..)) || pos == 1;
let explicit = self
.explicit_owner(c.id())
.map(|o| Self::rel((o, key.1), &nodes));
let lead = (pos == 0 && rel == Rel::Inner(0, 0))
.then_some(keep_lead)
.flatten();
if let Some(home) = lead {
if lead_first.is_some() {
return Err(interleaved());
}
carry.rest.push(Expect {
text: c.text().into_owned(),
want: Want::Rel(Rel::Fixed(home)),
old: Some(c.id()),
explicit: None,
});
carry.kept_lead = carry.kept_lead.saturating_add(1);
seen_lead = true;
continue;
}
match (mine, pos) {
(true, 0) => {
seen_lead = true;
lead_first.get_or_insert(*c);
}
(false, 0) if seen_lead => {
return Err(interleaved());
}
(true, 2) if seen_other_after => return Err(interleaved()),
(true, 2) => {
if cm::line_start(*c) {
tail_last = Some(*c);
}
}
(false, 2) => seen_other_after = true,
_ => {}
}
if mine {
carry
.owned
.push((c.id(), c.text().into_owned(), rel, explicit));
} else {
carry.rest.push(Expect {
text: c.text().into_owned(),
want: Want::Rel(Rel::Fixed(key)),
old: Some(c.id()),
explicit: None,
});
}
}
let mine_ids: std::collections::HashSet<TreeId> = carry.owned.iter().map(|x| x.0).collect();
let inner: Vec<TreeId> = if explicit {
nodes.keys().copied().collect()
} else {
Vec::new()
};
for (c, o) in self.explicit_owned_by(&inner) {
if !mine_ids.contains(&c) {
let doc = self.doc_node().map(TreeNode::id);
if let Rel::Inner(k, i) = Self::rel((o, doc), &nodes) {
carry.outside_explicit.push((c, Rel::Inner(k, i)));
}
}
}
if flow {
return Ok(carry);
}
let head = line_head(start);
let before_head = place::prev_token(head);
let lead_from = lead_first;
let take_blank = take_blank && keep_lead.is_none();
if cm::line_start(start) {
let mut above = before_head;
while let Some(t) = above.filter(|t| lead_from.is_some() && cm::blankish(*t)) {
above = place::prev_token(t);
}
if let Some(lb) = above.filter(|t| t.kind() == SyntaxKind::LineBreak) {
let first = match lead_from {
Some(c) if !take_blank => Some(line_head(c)),
Some(c) => Some(self.after_blank(line_head(c))),
None if take_blank => Some(self.after_blank(head)),
None => None,
};
if let Some(first) = first.filter(|f| *f != head) {
let lines = siblings(first, lb).ok_or_else(|| EditError::NotWellFormed {
at: oneyaml_core::Path::new(),
})?;
let text: String = lines.iter().map(|x| x.text().into_owned()).collect();
carry.lead = Some((first.id(), lb.id(), text));
}
} else if lead_from.is_some() {
return Err(internal("leading comments above a line not ended"));
}
} else if lead_from.is_some() {
return Err(internal(
"leading comments of an entry not starting its line",
));
}
if let Some(last) = tail_last {
let end_of_line = line_end(end).ok_or_else(|| internal("an entry line not ended"))?;
let brk = last
.next_sibling()
.filter(|x| x.kind() == SyntaxKind::LineBreak);
let range = match brk {
Some(b) => place::next_token(end_of_line).map(|f| (f, b, false)),
None => Some((end_of_line, last, true)),
};
let (f, l, shifted) = range.ok_or_else(|| internal("tail lines not found"))?;
let lines = siblings(f, l).ok_or_else(|| EditError::NotWellFormed {
at: oneyaml_core::Path::new(),
})?;
let text: String = lines.iter().map(|x| x.text().into_owned()).collect();
carry.tail = Some((f.id(), l.id(), text, shifted));
}
Ok(carry)
}
pub(super) fn after_blank<'s>(&self, head: TreeNode<'s>) -> TreeNode<'s> {
let mut first = head;
let mut at = place::prev_token(head);
loop {
let Some(lb) = at.filter(|t| t.kind() == SyntaxKind::LineBreak) else {
return first;
};
let mut x = place::prev_token(lb);
let mut ws = None;
while let Some(w) = x.filter(|w| cm::blankish(*w)) {
if w.kind() == SyntaxKind::Whitespace {
ws = Some(w);
}
x = place::prev_token(w);
}
match x {
Some(p) if cm::ends_line(p) => {
first = ws.unwrap_or(lb);
at = Some(p).filter(|p| p.kind() == SyntaxKind::LineBreak);
if at.is_none() {
return first;
}
}
None => return ws.unwrap_or(lb),
Some(_) => return first,
}
}
}
pub(super) fn blank_lines_up<'s>(&self, head: TreeNode<'s>, k: usize) -> TreeNode<'s> {
let top = self.after_blank(head);
let mut starts = Vec::new();
let mut line = true;
let mut at = Some(top);
while let Some(x) = at.filter(|x| *x != head) {
if line {
starts.push(x);
}
line = x.kind() == SyntaxKind::LineBreak;
at = place::next_token(x);
}
let n = starts.len();
starts
.get(n.saturating_sub(k))
.copied()
.filter(|_| k > 0)
.unwrap_or(head)
}
pub(super) fn blank_lines_above(&self, head: TreeNode<'_>) -> usize {
let mut n = 0usize;
let mut at = Some(self.after_blank(head));
while let Some(x) = at.filter(|x| *x != head) {
if x.kind() == SyntaxKind::LineBreak {
n = n.saturating_add(1);
}
at = place::next_token(x);
}
n
}
pub(super) fn check_window(
&self,
w: Window,
expect: &[Expect],
inner: &[TreeId],
) -> Result<Vec<Moved>, EditError> {
let tree = self.tree();
let a = match w.a {
Some(a) => Some(
tree.get(a)
.map_err(|_| internal("the window's start went"))?,
),
None => None,
};
let b = match w.b {
Some(b) => Some(tree.get(b).map_err(|_| internal("the window's end went"))?),
None => None,
};
let found = window_comments(tree.root(), a, b);
if found.len() != expect.len()
|| found
.iter()
.zip(expect)
.any(|(f, x)| f.text().as_ref() != x.text.as_str())
{
return Err(EditError::NotWellFormed {
at: oneyaml_core::Path::new(),
});
}
let owners = self.rule_owners(&found)?;
let doc = self.doc_node().map(TreeNode::id);
let mut out = Vec::with_capacity(found.len());
for (f, x) in found.iter().zip(expect) {
let got = *owners
.get(&f.id())
.ok_or_else(|| internal("an undecided comment"))?;
let ok = match x.want {
Want::Rel(Rel::Fixed(k)) => got == k,
Want::Rel(Rel::Inner(k, i)) => {
let n = inner
.get(i)
.ok_or_else(|| internal("a carried owner outside the entry"))?;
got == (owner_of_kind(k, NodeId(*n)), doc)
}
Want::Within(i) => {
let n = inner
.get(i)
.ok_or_else(|| internal("a carried owner outside the entry"))?;
let key = tree.get(*n).ok().and_then(|x| {
let p = x.parent()?;
Coll::of(p)
.filter(|k| k.is_map())
.and_then(|_| place::prev_item(x).ok().flatten())
.filter(|k| place::is_key(p, *k))
});
owner_node(got.0)
.is_some_and(|(_, o)| o.0 == *n || key.is_some_and(|k| k.id() == o.0))
}
Want::Rewritten => !matches!(
got.0,
CommentOwner::DocumentHeader | CommentOwner::DocumentFooter
),
Want::Collapsed => got.0 != CommentOwner::DocumentHeader,
};
if !ok {
let (_, a) = self
.decide_runs(Some(*f))
.into_iter()
.find(|(c, _)| c == f)
.ok_or_else(|| internal("an undecided comment"))?;
let rule = cm::rule_of(a.rule()).ok_or_else(|| internal("unknown rule"))?;
return Err(EditError::CommentReattaches { rule });
}
out.push((x.old, f.id(), x.explicit));
}
Ok(out)
}
pub(super) fn carry_explicit(
&mut self,
moved: &[Moved],
outside: &[(TreeId, Rel)],
inner: &[TreeId],
renamed: Option<(TreeId, TreeId)>,
) {
if self.explicit.is_none() {
return;
}
let doc = self.doc_node().map(TreeNode::id);
let resolve = |r: Rel| match r {
Rel::Inner(k, i) => inner.get(i).map(|n| owner_of_kind(k, NodeId(*n))),
Rel::Fixed((o, _)) => Some(o),
};
let rename = |o: CommentOwner| match (owner_node(o), renamed) {
(Some((k, n)), Some((from, to))) if n.0 == from => owner_of_kind(k, NodeId(to)),
_ => o,
};
let mut sets: Vec<(TreeId, CommentOwner)> = Vec::new();
let mut drops: Vec<TreeId> = Vec::new();
for (old, new, carried) in moved {
match (old, carried) {
(_, Some(r)) => {
if let Some(o) = resolve(*r) {
sets.push((*new, o));
}
}
(Some(o), None) => {
if let Some(owner) = self.explicit_raw(*o) {
let owner = rename(owner);
if o != new || renamed.is_some() {
sets.push((*new, owner));
}
}
if o != new {
drops.push(*o);
}
}
(None, None) => {}
}
}
let renamed: HashMap<TreeId, TreeId> = if outside.is_empty() {
HashMap::new()
} else {
moved
.iter()
.filter_map(|(o, n, _)| Some(((*o)?, *n)))
.collect()
};
for (c, r) in outside {
let c = renamed.get(c).unwrap_or(c);
if self.tree().get(*c).is_ok() {
if let Some(o) = resolve(*r) {
sets.push((*c, o));
}
}
}
if sets.is_empty() && drops.is_empty() {
return;
}
self.forget_explicit(&drops);
for (c, o) in sets {
if self.owner_is_live(o) {
self.set_explicit(c, o, doc);
}
}
}
}
pub(super) struct PropertyCarry {
pub(super) window: Window,
pub(super) expect: Vec<Expect>,
pub(super) lead: Option<TreeId>,
}
#[derive(Clone, Debug)]
pub(super) struct ReplaceCarry {
pub(super) window: Window,
pub(super) expect: Vec<Expect>,
pub(super) same_line: Option<(TreeId, TreeId)>,
pub(super) lines: Option<(TreeId, TreeId)>,
pub(super) lead: Option<(TreeId, TreeId)>,
pub(super) header: Option<String>,
pub(super) footer_lines: Option<String>,
pub(super) gone: Vec<TreeId>,
}
impl Document {
pub(super) fn replace_carry(
&self,
n: TreeNode<'_>,
first: TreeNode<'_>,
last: TreeNode<'_>,
keep: bool,
) -> Result<ReplaceCarry, EditError> {
let start = cm::first_token(first).ok_or_else(|| internal("a slot without tokens"))?;
let end = cm::last_token(last).ok_or_else(|| internal("a slot without tokens"))?;
let (a, b) = (visible_before(start), visible_after(end));
let comments = window_comments(self.tree().root(), a, b);
let owners = self.rule_owners(&comments)?;
let (n_at, n_len) = (n.preorder_index(), n.descendants().len());
let in_n = |at: usize| n_at <= at && at < n_at.saturating_add(n_len);
let tree = self.tree();
let inside = |o: TreeId| {
tree.get(o)
.is_ok_and(|x| x.kind().is_yaml_node() && in_n(x.preorder_index()))
};
let (s_at, e_at) = (start.preorder_index(), end.preorder_index());
let n_end = cm::last_token(n).map_or(e_at, TreeNode::preorder_index);
let root = n.parent().is_some_and(|p| p.kind() == SyntaxKind::Document);
let mut expect = Vec::new();
let mut gone = Vec::new();
let (mut same_line, mut last_line): (Option<TreeNode<'_>>, Option<TreeNode<'_>>) =
(None, None);
let mut other_after = false;
let mut lead: Option<(TreeNode<'_>, TreeNode<'_>)> = None;
let mut header: Option<String> = None;
let mut folded = false;
let mut kept_inside = false;
let n_first = cm::first_token(n);
for c in &comments {
let key = *owners
.get(&c.id())
.ok_or_else(|| internal("an undecided comment"))?;
let at = c.preorder_index();
let owner = owner_node(key.0);
let within_n = at != n_at && in_n(at);
let pair_outer = n.kind() == SyntaxKind::FlowPair
&& !within_n
&& owner.is_some_and(|(k, o)| k != 2 && inside(o.0));
let own = owner.is_some_and(|(k, o)| o.0 == n.id() && k != 2) || pair_outer;
let mine = owner.is_some_and(|(_, o)| inside(o.0)) && !own;
if keep && within_n {
kept_inside |= !mine && !own;
gone.push(c.id());
continue;
}
let footer = owner.is_some_and(|(k, o)| o.0 == n.id() && k == 2);
if footer && keep && !within_n {
if root {
expect.push(Expect {
text: c.text().into_owned(),
want: Want::Collapsed,
old: Some(c.id()),
explicit: None,
});
continue;
}
if other_after || !cm::line_start(line_head(*c)) {
return Err(EditError::Unsupported {
at: oneyaml_core::Path::new(),
reason: NESTED_COLLAPSE,
});
}
gone.push(c.id());
last_line = Some(*c);
expect.push(Expect {
text: c.text().into_owned(),
want: Want::Within(0),
old: None,
explicit: None,
});
folded = true;
continue;
}
if mine {
gone.push(c.id());
if within_n || (s_at <= at && at <= e_at) {
continue;
}
if at < s_at {
if !cm::line_start(line_head(*c)) {
return Err(interleaved());
}
lead = Some((lead.map_or(*c, |(f, _)| f), *c));
continue;
}
if other_after {
return Err(interleaved());
}
if at > n_end && !cm::line_start(*c) {
same_line = Some(*c);
} else {
last_line = Some(*c);
}
continue;
}
if within_n {
let first_line = n_first.is_some_and(|f| {
let mut t = Some(f);
while let Some(x) = t {
if x == *c {
return true;
}
if x.kind() == SyntaxKind::LineBreak || cm::ends_line(x) {
return false;
}
t = place::next_token(x);
}
false
});
if !first_line || header.is_some() {
return Err(EditError::Unsupported {
at: oneyaml_core::Path::new(),
reason: HEADER_COMMENT_LIMIT,
});
}
let ws = c
.prev_sibling()
.filter(|w| w.kind() == SyntaxKind::Whitespace)
.map_or_else(|| " ".to_owned(), |w| w.text().into_owned());
header = Some(format!("{ws}{}", c.text()));
gone.push(c.id());
expect.push(Expect {
text: c.text().into_owned(),
want: if own {
Want::Within(0)
} else {
Want::Rel(Rel::Fixed(key))
},
old: None,
explicit: None,
});
continue;
}
let want = if own {
Want::Within(0)
} else {
if at > e_at {
other_after = true;
}
Want::Rel(Rel::Fixed(key))
};
expect.push(Expect {
text: c.text().into_owned(),
want,
old: Some(c.id()),
explicit: None,
});
}
let lead = match lead {
Some((f, l)) => {
let not_wf = || EditError::NotWellFormed {
at: oneyaml_core::Path::new(),
};
let from = line_head(f);
let to = l
.next_sibling()
.filter(|x| x.kind() == SyntaxKind::LineBreak)
.ok_or_else(not_wf)?;
let run = siblings(from, to).ok_or_else(interleaved)?;
let ok = run.iter().all(|x| match x.kind() {
SyntaxKind::Whitespace | SyntaxKind::LineBreak | SyntaxKind::ByteOrderMark => {
true
}
SyntaxKind::Comment => gone.contains(&x.id()),
_ => false,
});
if !ok {
return Err(interleaved());
}
Some((from.id(), to.id()))
}
None => None,
};
let not_wf = || EditError::NotWellFormed {
at: oneyaml_core::Path::new(),
};
let same_line = match same_line {
Some(c) => {
let w = c
.prev_sibling()
.filter(|w| w.kind() == SyntaxKind::Whitespace)
.unwrap_or(c);
Some((w.id(), c.id()))
}
None => None,
};
if folded && kept_inside {
return Err(EditError::Unsupported {
at: oneyaml_core::Path::new(),
reason: NESTED_COLLAPSE,
});
}
let mut footer_lines = None;
let lines = match last_line {
Some(l) => {
let e = line_end(end).ok_or_else(not_wf)?;
let from = place::next_token(e).ok_or_else(not_wf)?;
let to = l
.next_sibling()
.filter(|x| x.kind() == SyntaxKind::LineBreak)
.ok_or_else(not_wf)?;
siblings(from, to).ok_or_else(not_wf)?;
if folded {
footer_lines = Some(place::range_text(from, to));
}
Some((from.id(), to.id()))
}
None => None,
};
Ok(ReplaceCarry {
window: Window {
a: a.map(TreeNode::id),
b: b.map(TreeNode::id),
},
expect,
same_line,
lines,
lead,
header,
footer_lines,
gone,
})
}
}
pub(super) const HEADER_COMMENT_LIMIT: &str =
"the entry's comment inside the replaced value cannot be written back there";
pub(super) const NESTED_COLLAPSE: &str =
"the footer comments of a nested collection whose only entry is removed have no owner";
fn interleaved() -> EditError {
debug_assert!(
false,
"comments of the entry and of other owners interleave"
);
EditError::Internal
}
#[cfg(test)]
mod tests {
#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::arithmetic_side_effects
)]
use oneyaml_syntax::tree::{Fragment, Node as TreeNode, SyntaxKind, SyntaxTree};
use super::{IdMap, entry_nodes, entry_positions, window_comments};
fn walked<'t>(
root: TreeNode<'t>,
a: Option<TreeNode<'t>>,
b: Option<TreeNode<'t>>,
) -> Vec<TreeNode<'t>> {
let mut out = Vec::new();
let mut at = match a {
Some(a) => super::place::next_token(a),
None => super::cm::first_token(root),
};
while let Some(t) = at {
if Some(t) == b {
break;
}
if t.kind() == SyntaxKind::Comment {
out.push(t);
}
at = super::place::next_token(t);
}
out
}
fn inputs() -> Vec<String> {
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../test-suite/data/yaml-test-suite");
let mut stack = vec![dir];
let mut out = Vec::new();
while let Some(d) = stack.pop() {
for e in std::fs::read_dir(&d).unwrap() {
let p = e.unwrap().path();
if p.is_dir() {
stack.push(p);
} else if p.file_name().is_some_and(|n| n == "in.yaml") {
out.push(std::fs::read_to_string(&p).unwrap());
}
}
}
assert!(out.len() > 300);
let mut big = String::from("# h\n\na: 0 # t\nbig:\n");
for i in 0..80 {
big.push_str(&format!(" # l{i}\n k{i}: [p, {{q: {i}}}] # t{i}\n"));
}
big.push_str(" # f\nz: 1\n");
out.push(big);
out
}
fn check(tree: &SyntaxTree, src: &str) {
let root = tree.root();
let content: Vec<TreeNode<'_>> = root
.descendants()
.filter(|t| t.is_token() && super::cm::visible(*t))
.collect();
let ends = std::iter::once(None).chain(content.iter().copied().map(Some));
let step_a = 1 + content.len() / 60;
for (i, a) in ends.enumerate().step_by(step_a) {
let step = 1 + content.len() / 24;
let bs = content.iter().skip(i).step_by(step).copied().map(Some);
for b in bs.chain([None]) {
assert_eq!(
window_comments(root, a, b),
walked(root, a, b),
"{src:?} {a:?} {b:?}"
);
}
}
let all: Vec<_> = root.descendants().map(TreeNode::id).collect();
for n in root.descendants().filter(|n| n.kind().is_yaml_node()) {
let key = n.prev_sibling().filter(|k| k.kind().is_yaml_node());
let list: IdMap<usize> = entry_nodes(key, n)
.into_iter()
.enumerate()
.map(|(i, x)| (x, i))
.collect();
let got = entry_positions(tree, key, n, all.iter().copied());
assert_eq!(got, list, "{src:?} {n:?}");
}
}
#[test]
fn windows_and_positions_agree_with_the_walks() {
for src in inputs() {
let mut tree = SyntaxTree::parse(src.as_str());
check(&tree, &src);
let scalars: Vec<_> = tree
.root()
.descendants()
.filter(|n| n.kind() == SyntaxKind::Scalar)
.map(TreeNode::id)
.collect();
let mut edited = false;
for (i, id) in scalars.iter().enumerate().take(6) {
edited |= match i % 3 {
0 => tree.remove(*id).is_ok(),
1 => tree
.insert_after(*id, Fragment::parse("x").unwrap())
.is_ok(),
_ => tree.replace(*id, Fragment::parse("yy").unwrap()).is_ok(),
};
}
if edited {
assert!(tree.is_edited());
check(&tree, &src);
}
}
}
}