Files
ruin/lib/reactivity_parsing/examples/json_reactive_parser.rs
Will Temple 4347232b98 Clippy clean
2026-05-16 16:59:09 -04:00

790 lines
24 KiB
Rust

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<String>,
}
#[derive(Clone, Debug)]
struct AstNode {
id: u64,
kind: JsonKind,
local_range: TextRange,
children: Vec<AstNode>,
}
#[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<ParsedNode>,
}
#[derive(Default)]
struct JsonParser {
next_id: u64,
}
struct ReuseContext<'a, 'count> {
old_text: &'a str,
new_text: &'a str,
delta: &'a EditDelta,
reused: &'count mut usize,
}
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 -- <path/to/file.json>");
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::<EditDelta>);
{
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 <text>` 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 <byte_offset> <text>");
println!(" replace <start> <end> <text>");
println!(" replace_all <text>");
println!(" delete <start> <end>");
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 <byte_offset> <text>");
println!(" replace <start> <end> <text>");
println!(" replace_all <text>");
println!(" delete <start> <end>");
println!(" print");
println!(" help");
println!(" quit");
}
Ok(Command::Quit) => break,
Err(error) => eprintln!("{error}"),
}
}
}
fn parse_command(input: &str) -> Result<Command, String> {
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 <offset> <text>".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 <start> <end> <text>".to_string())?;
let end = parts
.next()
.ok_or_else(|| "replace expects: replace <start> <end> <text>".to_string())?;
let text = parts
.next()
.ok_or_else(|| "replace expects: replace <start> <end> <text>".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 <start> <end>".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<ParseSnapshot, String> {
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<ParseSnapshot, String> {
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 mut reuse = ReuseContext {
old_text: &previous.text,
new_text: &text,
delta,
reused: &mut reused,
};
let root = self.reuse_node(
&parsed,
Some(&previous.root),
previous.root_anchor.byte,
root_anchor.byte,
&mut reuse,
);
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,
reuse: &mut ReuseContext<'_, '_>,
) -> 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), reuse.delta) == parsed.range
&& reuse.old_text.get(
absolute_range(old, old_parent_start).start
..absolute_range(old, old_parent_start).end,
) == reuse.new_text.get(parsed.range.start..parsed.range.end)
{
*reuse.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,
reuse,
)
})
.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<ParsedNode, String> {
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<char> {
self.text[self.offset..].chars().next()
}
fn bump_char(&mut self) -> Option<char> {
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<ParsedNode, String> {
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<ParsedNode, String> {
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<ParsedNode, String> {
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<ParsedNode, String> {
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<ParsedNode, String> {
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<ParsedNode, String> {
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::<usize>()
}