use cirru_parser::Cirru;
use colored::Colorize;
use super::chunk_display::{ChunkDisplayOptions, ChunkedDisplay, fragment_nesting_level, maybe_chunk_node};
use super::common::{
ERR_CODE_INPUT_REQUIRED, cirru_to_json, format_path, format_path_bracketed, parse_input_to_cirru, parse_path, read_code_input,
};
use super::tips::{TipPriority, Tips, command_guidance_enabled, tip_prefer_oneliner_json, tip_root_edit};
use crate::cli_args::{
TreeAppendChildCommand, TreeCommand, TreeDeleteCommand, TreeInsertAfterCommand, TreeInsertBeforeCommand, TreeInsertChildCommand,
TreeRaiseCommand, TreeReplaceCommand, TreeReplaceLeafCommand, TreeShowCommand, TreeStructuralCommand, TreeSubcommand,
TreeSwapNextCommand, TreeSwapPrevCommand, TreeTargetReplaceCommand, TreeUnwrapCommand, TreeWrapCommand,
};
use super::edit::{
apply_operation_at_path, check_ns_editable, load_snapshot, navigate_to_path, parse_target, process_node_with_references,
save_snapshot,
};
pub fn handle_tree_command(cmd: &TreeCommand, snapshot_file: &str) -> Result<(), String> {
match &cmd.subcommand {
TreeSubcommand::Show(opts) => handle_show(opts, snapshot_file, opts.json),
TreeSubcommand::Replace(opts) => handle_replace(opts, snapshot_file),
TreeSubcommand::ReplaceLeaf(opts) => handle_replace_leaf(opts, snapshot_file),
TreeSubcommand::Delete(opts) => handle_delete(opts, snapshot_file),
TreeSubcommand::InsertBefore(opts) => handle_insert_before(opts, snapshot_file),
TreeSubcommand::InsertAfter(opts) => handle_insert_after(opts, snapshot_file),
TreeSubcommand::InsertChild(opts) => handle_insert_child(opts, snapshot_file),
TreeSubcommand::AppendChild(opts) => handle_append_child(opts, snapshot_file),
TreeSubcommand::SwapNext(opts) => handle_swap_next(opts, snapshot_file),
TreeSubcommand::SwapPrev(opts) => handle_swap_prev(opts, snapshot_file),
TreeSubcommand::Unwrap(opts) => handle_unwrap(opts, snapshot_file),
TreeSubcommand::Raise(opts) => handle_raise(opts, snapshot_file),
TreeSubcommand::Wrap(opts) => handle_wrap(opts, snapshot_file),
TreeSubcommand::TargetReplace(opts) => handle_target_replace(opts, snapshot_file),
TreeSubcommand::Rewrite(opts) => handle_rewrite(opts, snapshot_file),
}
}
fn parse_with_references(with_strs: &[String], original_node: &Cirru) -> Result<std::collections::BTreeMap<String, Cirru>, String> {
let mut references = std::collections::BTreeMap::new();
for s in with_strs {
let (name, path_str) = s
.split_once('=')
.ok_or_else(|| format!("Invalid --with format '{s}'. Expected 'name=path', e.g. --with self=. --with arg=1,0"))?;
if name.trim().is_empty() {
return Err(format!("Invalid --with format '{s}': name cannot be empty"));
}
let path = if path_str.trim() == "." {
vec![]
} else {
parse_path(path_str.trim())?
};
let node = navigate_to_path(original_node, &path)?;
references.insert(name.trim().to_string(), node.clone());
}
Ok(references)
}
fn format_preview(node: &Cirru, max_lines: usize) -> String {
let formatted = match node {
Cirru::Leaf(s) => {
format!(" {:?}", s.as_ref())
}
Cirru::List(_) => {
match cirru_parser::format(std::slice::from_ref(node), cirru_parser::CirruWriterOptions { use_inline: false }) {
Ok(cirru_str) => {
let lines: Vec<&str> = cirru_str.lines().collect();
if lines.len() > max_lines {
let mut result = String::new();
for line in lines.iter().take(max_lines) {
result.push_str(" ");
result.push_str(line);
result.push('\n');
}
result.push_str(&format!(" {}\n", format!("... ({} more lines)", lines.len() - max_lines).dimmed()));
result
} else {
lines.iter().map(|line| format!(" {line}\n")).collect()
}
}
Err(e) => format!(" {}\n", format!("(failed to format: {e})").red()),
}
}
};
formatted.trim_end().to_string()
}
fn format_preview_with_type(node: &Cirru, max_lines: usize) -> String {
let base_preview = format_preview(node, max_lines);
let type_label = match node {
Cirru::Leaf(_) => " (leaf)".dimmed().to_string(),
Cirru::List(items) => {
if items.len() == 1 {
" (expr)".dimmed().to_string()
} else {
String::new()
}
}
};
if type_label.is_empty() {
base_preview
} else {
format!("{base_preview}{type_label}")
}
}
fn print_preview_block(label: &str, node: &Cirru, max_lines: usize, color: &str) {
let label_text = match color {
"yellow" => label.yellow().bold(),
"green" => label.green().bold(),
"cyan" => label.cyan().bold(),
_ => label.bold(),
};
println!("{label_text}:");
for line in format_preview_with_type(node, max_lines).lines() {
println!(" {line}");
}
}
fn first_leaf_preview(node: &Cirru) -> Option<String> {
match node {
Cirru::Leaf(s) => Some(format!("{:?}", s.as_ref())),
Cirru::List(items) => items.iter().find_map(first_leaf_preview),
}
}
fn format_child_preview(node: &Cirru) -> String {
match node {
Cirru::Leaf(s) => format!("{:?}", s.as_ref()),
Cirru::List(items) => {
if items.is_empty() {
"(empty)".to_string()
} else {
let head = first_leaf_preview(node).unwrap_or_else(|| "<expr>".to_string());
format!("({head} ...)")
}
}
}
}
fn show_diff_preview(old_node: &Cirru, new_node: &Cirru, operation: &str) -> String {
let mut output = String::new();
output.push_str(&format!("\n{}: {}\n", "Preview".blue().bold(), operation));
output.push('\n');
let old_preview = format_preview_with_type(old_node, 10);
let new_preview = format_preview_with_type(new_node, 10);
output.push_str(&format!("{}:\n", "Before".yellow().bold()));
output.push_str(&old_preview);
output.push_str("\n\n");
output.push_str(&format!("{}:\n", "After".green().bold()));
output.push_str(&new_preview);
output.push('\n');
output
}
fn render_chunked_display(display: &ChunkedDisplay, chunk_expand_depth: usize) -> usize {
println!("{}", "Chunked preview".green().bold());
println!(
"{}",
format!(
"nodes: {}, branches: {}, leaves: {}, max depth: {}, fragments: {}",
display.total.nodes,
display.total.branches,
display.total.leaves,
display.total.max_depth,
display.fragments.len()
)
.dimmed()
);
println!();
let visible_fragments: Vec<_> = display
.fragments
.iter()
.filter(|fragment| fragment_nesting_level(fragment, &display.fragments) <= chunk_expand_depth)
.collect();
if visible_fragments.len() < display.fragments.len() {
println!(
"{}",
format!(
"showing {}/{} fragments; nested chunks beyond level {} are hidden",
visible_fragments.len(),
display.fragments.len(),
chunk_expand_depth
)
.dimmed()
);
println!();
}
for fragment in &visible_fragments {
println!("{} {}", fragment.id.cyan().bold(), format!("at {}", fragment.coord).dimmed());
println!("{}", format!("nodes: {}, max depth: {}", fragment.nodes, fragment.depth).dimmed());
for line in fragment.cirru.lines() {
println!(" {line}");
}
println!();
}
visible_fragments.len()
}
fn handle_show(opts: &TreeShowCommand, snapshot_file: &str, show_json: bool) -> Result<(), String> {
let (namespace, definition) = parse_target(&opts.target)?;
let path = parse_path(&opts.path)?;
let snapshot = load_snapshot(snapshot_file)?;
let file_data = snapshot
.files
.get(namespace)
.ok_or_else(|| format!("Namespace '{namespace}' not found"))?;
let code_entry = file_data
.defs
.get(definition)
.ok_or_else(|| format!("Definition '{definition}' not found"))?;
let node = match navigate_to_path(&code_entry.code, &path) {
Ok(n) => n,
Err(original_error) => {
let mut valid_depth = 0;
let mut current = &code_entry.code;
for (depth, &idx) in path.iter().enumerate() {
match current {
Cirru::Leaf(_) => {
valid_depth = depth;
break;
}
Cirru::List(items) => {
if idx >= items.len() {
valid_depth = depth;
break;
}
current = &items[idx];
valid_depth = depth + 1;
}
}
}
let valid_path = &path[..valid_depth];
let valid_node = navigate_to_path(&code_entry.code, valid_path).unwrap();
let valid_path_display = format_path_bracketed(valid_path);
let node_preview = match &valid_node {
Cirru::Leaf(s) => format!("{:?} (leaf)", s.as_ref()),
Cirru::List(items) => {
let preview = valid_node.format_one_liner().unwrap_or_else(|_| "<complex>".to_string());
let truncated = if preview.len() > 60 {
format!("{}...", &preview[..60])
} else {
preview
};
format!("{} ({} items)", truncated, items.len())
}
};
eprintln!("{}", "Error: Invalid path".red().bold());
eprintln!("{original_error}");
eprintln!();
eprintln!("{} Longest valid path: {}", "→".cyan(), valid_path_display.yellow());
eprintln!("{} Node at that path: {}", "→".cyan(), node_preview.dimmed());
eprintln!();
match &valid_node {
Cirru::Leaf(_) => {
eprintln!("{} This is a leaf node (cannot navigate deeper)", "Note:".yellow().bold());
eprintln!(
"{} View it with: {}",
"→".cyan(),
format!("cr tree show {} -p '{}'", opts.target, format_path(valid_path)).cyan()
);
}
Cirru::List(items) => {
eprintln!(
"{} This node has {} children (indices 0-{})",
"Available:".green().bold(),
items.len(),
items.len().saturating_sub(1)
);
eprintln!(
"{} View it with: {}",
"→".cyan(),
format!("cr tree show {} -p '{}'", opts.target, format_path(valid_path)).cyan()
);
if !items.is_empty() {
eprintln!();
eprintln!("{} First few children:", "Hint:".blue().bold());
for (i, item) in items.iter().enumerate().take(3) {
let child_preview = format_child_preview(item);
let child_path = if valid_path.is_empty() {
i.to_string()
} else {
format!("{}.{}", format_path(valid_path), i)
};
eprintln!(" [{}] {} {} -p '{}'", i, child_preview.yellow(), "->".dimmed(), child_path);
}
if items.len() > 3 {
eprintln!(" {}", format!("... and {} more", items.len() - 3).dimmed());
}
}
}
}
return Err(String::new()); }
};
let node_type = match &node {
Cirru::Leaf(_) => "leaf",
Cirru::List(items) => {
let chunk_options = ChunkDisplayOptions {
trigger_nodes: opts.chunk_trigger_nodes,
target_nodes: opts.chunk_target_nodes,
max_nodes: opts.chunk_max_nodes,
max_branches: 64,
};
let chunked_display = if opts.raw { None } else { maybe_chunk_node(&node, &chunk_options)? };
println!("{}: {} ({} items)", "Type".green().bold(), "list".yellow(), items.len());
println!();
let shown_fragments = if let Some(display) = chunked_display {
Some((render_chunked_display(&display, opts.chunk_expand_depth), display.fragments.len()))
} else {
println!("{}:", "Cirru preview".green().bold());
println!(" ");
let cirru_str = cirru_parser::format(std::slice::from_ref(&node), cirru_parser::CirruWriterOptions { use_inline: true })
.map_err(|e| format!("Failed to format Cirru: {e}"))?;
for line in cirru_str.lines() {
println!(" {line}");
}
println!();
None
};
if show_json {
println!("{}:", "JSON".green().bold());
println!("{}", cirru_to_json(&node));
if opts.depth > 0 {
println!("{}", format!("(depth limited to {})", opts.depth).dimmed());
}
println!();
}
let mut tips = Tips::new();
if let Some((shown_fragments, total_fragments)) = shown_fragments {
if shown_fragments < total_fragments {
tips.add_with_priority(
TipPriority::High,
format!(
"Showing ROOT plus {} chunk layer(s). Use {} to reveal deeper nested fragments, or {} to disable chunking.",
opts.chunk_expand_depth,
format!("--chunk-expand-depth {}", opts.chunk_expand_depth + 1).yellow(),
"--raw".yellow()
),
);
}
}
tips.append(tip_prefer_oneliner_json(show_json));
tips.print();
return Ok(());
}
};
if matches!(node_type, "leaf") {
println!("{}: {}", "Type".green().bold(), "leaf".yellow());
if let Cirru::Leaf(s) = &node {
println!("{}: {:?}", "Value".green().bold(), s.as_ref());
println!();
if command_guidance_enabled() {
println!(
"{}: Use {} for symbols, {} for strings",
"Tip".blue().bold(),
"-e 'symbol'".yellow(),
"-e '|text'".yellow()
);
}
}
}
Ok(())
}
fn handle_replace(opts: &TreeReplaceCommand, snapshot_file: &str) -> Result<(), String> {
let (namespace, definition) = parse_target(&opts.target)?;
let path = parse_path(&opts.path)?;
let code_input = read_code_input(&opts.file, &opts.code, &opts.json)?;
let raw = code_input.as_deref().ok_or(ERR_CODE_INPUT_REQUIRED)?;
let new_node = parse_input_to_cirru(raw, &opts.json, opts.json_input, opts.leaf, true)?;
let mut snapshot = load_snapshot(snapshot_file)?;
check_ns_editable(&snapshot, namespace)?;
let file_data = snapshot
.files
.get_mut(namespace)
.ok_or_else(|| format!("Namespace '{namespace}' not found"))?;
let code_entry = file_data
.defs
.get_mut(definition)
.ok_or_else(|| format!("Definition '{definition}' not found"))?;
let old_node = navigate_to_path(&code_entry.code, &path)?;
if let Some(t) = tip_root_edit(path.is_empty()) {
let mut tips = Tips::new();
tips.add_with_priority(TipPriority::High, t);
tips.print();
}
let new_code = apply_operation_at_path(&code_entry.code, &path, "replace", Some(&new_node))?;
code_entry.code = new_code.clone();
save_snapshot(&snapshot, snapshot_file)?;
let replaced_node = navigate_to_path(&new_code, &path)?;
println!("{} Replaced node", "✓".green());
println!();
println!("{}", "Changed node".blue().bold());
print_preview_block("Before", &old_node, 20, "yellow");
println!();
print_preview_block("After", &replaced_node, 20, "green");
if !path.is_empty() {
let parent_path = &path[..path.len() - 1];
let parent_after = if parent_path.is_empty() {
new_code.clone()
} else {
navigate_to_path(&new_code, parent_path)?.clone()
};
println!();
println!("{}", "Containing expression".blue().bold());
print_preview_block("After", &parent_after, 12, "cyan");
}
Ok(())
}
fn handle_rewrite(opts: &TreeStructuralCommand, snapshot_file: &str) -> Result<(), String> {
if opts.with.is_empty() {
return Err("`tree rewrite` needs at least one `--with name=path`; for plain replacement use `tree replace`.".to_string());
}
let (namespace, definition) = parse_target(&opts.target)?;
let path = parse_path(&opts.path)?;
let code_input = read_code_input(&opts.file, &opts.code, &opts.json)?;
let raw = code_input.as_deref().ok_or(ERR_CODE_INPUT_REQUIRED)?;
let new_node = parse_input_to_cirru(raw, &opts.json, opts.json_input, opts.leaf, true)?;
let mut snapshot = load_snapshot(snapshot_file)?;
check_ns_editable(&snapshot, namespace)?;
let file_data = snapshot
.files
.get_mut(namespace)
.ok_or_else(|| format!("Namespace '{namespace}' not found"))?;
let code_entry = file_data
.defs
.get_mut(definition)
.ok_or_else(|| format!("Definition '{definition}' not found"))?;
let original_node = navigate_to_path(&code_entry.code, &path)?;
let references = parse_with_references(&opts.with, &original_node)?;
let processed_node = process_node_with_references(&new_node, &references)?;
let old_node = navigate_to_path(&code_entry.code, &path)?;
println!("{}", show_diff_preview(&old_node, &processed_node, "rewrite"));
if let Some(t) = tip_root_edit(path.is_empty()) {
let mut tips = Tips::new();
tips.add_with_priority(TipPriority::High, t);
tips.print();
}
let new_code = apply_operation_at_path(&code_entry.code, &path, "replace", Some(&processed_node))?;
code_entry.code = new_code.clone();
save_snapshot(&snapshot, snapshot_file)?;
println!("{} Applied 'rewrite'", "✓".green());
println!();
println!("{}:", "From".yellow().bold());
println!("{}", format_preview_with_type(&old_node, 20));
println!();
println!("{}:", "To".green().bold());
let new_node = navigate_to_path(&new_code, &path)?;
println!("{}", format_preview_with_type(&new_node, 20));
println!();
Ok(())
}
fn handle_replace_leaf(opts: &TreeReplaceLeafCommand, snapshot_file: &str) -> Result<(), String> {
let (namespace, definition) = parse_target(&opts.target)?;
let code_input = read_code_input(&opts.file, &opts.code, &opts.json)?;
let raw = code_input.as_deref().ok_or(ERR_CODE_INPUT_REQUIRED)?;
let replacement_node = parse_input_to_cirru(raw, &opts.json, opts.json_input, opts.leaf, true)?;
let mut snapshot = load_snapshot(snapshot_file)?;
check_ns_editable(&snapshot, namespace)?;
let file_data = snapshot
.files
.get_mut(namespace)
.ok_or_else(|| format!("Namespace '{namespace}' not found"))?;
let code_entry = file_data
.defs
.get_mut(definition)
.ok_or_else(|| format!("Definition '{definition}' not found"))?;
let matches = find_all_leaf_matches(&code_entry.code, &opts.pattern, &[]);
if matches.is_empty() {
println!("{}", "No matches found.".yellow());
return Ok(());
}
println!("{} {} match(es):", "Search:".bold(), matches.len());
println!();
for (i, (path, old_value)) in matches.iter().enumerate().take(20) {
let path_str = path.iter().map(|idx| idx.to_string()).collect::<Vec<_>>().join(",");
println!(" {}. Path [{}]: {}", i + 1, path_str.dimmed(), format!("{old_value:?}").yellow());
}
if matches.len() > 20 {
println!(" ... and {} more", matches.len() - 20);
}
println!();
let mut new_code = code_entry.code.clone();
let mut replaced_count = 0;
let mut sorted_matches = matches.clone();
sorted_matches.sort_by(|a, b| b.0.cmp(&a.0));
for (path, _) in sorted_matches {
match apply_operation_at_path(&new_code, &path, "replace", Some(&replacement_node)) {
Ok(updated_code) => {
new_code = updated_code;
replaced_count += 1;
}
Err(e) => {
eprintln!(
"{} Failed to replace at path [{}]: {}",
"Warning:".yellow(),
path.iter().map(|i| i.to_string()).collect::<Vec<_>>().join(","),
e
);
}
}
}
code_entry.code = new_code;
save_snapshot(&snapshot, snapshot_file)?;
println!("{} Replaced {} occurrence(s)", "✓".green(), replaced_count);
println!();
println!("{}:", "Replacement".green().bold());
println!(
" {} → {}",
format!("{:?}", opts.pattern).yellow(),
format_preview_with_type(&replacement_node, 0)
);
println!();
Ok(())
}
fn handle_target_replace(opts: &TreeTargetReplaceCommand, snapshot_file: &str) -> Result<(), String> {
let (namespace, definition) = parse_target(&opts.target)?;
let code_input = read_code_input(&opts.file, &opts.code, &opts.json)?;
let raw = code_input.as_deref().ok_or(ERR_CODE_INPUT_REQUIRED)?;
let replacement_node = parse_input_to_cirru(raw, &opts.json, opts.json_input, opts.leaf, true)?;
let mut snapshot = load_snapshot(snapshot_file)?;
check_ns_editable(&snapshot, namespace)?;
let file_data = snapshot
.files
.get_mut(namespace)
.ok_or_else(|| format!("Namespace '{namespace}' not found"))?;
let code_entry = file_data
.defs
.get_mut(definition)
.ok_or_else(|| format!("Definition '{definition}' not found"))?;
let matches = find_all_leaf_matches(&code_entry.code, &opts.pattern, &[]);
if matches.is_empty() {
return Err("No matches found for target pattern".to_string());
}
if matches.len() > 1 {
println!("{} Found {} matches.", "Notice:".yellow().bold(), matches.len());
println!("Please use specific path to replace:");
println!();
let replacement_arg = if let Some(c) = &opts.code {
format!("-e '{c}'")
} else if let Some(j) = &opts.json {
format!("-j '{j}'")
} else if let Some(f) = &opts.file {
format!("-f '{f}'")
} else {
"-e '...'".to_string()
};
for (i, (path, _)) in matches.iter().enumerate().take(10) {
let path_str = path.iter().map(|idx| idx.to_string()).collect::<Vec<_>>().join(",");
println!(
" {}. {} {} -p '{}' {}",
i + 1,
"cr tree replace".cyan(),
opts.target,
path_str,
replacement_arg
);
}
if matches.len() > 10 {
println!(" ... and {} more", matches.len() - 10);
}
println!();
if command_guidance_enabled() {
println!("{}", "Tip: Use 'tree replace-leaf' if you want to replace ALL occurrences.".blue());
}
return Err(String::new());
}
let (path, old_value) = &matches[0];
let old_node = Cirru::Leaf(old_value.to_string().into());
println!("{}", show_diff_preview(&old_node, &replacement_node, "target-replace"));
let new_code = apply_operation_at_path(&code_entry.code, path, "replace", Some(&replacement_node))?;
code_entry.code = new_code;
save_snapshot(&snapshot, snapshot_file)?;
println!("{} Replaced unique occurrence", "✓".green());
Ok(())
}
fn find_all_leaf_matches(node: &Cirru, pattern: &str, current_path: &[usize]) -> Vec<(Vec<usize>, String)> {
let mut results = Vec::new();
match node {
Cirru::Leaf(s) => {
if s.as_ref() == pattern {
results.push((current_path.to_vec(), s.to_string()));
}
}
Cirru::List(items) => {
for (i, item) in items.iter().enumerate() {
let mut new_path = current_path.to_vec();
new_path.push(i);
results.extend(find_all_leaf_matches(item, pattern, &new_path));
}
}
}
results
}
fn handle_delete(opts: &TreeDeleteCommand, snapshot_file: &str) -> Result<(), String> {
let (namespace, definition) = parse_target(&opts.target)?;
let path = parse_path(&opts.path)?;
let mut snapshot = load_snapshot(snapshot_file)?;
check_ns_editable(&snapshot, namespace)?;
let file_data = snapshot
.files
.get_mut(namespace)
.ok_or_else(|| format!("Namespace '{namespace}' not found"))?;
let code_entry = file_data
.defs
.get_mut(definition)
.ok_or_else(|| format!("Definition '{definition}' not found"))?;
let old_node = navigate_to_path(&code_entry.code, &path)?;
let parent_path: Vec<usize> = if path.is_empty() { vec![] } else { path[..path.len() - 1].to_vec() };
let old_parent = if parent_path.is_empty() {
code_entry.code.clone()
} else {
navigate_to_path(&code_entry.code, &parent_path)?
};
println!("\n{}: delete", "Preview".blue().bold());
println!("{}:", "Node to delete".yellow().bold());
println!("{}", format_preview_with_type(&old_node, 10));
println!();
println!("{}:", "Parent context".dimmed());
println!("{}", format_preview_with_type(&old_parent, 8));
println!();
if let Some(t) = tip_root_edit(path.is_empty()) {
let mut tips = Tips::new();
tips.add_with_priority(TipPriority::High, t);
tips.print();
}
let new_code = apply_operation_at_path(&code_entry.code, &path, "delete", None)?;
code_entry.code = new_code.clone();
save_snapshot(&snapshot, snapshot_file)?;
println!("{} Deleted node", "✓".green());
println!();
println!("{}:", "Deleted node".yellow().bold());
println!("{}", format_preview_with_type(&old_node, 20));
println!();
println!("{}:", "Parent after deletion".green().bold());
let new_parent = if parent_path.is_empty() {
new_code.clone()
} else {
navigate_to_path(&new_code, &parent_path)?
};
println!("{}", format_preview_with_type(&new_parent, 20));
println!();
if !path.is_empty() {
let deleted_index = path[path.len() - 1];
println!(
"{}: Sibling nodes after index {} have shifted down by 1",
"⚠️ Index change".yellow().bold(),
deleted_index
);
println!(
" Example: path [{},{}] is now [{},{}]",
parent_path.iter().map(|i| i.to_string()).collect::<Vec<_>>().join(","),
deleted_index + 1,
parent_path.iter().map(|i| i.to_string()).collect::<Vec<_>>().join(","),
deleted_index
);
println!(
" {}: Re-run {} to get updated paths",
"Tip".blue().bold(),
"cr query search".cyan()
);
}
Ok(())
}
fn handle_insert_before(opts: &TreeInsertBeforeCommand, snapshot_file: &str) -> Result<(), String> {
generic_insert_handler(&opts.target, &opts.path, "insert-before", opts, snapshot_file, opts.depth)
}
fn handle_insert_after(opts: &TreeInsertAfterCommand, snapshot_file: &str) -> Result<(), String> {
generic_insert_handler(&opts.target, &opts.path, "insert-after", opts, snapshot_file, opts.depth)
}
fn handle_insert_child(opts: &TreeInsertChildCommand, snapshot_file: &str) -> Result<(), String> {
generic_insert_handler(&opts.target, &opts.path, "insert-child", opts, snapshot_file, opts.depth)
}
fn handle_append_child(opts: &TreeAppendChildCommand, snapshot_file: &str) -> Result<(), String> {
generic_insert_handler(&opts.target, &opts.path, "append-child", opts, snapshot_file, opts.depth)
}
trait InsertOperation {
fn file(&self) -> &Option<String>;
fn code(&self) -> &Option<String>;
fn json(&self) -> &Option<String>;
fn json_input(&self) -> bool;
fn leaf(&self) -> bool;
fn with(&self) -> &[String] {
&[]
}
}
impl InsertOperation for TreeInsertBeforeCommand {
fn file(&self) -> &Option<String> {
&self.file
}
fn code(&self) -> &Option<String> {
&self.code
}
fn json(&self) -> &Option<String> {
&self.json
}
fn json_input(&self) -> bool {
self.json_input
}
fn leaf(&self) -> bool {
self.leaf
}
}
impl InsertOperation for TreeInsertAfterCommand {
fn file(&self) -> &Option<String> {
&self.file
}
fn code(&self) -> &Option<String> {
&self.code
}
fn json(&self) -> &Option<String> {
&self.json
}
fn json_input(&self) -> bool {
self.json_input
}
fn leaf(&self) -> bool {
self.leaf
}
}
impl InsertOperation for TreeInsertChildCommand {
fn file(&self) -> &Option<String> {
&self.file
}
fn code(&self) -> &Option<String> {
&self.code
}
fn json(&self) -> &Option<String> {
&self.json
}
fn json_input(&self) -> bool {
self.json_input
}
fn leaf(&self) -> bool {
self.leaf
}
}
impl InsertOperation for TreeAppendChildCommand {
fn file(&self) -> &Option<String> {
&self.file
}
fn code(&self) -> &Option<String> {
&self.code
}
fn json(&self) -> &Option<String> {
&self.json
}
fn json_input(&self) -> bool {
self.json_input
}
fn leaf(&self) -> bool {
self.leaf
}
}
impl InsertOperation for TreeTargetReplaceCommand {
fn file(&self) -> &Option<String> {
&self.file
}
fn code(&self) -> &Option<String> {
&self.code
}
fn json(&self) -> &Option<String> {
&self.json
}
fn json_input(&self) -> bool {
self.json_input
}
fn leaf(&self) -> bool {
self.leaf
}
}
impl InsertOperation for TreeStructuralCommand {
fn file(&self) -> &Option<String> {
&self.file
}
fn code(&self) -> &Option<String> {
&self.code
}
fn json(&self) -> &Option<String> {
&self.json
}
fn json_input(&self) -> bool {
self.json_input
}
fn leaf(&self) -> bool {
self.leaf
}
fn with(&self) -> &[String] {
&self.with
}
}
fn generic_insert_handler<T: InsertOperation>(
target: &str,
path_str: &str,
operation: &str,
opts: &T,
snapshot_file: &str,
_depth: usize,
) -> Result<(), String> {
let (namespace, definition) = parse_target(target)?;
let path = parse_path(path_str)?;
let code_input = read_code_input(opts.file(), opts.code(), opts.json())?;
let raw = code_input.as_deref().ok_or(ERR_CODE_INPUT_REQUIRED)?;
let new_node = parse_input_to_cirru(raw, opts.json(), opts.json_input(), opts.leaf(), true)?;
let mut snapshot = load_snapshot(snapshot_file)?;
check_ns_editable(&snapshot, namespace)?;
let file_data = snapshot
.files
.get_mut(namespace)
.ok_or_else(|| format!("Namespace '{namespace}' not found"))?;
let code_entry = file_data
.defs
.get_mut(definition)
.ok_or_else(|| format!("Definition '{definition}' not found"))?;
let processed_node = if opts.with().is_empty() {
new_node
} else {
let original_node = navigate_to_path(&code_entry.code, &path)?;
let references = parse_with_references(opts.with(), &original_node)?;
process_node_with_references(&new_node, &references)?
};
let parent_path: Vec<usize> = if path.is_empty() { vec![] } else { path[..path.len() - 1].to_vec() };
let old_parent = if parent_path.is_empty() {
code_entry.code.clone()
} else {
navigate_to_path(&code_entry.code, &parent_path)?
};
println!("\n{}: {}", "Preview".blue().bold(), operation);
println!("{}:", "Node to insert".cyan().bold());
println!("{}", format_preview_with_type(&processed_node, 8));
println!();
println!("{}:", "Parent before".dimmed());
println!("{}", format_preview_with_type(&old_parent, 8));
println!();
if let Some(t) = tip_root_edit(path.is_empty()) {
let mut tips = Tips::new();
tips.add_with_priority(TipPriority::High, t);
tips.print();
}
let new_code = apply_operation_at_path(&code_entry.code, &path, operation, Some(&processed_node))?;
code_entry.code = new_code.clone();
save_snapshot(&snapshot, snapshot_file)?;
println!("{} Applied '{}'", "✓".green(), operation);
println!();
println!("{}:", "Inserted node".cyan().bold());
println!("{}", format_preview_with_type(&processed_node, 10));
println!();
println!("{}:", "Parent before".yellow().bold());
println!("{}", format_preview_with_type(&old_parent, 15));
println!();
println!("{}:", "Parent after".green().bold());
let new_parent = if parent_path.is_empty() {
new_code.clone()
} else {
navigate_to_path(&new_code, &parent_path)?
};
println!("{}", format_preview_with_type(&new_parent, 15));
println!();
match operation {
"insert-before" => {
if !path.is_empty() {
let insert_index = path[path.len() - 1];
println!(
"{}: Node inserted at index {}, original node and siblings shifted up by 1",
"Index impact".yellow().bold(),
insert_index
);
println!(
" Old path [{},{}] → New path [{},{}]",
parent_path.iter().map(|i| i.to_string()).collect::<Vec<_>>().join(","),
insert_index,
parent_path.iter().map(|i| i.to_string()).collect::<Vec<_>>().join(","),
insert_index + 1
);
}
}
"insert-after" => {
if !path.is_empty() {
let ref_index = path[path.len() - 1];
println!(
"{}: Node inserted at index {}, nodes after reference shifted up by 1",
"Index impact".yellow().bold(),
ref_index + 1
);
}
}
"insert-child" => {
println!(
"{}: Node inserted as first child (index 0), all existing children shifted up by 1",
"Index impact".yellow().bold()
);
println!(" Old child [0] → New child [1], [1] → [2], etc.");
}
"append-child" => {
println!(
"{}: Node appended as last child, no index changes to existing nodes",
"Index impact".green().bold()
);
println!(" {}: Use this for multiple insertions to keep paths stable", "Tip".blue().bold());
}
_ => {}
}
Ok(())
}
fn handle_swap_next(opts: &TreeSwapNextCommand, snapshot_file: &str) -> Result<(), String> {
generic_swap_handler(&opts.target, &opts.path, "swap-next-sibling", snapshot_file, opts.depth)
}
fn handle_swap_prev(opts: &TreeSwapPrevCommand, snapshot_file: &str) -> Result<(), String> {
generic_swap_handler(&opts.target, &opts.path, "swap-prev-sibling", snapshot_file, opts.depth)
}
fn generic_swap_handler(target: &str, path_str: &str, operation: &str, snapshot_file: &str, _depth: usize) -> Result<(), String> {
let (namespace, definition) = parse_target(target)?;
let path = parse_path(path_str)?;
let mut snapshot = load_snapshot(snapshot_file)?;
check_ns_editable(&snapshot, namespace)?;
let file_data = snapshot
.files
.get_mut(namespace)
.ok_or_else(|| format!("Namespace '{namespace}' not found"))?;
let code_entry = file_data
.defs
.get_mut(definition)
.ok_or_else(|| format!("Definition '{definition}' not found"))?;
let parent_path: Vec<usize> = if path.is_empty() { vec![] } else { path[..path.len() - 1].to_vec() };
let old_parent = if parent_path.is_empty() {
code_entry.code.clone()
} else {
navigate_to_path(&code_entry.code, &parent_path)?
};
let new_code = apply_operation_at_path(&code_entry.code, &path, operation, None)?;
code_entry.code = new_code.clone();
save_snapshot(&snapshot, snapshot_file)?;
println!("{} Applied '{}'", "✓".green(), operation);
println!();
if !path.is_empty() {
let current_index = path[path.len() - 1];
let parent_display = if parent_path.is_empty() {
"root".to_string()
} else {
format!("[{}]", parent_path.iter().map(|i| i.to_string()).collect::<Vec<_>>().join("."))
};
match operation {
"swap-next-sibling" => {
println!(
"{}: Swapped child [{}] with [{}] under parent {}",
"Index change".yellow().bold(),
current_index,
current_index + 1,
parent_display
);
}
"swap-prev-sibling" => {
println!(
"{}: Swapped child [{}] with [{}] under parent {}",
"Index change".yellow().bold(),
current_index,
current_index - 1,
parent_display
);
}
_ => {}
}
println!();
}
println!("{}:", "Parent before swap".yellow().bold());
println!("{}", format_preview_with_type(&old_parent, 15));
println!();
if let Some(t) = tip_root_edit(path.is_empty()) {
let mut tips = Tips::new();
tips.add_with_priority(TipPriority::High, t);
tips.print();
}
println!("{}:", "Parent after swap".green().bold());
let new_parent = if parent_path.is_empty() {
new_code.clone()
} else {
navigate_to_path(&new_code, &parent_path)?
};
println!("{}", format_preview_with_type(&new_parent, 15));
Ok(())
}
fn splice_at_path(code: &Cirru, path: &[usize]) -> Result<Cirru, String> {
if path.is_empty() {
return Err("Cannot unwrap root node (no parent to splice into)".to_string());
}
splice_recursive(code, path, 0)
}
fn splice_recursive(code: &Cirru, path: &[usize], depth: usize) -> Result<Cirru, String> {
match code {
Cirru::Leaf(_) => Err(format!("Cannot navigate into leaf node at depth {depth}")),
Cirru::List(items) => {
let idx = path[depth];
if idx >= items.len() {
return Err(format!("Path index {} out of bounds (list has {} items)", idx, items.len()));
}
if depth == path.len() - 1 {
let splice_children = match &items[idx] {
Cirru::List(children) => children.clone(),
Cirru::Leaf(_) => return Err("Node at path is a leaf; cannot unwrap".to_string()),
};
let mut new_items: Vec<Cirru> = Vec::with_capacity(items.len() - 1 + splice_children.len());
new_items.extend_from_slice(&items[..idx]);
new_items.extend(splice_children);
new_items.extend_from_slice(&items[idx + 1..]);
Ok(Cirru::List(new_items))
} else {
let mut new_items = items.clone();
new_items[idx] = splice_recursive(&items[idx], path, depth + 1)?;
Ok(Cirru::List(new_items))
}
}
}
}
fn handle_unwrap(opts: &TreeUnwrapCommand, snapshot_file: &str) -> Result<(), String> {
let (namespace, definition) = parse_target(&opts.target)?;
let path = parse_path(&opts.path)?;
if path.is_empty() {
return Err("Cannot unwrap root node (no parent to splice into)".to_string());
}
let mut snapshot = load_snapshot(snapshot_file)?;
check_ns_editable(&snapshot, namespace)?;
let file_data = snapshot
.files
.get_mut(namespace)
.ok_or_else(|| format!("Namespace '{namespace}' not found"))?;
let code_entry = file_data
.defs
.get_mut(definition)
.ok_or_else(|| format!("Definition '{definition}' not found"))?;
let node = navigate_to_path(&code_entry.code, &path)?;
let children = match &node {
Cirru::List(children) => children.clone(),
_ => return Err(format!("Node at path [{}] is a leaf; cannot unwrap", opts.path)),
};
if children.is_empty() {
return Err(format!("Node at path [{}] has no children to splice", opts.path));
}
println!("\n{}: unwrap {} child(ren)", "Preview".blue().bold(), children.len());
println!("{}:", "Before".dimmed());
println!("{}", format_preview_with_type(&node, opts.depth));
println!("{} (spliced):", "After".cyan().bold());
for (i, child) in children.iter().enumerate() {
println!(" [{}] {}", i, format_preview_with_type(child, opts.depth));
}
println!();
let new_code = splice_at_path(&code_entry.code, &path)?;
code_entry.code = new_code;
save_snapshot(&snapshot, snapshot_file)?;
println!("{} Unwrapped node", "✓".green());
Ok(())
}
fn handle_raise(opts: &TreeRaiseCommand, snapshot_file: &str) -> Result<(), String> {
let (namespace, definition) = parse_target(&opts.target)?;
let path = parse_path(&opts.path)?;
if path.is_empty() {
return Err("Cannot raise root node (no parent to replace). Path must have at least one element.".to_string());
}
let parent_path = &path[..path.len() - 1];
let mut snapshot = load_snapshot(snapshot_file)?;
check_ns_editable(&snapshot, namespace)?;
let file_data = snapshot
.files
.get_mut(namespace)
.ok_or_else(|| format!("Namespace '{namespace}' not found"))?;
let code_entry = file_data
.defs
.get_mut(definition)
.ok_or_else(|| format!("Definition '{definition}' not found"))?;
let child_node = navigate_to_path(&code_entry.code, &path)?.clone();
let parent_node = navigate_to_path(&code_entry.code, parent_path)?;
println!("\n{}: raise", "Preview".blue().bold());
println!("{}:", "Before (parent)".dimmed());
println!("{}", format_preview_with_type(&parent_node, opts.depth));
println!("{}:", "After (raised child)".cyan().bold());
println!("{}", format_preview_with_type(&child_node, opts.depth));
println!();
let new_code = apply_operation_at_path(&code_entry.code, parent_path, "replace", Some(&child_node))?;
code_entry.code = new_code;
save_snapshot(&snapshot, snapshot_file)?;
println!("{} Raised node", "✓".green());
Ok(())
}
fn handle_wrap(opts: &TreeWrapCommand, snapshot_file: &str) -> Result<(), String> {
let (namespace, definition) = parse_target(&opts.target)?;
let path = parse_path(&opts.path)?;
let raw = read_code_input(&opts.file, &opts.code, &opts.json)?.ok_or(ERR_CODE_INPUT_REQUIRED)?;
let auto_json = opts.code.is_some();
let template = parse_input_to_cirru(&raw, &opts.json, opts.json_input, opts.leaf, auto_json)?;
let mut snapshot = load_snapshot(snapshot_file)?;
check_ns_editable(&snapshot, namespace)?;
let file_data = snapshot
.files
.get_mut(namespace)
.ok_or_else(|| format!("Namespace '{namespace}' not found"))?;
let code_entry = file_data
.defs
.get_mut(definition)
.ok_or_else(|| format!("Definition '{definition}' not found"))?;
let original_node = navigate_to_path(&code_entry.code, &path)?.clone();
let mut references = std::collections::BTreeMap::new();
references.insert("self".to_string(), original_node.clone());
let new_node = process_node_with_references(&template, &references)?;
println!("\n{}: wrap", "Preview".blue().bold());
println!("{}:", "Before".dimmed());
println!("{}", format_preview_with_type(&original_node, opts.depth));
println!("{}:", "After".cyan().bold());
println!("{}", format_preview_with_type(&new_node, opts.depth));
println!();
let new_code = apply_operation_at_path(&code_entry.code, &path, "replace", Some(&new_node))?;
code_entry.code = new_code;
save_snapshot(&snapshot, snapshot_file)?;
println!("{} Wrapped node", "✓".green());
Ok(())
}