use crate::cst::{Item, Node};
use crate::document::Document;
fn item_index_of_data(items: &[Item], data_index: usize) -> Option<usize> {
let mut seen = 0;
for (i, item) in items.iter().enumerate() {
if matches!(item, Item::Node(_)) {
if seen == data_index {
return Some(i);
}
seen += 1;
}
}
None
}
fn separator_before(items: &[Item], item_idx: usize) -> String {
if item_idx > 0 {
if let Some(Item::Ws(w)) = items.get(item_idx - 1) {
if let Some(nl) = w.rfind('\n') {
return format!("\n{}", &w[nl + 1..]);
}
}
}
" ".to_string()
}
fn reflow(node: Node, indent: usize) -> Node {
let text = node.to_pretty(indent);
match Document::parse(&text) {
Ok(doc) => doc
.items
.into_iter()
.find_map(|i| match i {
Item::Node(n) => Some(n),
_ => None,
})
.unwrap_or(node),
Err(_) => node,
}
}
fn indent_of(sep: &str) -> usize {
sep.strip_prefix('\n').map_or(0, str::len)
}
impl Node {
fn items_mut(&mut self) -> Option<&mut Vec<Item>> {
match self {
Node::List { items, .. } => Some(items),
_ => None,
}
}
pub fn data_len(&self) -> usize {
self.list_nodes().count()
}
pub fn data_child(&self, data_index: usize) -> Option<&Node> {
self.list_nodes().nth(data_index)
}
pub fn data_child_mut(&mut self, data_index: usize) -> Option<&mut Node> {
let items = self.items_mut()?;
let idx = item_index_of_data(items, data_index)?;
match &mut items[idx] {
Item::Node(n) => Some(n),
_ => None,
}
}
pub fn position_of<F: Fn(&Node) -> bool>(&self, pred: F) -> Option<usize> {
self.list_nodes().position(pred)
}
pub fn insert_child(&mut self, data_index: usize, node: Node) {
let Some(items) = self.items_mut() else {
return;
};
let Some(idx) = item_index_of_data(items, data_index) else {
self.push_child(node);
return;
};
let sep = separator_before(items, idx);
let node = reflow(node, indent_of(&sep));
items.insert(idx, Item::Node(node));
items.insert(idx + 1, Item::Ws(sep));
}
pub fn push_child(&mut self, node: Node) {
let last = self.data_len().checked_sub(1);
let Some(items) = self.items_mut() else {
return;
};
match last.and_then(|d| item_index_of_data(items, d)) {
Some(idx) => {
let sep = separator_before(items, idx);
let node = reflow(node, indent_of(&sep));
items.insert(idx + 1, Item::Ws(sep));
items.insert(idx + 2, Item::Node(node));
}
None => items.push(Item::Node(reflow(node, 0))),
}
}
pub fn replace_child(&mut self, data_index: usize, node: Node) -> bool {
let Some(items) = self.items_mut() else {
return false;
};
let Some(idx) = item_index_of_data(items, data_index) else {
return false;
};
let sep = separator_before(items, idx);
items[idx] = Item::Node(reflow(node, indent_of(&sep)));
true
}
pub fn remove_child(&mut self, data_index: usize, take_leading_trivia: bool) -> Option<Node> {
let items = self.items_mut()?;
let idx = item_index_of_data(items, data_index)?;
let mut start = idx;
while start > 0 {
match &items[start - 1] {
Item::Ws(_) => start -= 1,
Item::LineComment(_) | Item::BlockComment(_) | Item::DatumComment(_)
if take_leading_trivia =>
{
start -= 1
}
_ => break,
}
}
let mut removed = None;
for item in items.drain(start..=idx) {
if let Item::Node(n) = item {
removed = Some(n);
}
}
removed
}
}
#[cfg(test)]
mod tests {
use crate::{list, sym, Document};
#[test]
fn push_child_indents_like_siblings() {
let mut doc = Document::parse("(list\n (channel a)\n (channel b))").unwrap();
let form = doc.forms_mut().next().unwrap();
form.push_child(list(vec![sym("channel"), sym("c")]));
assert_eq!(
doc.to_string(),
"(list\n (channel a)\n (channel b)\n (channel c))"
);
}
#[test]
fn remove_child_takes_attached_comment() {
let mut doc =
Document::parse("(list\n (channel a)\n ;; b's comment\n (channel b))").unwrap();
let form = doc.forms_mut().next().unwrap();
let idx = form
.position_of(|n| n.to_source().contains("channel b"))
.unwrap();
form.remove_child(idx, true);
assert_eq!(doc.to_string(), "(list\n (channel a))");
}
#[test]
fn remove_child_keeps_unrelated_regions_verbatim() {
let src = "(list\n (channel a) ; weird spacing preserved\n (channel b))";
let mut doc = Document::parse(src).unwrap();
let form = doc.forms_mut().next().unwrap();
let idx = form
.position_of(|n| n.to_source().contains("channel b"))
.unwrap();
form.remove_child(idx, false);
assert!(doc
.to_string()
.contains("(channel a) ; weird spacing preserved"));
}
#[test]
fn insert_before_tail_symbol() {
let mut doc = Document::parse("(cons* (channel a)\n %default-channels)").unwrap();
let form = doc.forms_mut().next().unwrap();
let tail = form
.position_of(|n| n.as_symbol() == Some("%default-channels"))
.unwrap();
form.insert_child(tail, list(vec![sym("channel"), sym("b")]));
assert_eq!(
doc.to_string(),
"(cons* (channel a)\n (channel b)\n %default-channels)"
);
}
}