use std::env; use std::fmt; use std::rc::Rc; use ruin_reactivity::{cell_in, effect_in}; use ruin_reactivity_parsing::{ Anchor, AnchorBias, EditDelta, ReactiveRope, RopeEdit, TextPoint, TextRange, }; use ruin_runtime::{async_main, fs, stdio}; #[derive(Clone, Debug)] struct ParseSnapshot { text: String, root_anchor: Anchor, root: AstNode, reused_nodes: usize, total_nodes: usize, diagnostics: Vec, } #[derive(Clone, Debug)] struct AstNode { id: u64, kind: JsonKind, local_range: TextRange, children: Vec, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum JsonKind { Object, Array, Pair, String, Number, True, False, Null, } impl fmt::Display for JsonKind { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let label = match self { Self::Object => "object", Self::Array => "array", Self::Pair => "pair", Self::String => "string", Self::Number => "number", Self::True => "true", Self::False => "false", Self::Null => "null", }; write!(f, "{label}") } } #[derive(Clone, Debug)] struct ParsedNode { kind: JsonKind, range: TextRange, children: Vec, } #[derive(Default)] struct JsonParser { next_id: u64, } struct Cursor<'a> { text: &'a str, offset: usize, } #[derive(Clone)] 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 json_reactive_parser -- "); let initial_text = fs::read_to_string(&path) .await .expect("json file should be readable"); let reactor = ruin_reactivity::Reactor::new(); let rope = ReactiveRope::from_text(&reactor, &initial_text); let parser = Rc::new(std::cell::RefCell::new(JsonParser::default())); let initial_snapshot = parser .borrow_mut() .parse_initial(&rope) .expect("initial JSON should parse"); let parse_state = cell_in(&reactor, initial_snapshot); let last_delta = cell_in(&reactor, None::); let _parser_subscription = { let rope = rope.clone(); let parser = Rc::clone(&parser); let parse_state = parse_state.clone(); let last_delta = last_delta.clone(); rope.edit_events().subscribe(move |delta| { let previous = parse_state.get(); match parser.borrow_mut().parse_incremental(&rope, &previous, delta) { Ok(next) => { last_delta.replace(Some(delta.clone())); parse_state.replace(next); } Err(error) => { eprintln!("parse error after edit: {error}"); eprintln!( "note: current text length is {} bytes; use `replace_all ` to replace the full document without manual offsets", rope.len_bytes() ); } } }); }; effect_in(&reactor, { let rope = rope.clone(); let parse_state = parse_state.clone(); let last_delta = last_delta.clone(); move || { parse_state.with(|snapshot| print_snapshot(snapshot, &rope, last_delta.with(Clone::clone).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 = parse_state.get(); print_snapshot(&snapshot, &rope, None); } 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()) } impl JsonParser { fn parse_initial(&mut self, rope: &ReactiveRope) -> Result { let text = read_rope_text(rope); let parsed = parse_json(&text)?; let root = self.materialize_fresh(&parsed, 0); Ok(ParseSnapshot { text, root_anchor: Anchor::new(0, AnchorBias::Left), total_nodes: count_nodes(&root), reused_nodes: 0, diagnostics: Vec::new(), root, }) } fn parse_incremental( &mut self, rope: &ReactiveRope, previous: &ParseSnapshot, delta: &EditDelta, ) -> Result { let text = read_rope_text(rope); let parsed = parse_json(&text)?; let mut root_anchor = previous.root_anchor; root_anchor.map_through(delta); let mut reused = 0usize; let root = self.reuse_node( &parsed, Some(&previous.root), previous.root_anchor.byte, root_anchor.byte, &previous.text, &text, delta, &mut reused, ); Ok(ParseSnapshot { text, root_anchor, total_nodes: count_nodes(&root), reused_nodes: reused, diagnostics: Vec::new(), root, }) } fn materialize_fresh(&mut self, parsed: &ParsedNode, parent_start: usize) -> AstNode { AstNode { id: self.alloc_id(), kind: parsed.kind, local_range: localize(parsed.range, parent_start), children: parsed .children .iter() .map(|child| self.materialize_fresh(child, parsed.range.start)) .collect(), } } fn reuse_node( &mut self, parsed: &ParsedNode, old: Option<&AstNode>, old_parent_start: usize, new_parent_start: usize, old_text: &str, new_text: &str, delta: &EditDelta, reused: &mut usize, ) -> AstNode { let parsed_local = localize(parsed.range, new_parent_start); if let Some(old) = old && old.kind == parsed.kind && map_range(absolute_range(old, old_parent_start), delta) == parsed.range && old_text .get(absolute_range(old, old_parent_start).start..absolute_range(old, old_parent_start).end) == new_text.get(parsed.range.start..parsed.range.end) { *reused += count_nodes(old); return AstNode { id: old.id, kind: old.kind, local_range: parsed_local, children: old.children.clone(), }; } let children = parsed .children .iter() .enumerate() .map(|(index, child)| { let previous_child = old.and_then(|node| node.children.get(index)); let next_old_parent_start = old .map(|node| absolute_range(node, old_parent_start).start) .unwrap_or(old_parent_start); self.reuse_node( child, previous_child, next_old_parent_start, parsed.range.start, old_text, new_text, delta, reused, ) }) .collect(); AstNode { id: self.alloc_id(), kind: parsed.kind, local_range: parsed_local, children, } } fn alloc_id(&mut self) -> u64 { self.next_id = self.next_id.wrapping_add(1); self.next_id } } fn read_rope_text(rope: &ReactiveRope) -> String { let mut cursor = rope.cursor(0); let mut text = String::new(); while let Some(chunk) = cursor.next_chunk() { text.push_str(chunk.text()); } text } fn localize(range: TextRange, parent_start: usize) -> TextRange { TextRange::new( range.start.saturating_sub(parent_start), range.end.saturating_sub(parent_start), ) } fn absolute_range(node: &AstNode, parent_start: usize) -> TextRange { TextRange::new( parent_start + node.local_range.start, parent_start + node.local_range.end, ) } fn map_range(range: TextRange, delta: &EditDelta) -> TextRange { if range.end <= delta.range.start { return range; } if range.start >= delta.range.end { let shift = delta.net_byte_delta(); return TextRange::new( range.start.saturating_add_signed(shift), range.end.saturating_add_signed(shift), ); } TextRange::new(usize::MAX, usize::MAX) } fn parse_json(text: &str) -> Result { let mut cursor = Cursor { text, offset: 0 }; cursor.skip_ws(); let node = cursor.parse_value()?; cursor.skip_ws(); if !cursor.is_eof() { return Err(format!("unexpected trailing data at byte {}", cursor.offset)); } Ok(node) } impl<'a> Cursor<'a> { fn is_eof(&self) -> bool { self.offset >= self.text.len() } fn skip_ws(&mut self) { while let Some(ch) = self.peek_char() { if ch.is_ascii_whitespace() { self.offset += ch.len_utf8(); } else { break; } } } fn peek_char(&self) -> Option { self.text[self.offset..].chars().next() } fn bump_char(&mut self) -> Option { let ch = self.peek_char()?; self.offset += ch.len_utf8(); Some(ch) } fn expect_char(&mut self, expected: char) -> Result<(), String> { match self.bump_char() { Some(ch) if ch == expected => Ok(()), Some(ch) => Err(format!( "expected `{expected}` at byte {}, found `{ch}`", self.offset.saturating_sub(ch.len_utf8()) )), None => Err(format!("expected `{expected}` at end of file")), } } fn parse_value(&mut self) -> Result { self.skip_ws(); match self.peek_char() { Some('{') => self.parse_object(), Some('[') => self.parse_array(), Some('"') => self.parse_string(), Some('t') => self.parse_literal("true", JsonKind::True), Some('f') => self.parse_literal("false", JsonKind::False), Some('n') => self.parse_literal("null", JsonKind::Null), Some('-' | '0'..='9') => self.parse_number(), Some(other) => Err(format!("unexpected `{other}` at byte {}", self.offset)), None => Err("unexpected end of JSON input".to_string()), } } fn parse_object(&mut self) -> Result { let start = self.offset; self.expect_char('{')?; self.skip_ws(); let mut children = Vec::new(); if self.peek_char() == Some('}') { self.bump_char(); return Ok(ParsedNode { kind: JsonKind::Object, range: TextRange::new(start, self.offset), children, }); } loop { self.skip_ws(); let key = self.parse_string()?; self.skip_ws(); self.expect_char(':')?; self.skip_ws(); let value = self.parse_value()?; let pair = ParsedNode { kind: JsonKind::Pair, range: TextRange::new(key.range.start, value.range.end), children: vec![key, value], }; children.push(pair); self.skip_ws(); match self.peek_char() { Some(',') => { self.bump_char(); } Some('}') => { self.bump_char(); break; } Some(other) => { return Err(format!("expected `,` or `}}` at byte {}, found `{other}`", self.offset)); } None => return Err("unterminated object".to_string()), } } Ok(ParsedNode { kind: JsonKind::Object, range: TextRange::new(start, self.offset), children, }) } fn parse_array(&mut self) -> Result { let start = self.offset; self.expect_char('[')?; self.skip_ws(); let mut children = Vec::new(); if self.peek_char() == Some(']') { self.bump_char(); return Ok(ParsedNode { kind: JsonKind::Array, range: TextRange::new(start, self.offset), children, }); } loop { let value = self.parse_value()?; children.push(value); self.skip_ws(); match self.peek_char() { Some(',') => { self.bump_char(); self.skip_ws(); } Some(']') => { self.bump_char(); break; } Some(other) => { return Err(format!("expected `,` or `]` at byte {}, found `{other}`", self.offset)); } None => return Err("unterminated array".to_string()), } } Ok(ParsedNode { kind: JsonKind::Array, range: TextRange::new(start, self.offset), children, }) } fn parse_string(&mut self) -> Result { let start = self.offset; self.expect_char('"')?; loop { match self.bump_char() { Some('"') => { return Ok(ParsedNode { kind: JsonKind::String, range: TextRange::new(start, self.offset), children: Vec::new(), }); } Some('\\') => match self.bump_char() { Some('"') | Some('\\') | Some('/') | Some('b') | Some('f') | Some('n') | Some('r') | Some('t') => {} Some('u') => { for _ in 0..4 { match self.bump_char() { Some(ch) if ch.is_ascii_hexdigit() => {} _ => return Err(format!("invalid unicode escape at byte {}", self.offset)), } } } _ => return Err(format!("invalid escape sequence at byte {}", self.offset)), }, Some(ch) if ch >= '\u{20}' => {} Some(_) => return Err(format!("control character in string at byte {}", self.offset)), None => return Err("unterminated string".to_string()), } } } fn parse_number(&mut self) -> Result { let start = self.offset; if self.peek_char() == Some('-') { self.bump_char(); } match self.peek_char() { Some('0') => { self.bump_char(); } Some('1'..='9') => { self.bump_char(); while matches!(self.peek_char(), Some('0'..='9')) { self.bump_char(); } } _ => return Err(format!("invalid number at byte {}", self.offset)), } if self.peek_char() == Some('.') { self.bump_char(); let mut digits = 0usize; while matches!(self.peek_char(), Some('0'..='9')) { digits += 1; self.bump_char(); } if digits == 0 { return Err(format!("expected fraction digits at byte {}", self.offset)); } } if matches!(self.peek_char(), Some('e' | 'E')) { self.bump_char(); if matches!(self.peek_char(), Some('+' | '-')) { self.bump_char(); } let mut digits = 0usize; while matches!(self.peek_char(), Some('0'..='9')) { digits += 1; self.bump_char(); } if digits == 0 { return Err(format!("expected exponent digits at byte {}", self.offset)); } } Ok(ParsedNode { kind: JsonKind::Number, range: TextRange::new(start, self.offset), children: Vec::new(), }) } fn parse_literal(&mut self, literal: &str, kind: JsonKind) -> Result { let start = self.offset; if !self.text[self.offset..].starts_with(literal) { return Err(format!("expected `{literal}` at byte {}", self.offset)); } self.offset += literal.len(); Ok(ParsedNode { kind, range: TextRange::new(start, self.offset), children: Vec::new(), }) } } fn print_snapshot(snapshot: &ParseSnapshot, rope: &ReactiveRope, delta: Option<&EditDelta>) { 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.total_nodes, snapshot.reused_nodes ); if let Some(delta) = delta { println!( "Last edit: [{}..{}) -> {:?}", delta.range.start, delta.range.end, delta.inserted_text ); let focus = delta.range.start.min(snapshot.text.len().saturating_sub(1)); let path = path_for_offset(&snapshot.root, snapshot.root_anchor.byte, focus); println!("Changed path:"); for (node, range) in path { let start = rope.point_at(range.start); let end = rope.point_at(range.end); println!( " id={} kind={} span={}", node.id, node.kind, format_span(start, end) ); } } if !snapshot.diagnostics.is_empty() { println!("Diagnostics:"); for diagnostic in &snapshot.diagnostics { println!(" - {diagnostic}"); } } println!("Root id={} kind={}", snapshot.root.id, snapshot.root.kind); println!("Parse tree:"); print_tree(&snapshot.root, snapshot.root_anchor.byte, rope, 0); println!("Current text:"); println!("{}", snapshot.text); } fn print_tree(node: &AstNode, parent_start: usize, rope: &ReactiveRope, depth: usize) { let range = absolute_range(node, parent_start); let start = rope.point_at(range.start); let end = rope.point_at(range.end); let indent = " ".repeat(depth); println!( "{indent}- id={} kind={} span={}", node.id, node.kind, format_span(start, end) ); for child in &node.children { print_tree(child, range.start, rope, depth + 1); } } fn path_for_offset(root: &AstNode, root_start: usize, offset: usize) -> Vec<(&AstNode, TextRange)> { fn walk<'a>( node: &'a AstNode, parent_start: usize, offset: usize, out: &mut Vec<(&'a AstNode, TextRange)>, ) -> bool { let range = absolute_range(node, parent_start); if offset < range.start || offset >= range.end { return false; } out.push((node, range)); for child in &node.children { if walk(child, range.start, offset, out) { return true; } } true } let mut path = Vec::new(); let _ = walk(root, root_start, offset, &mut path); path } fn format_span(start: TextPoint, end: TextPoint) -> String { format!( "{}:{}..{}:{}", start.line + 1, start.column + 1, end.line + 1, end.column + 1 ) } fn count_nodes(node: &AstNode) -> usize { 1 + node.children.iter().map(count_nodes).sum::() }