use crate::primes::Cirru;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CirruFocusOptions {
focus_marker: String,
folded_marker: String,
root_prefix: usize,
non_target_children: usize,
target_children: usize,
non_target_depth: usize,
target_depth: usize,
}
impl Default for CirruFocusOptions {
fn default() -> Self {
Self {
focus_marker: "'FOCUSED".to_string(),
folded_marker: "'FOLDED".to_string(),
root_prefix: 0,
non_target_children: 2,
target_children: 3,
non_target_depth: 2,
target_depth: 2,
}
}
}
impl CirruFocusOptions {
pub fn with_focus_marker(mut self, marker: impl Into<String>) -> Self {
self.focus_marker = marker.into();
self
}
pub fn with_folded_marker(mut self, marker: impl Into<String>) -> Self {
self.folded_marker = marker.into();
self
}
pub fn with_root_prefix(mut self, count: usize) -> Self {
self.root_prefix = count;
self
}
pub fn with_non_target_limits(mut self, children: usize, depth: usize) -> Self {
self.non_target_children = children;
self.non_target_depth = depth;
self
}
pub fn with_target_limits(mut self, children: usize, depth: usize) -> Self {
self.target_children = children;
self.target_depth = depth;
self
}
}
fn folded_place(detail: &str, options: &CirruFocusOptions) -> Cirru {
Cirru::leaf(format!("{}:{detail}", options.folded_marker))
}
fn sibling_anchor(node: &Cirru, options: &CirruFocusOptions) -> Cirru {
match node {
Cirru::Leaf(_) => node.clone(),
Cirru::List(xs) => {
let Some(head) = xs.first() else {
return node.clone();
};
let mut result = vec![head.clone()];
if xs.len() > 1 {
result.push(folded_place(&format!("inside:{}", xs.len() - 1), options));
}
Cirru::List(result)
}
}
}
fn focused_node(node: Cirru, options: &CirruFocusOptions) -> Cirru {
Cirru::List(vec![Cirru::leaf(options.focus_marker.as_str()), node])
}
fn folded_detail<'a>(node: &'a Cirru, options: &CirruFocusOptions) -> Option<&'a str> {
match node {
Cirru::Leaf(value) => value
.strip_prefix(options.folded_marker.as_str())
.and_then(|value| value.strip_prefix(':')),
Cirru::List(_) => None,
}
}
fn merge_folded_runs(nodes: Vec<Cirru>, options: &CirruFocusOptions) -> Vec<Cirru> {
let mut result = Vec::with_capacity(nodes.len());
let mut index = 0;
while index < nodes.len() {
let Some(detail) = folded_detail(&nodes[index], options) else {
result.push(nodes[index].clone());
index += 1;
continue;
};
let mut count = 1;
while index + count < nodes.len() && folded_detail(&nodes[index + count], options).is_some() {
count += 1;
}
if count == 1 {
result.push(nodes[index].clone());
} else {
let same_detail = (1..count).all(|offset| folded_detail(&nodes[index + offset], options) == Some(detail));
let kind = if same_detail { detail } else { "merged" };
result.push(folded_place(&format!("{kind}:{count}"), options));
}
index += count;
}
result
}
pub fn focus_cirru_preview(node: &Cirru, path: &[usize]) -> Cirru {
focus_cirru_preview_with_options(node, path, &CirruFocusOptions::default())
}
pub fn focus_cirru_preview_with_options(node: &Cirru, path: &[usize], options: &CirruFocusOptions) -> Cirru {
focus_cirru_preview_impl(node, path, 0, options)
}
fn focus_cirru_preview_impl(node: &Cirru, path: &[usize], depth: usize, options: &CirruFocusOptions) -> Cirru {
match node {
Cirru::Leaf(_) => {
if path.is_empty() {
focused_node(node.clone(), options)
} else {
node.clone()
}
}
Cirru::List(xs) => {
if path.is_empty() {
return focused_node(
fold_children_impl(node, options.target_children, 0, options.target_depth, options),
options,
);
}
let target_idx = path[0];
if target_idx >= xs.len() {
return fold_children_impl(node, options.non_target_children, 0, options.non_target_depth, options);
}
let rest_path = &path[1..];
let mut result: Vec<Cirru> = Vec::new();
let root_prefix = if depth == 0 { options.root_prefix.min(target_idx) } else { 0 };
if root_prefix > 0 {
result.extend(xs.iter().take(root_prefix).cloned());
let hidden = target_idx - root_prefix;
if hidden > 0 {
result.push(sibling_anchor(&xs[root_prefix], options));
if hidden > 1 {
result.push(folded_place(&format!("before:{}", hidden - 1), options));
}
}
} else {
if target_idx > 0 {
result.push(fold_children_impl(
&xs[0],
options.non_target_children,
0,
options.non_target_depth,
options,
));
}
if target_idx > 1 {
let hidden = target_idx - 1;
result.push(sibling_anchor(&xs[1], options));
if hidden > 1 {
result.push(folded_place(&format!("before:{}", hidden - 1), options));
}
}
}
result.push(focus_cirru_preview_impl(&xs[target_idx], rest_path, depth + 1, options));
let remaining = xs.len() - target_idx - 1;
if remaining > 0 {
result.push(sibling_anchor(&xs[target_idx + 1], options));
if remaining > 1 {
result.push(folded_place(&format!("after:{}", remaining - 1), options));
}
}
Cirru::List(merge_folded_runs(result, options))
}
}
}
fn fold_children_impl(node: &Cirru, max_children: usize, depth: usize, max_depth: usize, options: &CirruFocusOptions) -> Cirru {
match node {
Cirru::Leaf(_) => node.clone(),
Cirru::List(xs) => {
if depth >= max_depth {
return folded_place("max-depth", options);
}
let Some((head, rest)) = xs.split_first() else {
return node.clone();
};
let mut result = vec![fold_children_impl(head, max_children, depth + 1, max_depth, options)];
let hidden = rest.len().saturating_sub(max_children);
if hidden <= 2 {
for c in rest {
result.push(fold_children_impl(c, max_children, depth + 1, max_depth, options));
}
return Cirru::List(merge_folded_runs(result, options));
}
for c in rest.iter().take(max_children) {
result.push(fold_children_impl(c, max_children, depth + 1, max_depth, options));
}
result.push(folded_place(&format!("inside:{hidden}"), options));
Cirru::List(merge_folded_runs(result, options))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn leaf(value: &str) -> Cirru {
Cirru::leaf(value)
}
#[test]
fn marks_a_focused_leaf_without_changing_its_value() {
let tree = Cirru::List(vec![leaf("call"), leaf("first"), leaf("target"), leaf("last")]);
assert_eq!(
focus_cirru_preview(&tree, &[2]),
Cirru::List(vec![
leaf("call"),
leaf("first"),
Cirru::List(vec![leaf("'FOCUSED"), leaf("target")]),
leaf("last"),
])
);
}
#[test]
fn keeps_anchors_and_fold_markers_for_sibling_ranges() {
let tree = Cirru::List(vec![
leaf("pipeline"),
leaf("name"),
Cirru::List(vec![leaf("config"), leaf("a"), leaf("b")]),
leaf("target"),
Cirru::List(vec![leaf("tail"), leaf("x"), leaf("y")]),
leaf("last"),
]);
assert_eq!(
focus_cirru_preview(&tree, &[3]),
Cirru::List(vec![
leaf("pipeline"),
leaf("name"),
leaf("'FOLDED:before:1"),
Cirru::List(vec![leaf("'FOCUSED"), leaf("target")]),
Cirru::List(vec![leaf("tail"), leaf("'FOLDED:inside:2")]),
leaf("'FOLDED:after:1"),
])
);
}
#[test]
fn merges_adjacent_folded_placeholders_with_a_count() {
let tree = Cirru::List(vec![leaf("root"), Cirru::List(vec![leaf("a")]), Cirru::List(vec![leaf("b")])]);
assert_eq!(
fold_children_impl(&tree, 3, 0, 1, &CirruFocusOptions::default()),
Cirru::List(vec![leaf("root"), leaf("'FOLDED:max-depth:2")])
);
}
#[test]
fn out_of_range_path_does_not_mark_a_focus_node() {
let tree = Cirru::List(vec![leaf("root"), leaf("child")]);
assert_eq!(focus_cirru_preview(&tree, &[4]), Cirru::List(vec![leaf("root"), leaf("child")]));
}
#[test]
fn custom_options_preserve_root_signature_and_change_markers() {
let args = Cirru::List(vec![leaf("value"), leaf("options")]);
let tree = Cirru::List(vec![
leaf("defn"),
leaf("render"),
args.clone(),
Cirru::List(vec![leaf("let"), leaf("target"), leaf("tail")]),
leaf("metadata"),
]);
let options = CirruFocusOptions::default()
.with_focus_marker("CURSOR")
.with_folded_marker("FOLDED")
.with_root_prefix(3);
let focused = focus_cirru_preview_with_options(&tree, &[3, 1], &options);
let Cirru::List(items) = focused else {
panic!("focused definition should remain a list")
};
assert_eq!(&items[..3], &[leaf("defn"), leaf("render"), args]);
assert_eq!(
items[3],
Cirru::List(vec![leaf("let"), Cirru::List(vec![leaf("CURSOR"), leaf("target")]), leaf("tail"),])
);
assert_eq!(items[4], leaf("metadata"));
}
#[test]
fn custom_options_tune_target_and_non_target_fold_limits() {
let tree = Cirru::List(vec![leaf("root"), leaf("a"), leaf("b"), leaf("c"), leaf("d")]);
let target_options = CirruFocusOptions::default().with_target_limits(1, 2);
assert_eq!(
focus_cirru_preview_with_options(&tree, &[], &target_options),
Cirru::List(vec![
leaf("'FOCUSED"),
Cirru::List(vec![leaf("root"), leaf("a"), leaf("'FOLDED:inside:3")]),
])
);
let non_target_options = CirruFocusOptions::default().with_non_target_limits(0, 2);
assert_eq!(
focus_cirru_preview_with_options(&tree, &[9], &non_target_options),
Cirru::List(vec![leaf("root"), leaf("'FOLDED:inside:4")])
);
}
}