use std::env; use std::rc::Rc; use ruin_reactivity::effect_in; use ruin_reactivity_parsing::{ ReactiveRope, ReactiveTokenTree, RopeEdit, TextRange, TokenTreeNode, TokenTreeParse, TokenTreeUpdate, }; use ruin_runtime::{async_main, fs, stdio}; enum Command { Insert { offset: usize, text: String }, Replace { start: usize, end: usize, text: String }, ReplaceAll { text: String }, Delete { start: usize, end: usize }, Print, Help, Quit, } #[async_main] async fn main() { let path = env::args() .nth(1) .expect("usage: cargo run -p ruin-reactivity-parsing --example tokentree_reactive_parser -- "); let initial_text = fs::read_to_string(&path) .await .expect("source file should be readable"); let reactor = ruin_reactivity::Reactor::new(); let rope = ReactiveRope::from_text(&reactor, &initial_text); let parser = Rc::new(ReactiveTokenTree::new(&rope)); effect_in(&reactor, { let parser = Rc::clone(&parser); let rope = rope.clone(); move || { parser.with_snapshot(|snapshot| { let last_update = parser.last_update(); print_snapshot(snapshot, &rope, last_update.as_ref()); }); } }) .leak(); println!("Commands:"); println!(" insert "); println!(" replace "); println!(" replace_all "); println!(" delete "); println!(" print"); println!(" help"); println!(" quit"); let mut input = stdio::stdin().expect("stdin should be available"); while let Some(line) = input.read_line().await.expect("stdin read should succeed") { let trimmed = line.trim_end(); if trimmed.is_empty() { continue; } match parse_command(trimmed) { Ok(Command::Insert { offset, text }) => { let _ = rope.edit(RopeEdit::insert(offset, text)); } Ok(Command::Replace { start, end, text }) => { let _ = rope.edit(RopeEdit::replace(TextRange::new(start, end), text)); } Ok(Command::ReplaceAll { text }) => { let _ = rope.edit(RopeEdit::replace(TextRange::new(0, rope.len_bytes()), text)); } Ok(Command::Delete { start, end }) => { let _ = rope.edit(RopeEdit::delete(TextRange::new(start, end))); } Ok(Command::Print) => { let snapshot = parser.snapshot(); let last_update = parser.last_update(); print_snapshot(&snapshot, &rope, last_update.as_ref()); } Ok(Command::Help) => { println!("Commands:"); println!(" insert "); println!(" replace "); println!(" replace_all "); println!(" delete "); println!(" print"); println!(" help"); println!(" quit"); } Ok(Command::Quit) => break, Err(error) => eprintln!("{error}"), } } } fn parse_command(input: &str) -> Result { if input == "quit" { return Ok(Command::Quit); } if input == "print" { return Ok(Command::Print); } if input == "help" { return Ok(Command::Help); } if let Some(rest) = input.strip_prefix("insert ") { let (offset, text) = rest .split_once(' ') .ok_or_else(|| "insert expects: insert ".to_string())?; return Ok(Command::Insert { offset: offset.parse().map_err(|_| "invalid insert offset".to_string())?, text: text.to_string(), }); } if let Some(rest) = input.strip_prefix("replace ") { let mut parts = rest.splitn(3, ' '); let start = parts .next() .ok_or_else(|| "replace expects: replace ".to_string())?; let end = parts .next() .ok_or_else(|| "replace expects: replace ".to_string())?; let text = parts .next() .ok_or_else(|| "replace expects: replace ".to_string())?; return Ok(Command::Replace { start: start.parse().map_err(|_| "invalid replace start".to_string())?, end: end.parse().map_err(|_| "invalid replace end".to_string())?, text: text.to_string(), }); } if let Some(text) = input.strip_prefix("replace_all ") { return Ok(Command::ReplaceAll { text: text.to_string(), }); } if let Some(rest) = input.strip_prefix("delete ") { let (start, end) = rest .split_once(' ') .ok_or_else(|| "delete expects: delete ".to_string())?; return Ok(Command::Delete { start: start.parse().map_err(|_| "invalid delete start".to_string())?, end: end.parse().map_err(|_| "invalid delete end".to_string())?, }); } Err("unknown command; type `help`".to_string()) } fn print_snapshot(snapshot: &TokenTreeParse, rope: &ReactiveRope, update: Option<&TokenTreeUpdate>) { let metrics = rope.snapshot_metrics(); println!(); println!("Document revision {}", metrics.revision); println!( "Text bytes={}, leaves={}, nodes={}, reused_on_last_edit={}", metrics.text.bytes, metrics.leaves, snapshot.stats.total_nodes, snapshot.stats.reused_nodes ); if let Some(update) = update { println!( "Last edit: [{}..{}) -> {:?}", update.delta.range.start, update.delta.range.end, update.delta.inserted_text ); println!( "Diagnostics changed={}, trivia changed={}", update.diagnostics_changed, update.trivia_changed ); } println!("Parse tree:"); print_tree(snapshot, rope, &snapshot.root, 0); if !snapshot.diagnostics.is_empty() { println!("Diagnostics:"); for diagnostic in &snapshot.diagnostics { println!( " {:?} [{}..{}): {}", diagnostic.kind, diagnostic.range.start, diagnostic.range.end, diagnostic.message ); } } if !snapshot.trivia.comments().is_empty() { println!("Comments:"); for comment in snapshot.trivia.comments() { println!( " {:?} [{}..{}): {}", comment.kind, comment.range.start, comment.range.end, comment.text ); } } } fn print_tree(snapshot: &TokenTreeParse, rope: &ReactiveRope, node: &TokenTreeNode, depth: usize) { let indent = " ".repeat(depth); let (start, end) = snapshot .absolute_span(rope, node.id) .expect("node span should be available"); println!( "{}- id={} kind={} span=({}:{})..({}:{})", indent, node.id, node.kind, start.line, start.column, end.line, end.column ); for child in &node.children { print_tree(snapshot, rope, child, depth + 1); } }