Lots of owork on a calculator example, transparency, etc.
This commit is contained in:
13
lib/reactivity_parsing/Cargo.toml
Normal file
13
lib/reactivity_parsing/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "ruin-reactivity-parsing"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
name = "ruin_reactivity_parsing"
|
||||
|
||||
[dependencies]
|
||||
ruin_reactivity = { path = "../reactivity" }
|
||||
|
||||
[dev-dependencies]
|
||||
ruin_runtime = { package = "ruin-runtime", path = "../runtime" }
|
||||
745
lib/reactivity_parsing/examples/json_reactive_parser.rs
Normal file
745
lib/reactivity_parsing/examples/json_reactive_parser.rs
Normal file
@@ -0,0 +1,745 @@
|
||||
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 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 _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 <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 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<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>()
|
||||
}
|
||||
207
lib/reactivity_parsing/examples/tokentree_reactive_parser.rs
Normal file
207
lib/reactivity_parsing/examples/tokentree_reactive_parser.rs
Normal file
@@ -0,0 +1,207 @@
|
||||
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 -- <path/to/file>");
|
||||
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 <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 = parser.snapshot();
|
||||
let last_update = parser.last_update();
|
||||
print_snapshot(&snapshot, &rope, last_update.as_ref());
|
||||
}
|
||||
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())
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
152
lib/reactivity_parsing/src/delta.rs
Normal file
152
lib/reactivity_parsing/src/delta.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
use crate::metrics::{TextMetrics, TextPoint};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct TextRange {
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
}
|
||||
|
||||
impl TextRange {
|
||||
pub const fn new(start: usize, end: usize) -> Self {
|
||||
Self { start, end }
|
||||
}
|
||||
|
||||
pub const fn len(self) -> usize {
|
||||
self.end - self.start
|
||||
}
|
||||
|
||||
pub const fn is_empty(self) -> bool {
|
||||
self.start == self.end
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct RopeEdit {
|
||||
pub range: TextRange,
|
||||
pub insert: String,
|
||||
}
|
||||
|
||||
impl RopeEdit {
|
||||
pub fn insert(offset: usize, text: impl Into<String>) -> Self {
|
||||
Self {
|
||||
range: TextRange::new(offset, offset),
|
||||
insert: text.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete(range: TextRange) -> Self {
|
||||
Self {
|
||||
range,
|
||||
insert: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn replace(range: TextRange, text: impl Into<String>) -> Self {
|
||||
Self {
|
||||
range,
|
||||
insert: text.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct EditDelta {
|
||||
pub range: TextRange,
|
||||
pub removed_text: String,
|
||||
pub inserted_text: String,
|
||||
pub removed: TextMetrics,
|
||||
pub inserted: TextMetrics,
|
||||
pub start: TextPoint,
|
||||
pub old_end: TextPoint,
|
||||
pub new_end: TextPoint,
|
||||
}
|
||||
|
||||
impl EditDelta {
|
||||
pub fn net_byte_delta(&self) -> isize {
|
||||
self.inserted.bytes as isize - self.removed.bytes as isize
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum AnchorBias {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct Anchor {
|
||||
pub byte: usize,
|
||||
pub bias: AnchorBias,
|
||||
}
|
||||
|
||||
impl Anchor {
|
||||
pub const fn new(byte: usize, bias: AnchorBias) -> Self {
|
||||
Self { byte, bias }
|
||||
}
|
||||
|
||||
pub fn map_through(&mut self, delta: &EditDelta) {
|
||||
if self.byte < delta.range.start {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.byte > delta.range.end {
|
||||
self.byte = self.byte.saturating_add_signed(delta.net_byte_delta());
|
||||
return;
|
||||
}
|
||||
|
||||
self.byte = match self.bias {
|
||||
AnchorBias::Left => delta.range.start,
|
||||
AnchorBias::Right => delta.range.start + delta.inserted.bytes,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Anchor, AnchorBias, EditDelta, RopeEdit, TextRange};
|
||||
use crate::{TextMetrics, TextPoint};
|
||||
|
||||
#[test]
|
||||
fn constructors_build_expected_edits() {
|
||||
assert_eq!(RopeEdit::insert(2, "x").range, TextRange::new(2, 2));
|
||||
assert_eq!(RopeEdit::delete(TextRange::new(2, 4)).insert, "");
|
||||
assert_eq!(RopeEdit::replace(TextRange::new(1, 3), "zz").insert, "zz");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_range_helpers_report_size() {
|
||||
let range = TextRange::new(3, 7);
|
||||
assert_eq!(range.len(), 4);
|
||||
assert!(!range.is_empty());
|
||||
assert!(TextRange::new(5, 5).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anchors_map_before_inside_and_after_edits() {
|
||||
let delta = EditDelta {
|
||||
range: TextRange::new(4, 7),
|
||||
removed_text: "abc".into(),
|
||||
inserted_text: "x".into(),
|
||||
removed: TextMetrics::from_text("abc"),
|
||||
inserted: TextMetrics::from_text("x"),
|
||||
start: TextPoint::origin().advance("1234"),
|
||||
old_end: TextPoint::origin().advance("1234abc"),
|
||||
new_end: TextPoint::origin().advance("1234x"),
|
||||
};
|
||||
|
||||
let mut before = Anchor::new(2, AnchorBias::Left);
|
||||
let mut inside_left = Anchor::new(5, AnchorBias::Left);
|
||||
let mut inside_right = Anchor::new(5, AnchorBias::Right);
|
||||
let mut after = Anchor::new(10, AnchorBias::Left);
|
||||
|
||||
before.map_through(&delta);
|
||||
inside_left.map_through(&delta);
|
||||
inside_right.map_through(&delta);
|
||||
after.map_through(&delta);
|
||||
|
||||
assert_eq!(before.byte, 2);
|
||||
assert_eq!(inside_left.byte, 4);
|
||||
assert_eq!(inside_right.byte, 5);
|
||||
assert_eq!(after.byte, 8);
|
||||
}
|
||||
}
|
||||
24
lib/reactivity_parsing/src/lib.rs
Normal file
24
lib/reactivity_parsing/src/lib.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
//! Reactive rope text storage and incremental parsing substrate for RUIN.
|
||||
|
||||
mod delta;
|
||||
mod metrics;
|
||||
mod parse;
|
||||
mod rope;
|
||||
mod tokentree;
|
||||
|
||||
pub use delta::{Anchor, AnchorBias, EditDelta, RopeEdit, TextRange};
|
||||
pub use metrics::{TextMetrics, TextPoint};
|
||||
pub use parse::{
|
||||
IncrementalParser, ParseError, ParseInvalidation, ParseResult, ParserCheckpoint, ParseTree,
|
||||
SyntaxFragment,
|
||||
};
|
||||
pub use rope::{
|
||||
DEFAULT_LEAF_TARGET_BYTES, ReactiveRope, RopeBuilder, RopeChunk, RopeCursor,
|
||||
RopeSnapshotMetrics,
|
||||
};
|
||||
pub use tokentree::{
|
||||
DelimiterKind, NumberBase, NumberLiteral, NumberSuffix, StringDelimiter, StringFragment,
|
||||
ReactiveTokenTree, TokenTreeDiagnostic, TokenTreeDiagnosticKind, TokenTreeNode,
|
||||
TokenTreeNodeKind, TokenTreeParse, TokenTreeParser, TokenTreeStats, TokenTreeUpdate,
|
||||
TriviaComment, TriviaKind, TriviaStore,
|
||||
};
|
||||
139
lib/reactivity_parsing/src/metrics.rs
Normal file
139
lib/reactivity_parsing/src/metrics.rs
Normal file
@@ -0,0 +1,139 @@
|
||||
use core::fmt;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub struct TextMetrics {
|
||||
pub bytes: usize,
|
||||
pub chars: usize,
|
||||
pub utf16: usize,
|
||||
pub line_breaks: usize,
|
||||
}
|
||||
|
||||
impl TextMetrics {
|
||||
pub fn from_text(text: &str) -> Self {
|
||||
let mut metrics = Self::default();
|
||||
for ch in text.chars() {
|
||||
metrics.bytes += ch.len_utf8();
|
||||
metrics.chars += 1;
|
||||
metrics.utf16 += ch.len_utf16();
|
||||
metrics.line_breaks += usize::from(ch == '\n');
|
||||
}
|
||||
metrics
|
||||
}
|
||||
}
|
||||
|
||||
impl core::ops::Add for TextMetrics {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Self {
|
||||
bytes: self.bytes + rhs.bytes,
|
||||
chars: self.chars + rhs.chars,
|
||||
utf16: self.utf16 + rhs.utf16,
|
||||
line_breaks: self.line_breaks + rhs.line_breaks,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl core::ops::AddAssign for TextMetrics {
|
||||
fn add_assign(&mut self, rhs: Self) {
|
||||
*self = *self + rhs;
|
||||
}
|
||||
}
|
||||
|
||||
impl core::ops::Sub for TextMetrics {
|
||||
type Output = Self;
|
||||
|
||||
fn sub(self, rhs: Self) -> Self::Output {
|
||||
Self {
|
||||
bytes: self.bytes - rhs.bytes,
|
||||
chars: self.chars - rhs.chars,
|
||||
utf16: self.utf16 - rhs.utf16,
|
||||
line_breaks: self.line_breaks - rhs.line_breaks,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default, Eq, PartialEq)]
|
||||
pub struct TextPoint {
|
||||
pub byte: usize,
|
||||
pub char_offset: usize,
|
||||
pub utf16_offset: usize,
|
||||
pub line: usize,
|
||||
pub column: usize,
|
||||
}
|
||||
|
||||
impl TextPoint {
|
||||
pub const fn origin() -> Self {
|
||||
Self {
|
||||
byte: 0,
|
||||
char_offset: 0,
|
||||
utf16_offset: 0,
|
||||
line: 0,
|
||||
column: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advance(mut self, text: &str) -> Self {
|
||||
self.byte += text.len();
|
||||
for ch in text.chars() {
|
||||
self.char_offset += 1;
|
||||
self.utf16_offset += ch.len_utf16();
|
||||
if ch == '\n' {
|
||||
self.line += 1;
|
||||
self.column = 0;
|
||||
} else {
|
||||
self.column += 1;
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for TextPoint {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("TextPoint")
|
||||
.field("byte", &self.byte)
|
||||
.field("char_offset", &self.char_offset)
|
||||
.field("utf16_offset", &self.utf16_offset)
|
||||
.field("line", &self.line)
|
||||
.field("column", &self.column)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{TextMetrics, TextPoint};
|
||||
|
||||
#[test]
|
||||
fn metrics_count_utf8_utf16_and_lines() {
|
||||
let metrics = TextMetrics::from_text("a\né🙂");
|
||||
assert_eq!(metrics.bytes, "a\né🙂".len());
|
||||
assert_eq!(metrics.chars, 4);
|
||||
assert_eq!(metrics.utf16, 5);
|
||||
assert_eq!(metrics.line_breaks, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metrics_support_arithmetic() {
|
||||
let left = TextMetrics::from_text("hello\n");
|
||||
let right = TextMetrics::from_text("🙂");
|
||||
let combined = left + right;
|
||||
|
||||
assert_eq!(combined.bytes, "hello\n🙂".len());
|
||||
assert_eq!(combined.chars, 7);
|
||||
assert_eq!(combined.utf16, 8);
|
||||
assert_eq!(combined.line_breaks, 1);
|
||||
assert_eq!(combined - right, left);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn point_advance_tracks_offsets() {
|
||||
let point = TextPoint::origin().advance("ab\n🙂");
|
||||
assert_eq!(point.byte, "ab\n🙂".len());
|
||||
assert_eq!(point.char_offset, 4);
|
||||
assert_eq!(point.utf16_offset, 5);
|
||||
assert_eq!(point.line, 1);
|
||||
assert_eq!(point.column, 1);
|
||||
}
|
||||
}
|
||||
162
lib/reactivity_parsing/src/parse.rs
Normal file
162
lib/reactivity_parsing/src/parse.rs
Normal file
@@ -0,0 +1,162 @@
|
||||
use core::fmt;
|
||||
|
||||
use crate::{RopeCursor, TextRange};
|
||||
|
||||
pub trait ParserCheckpoint: Clone + Eq + fmt::Debug + 'static {}
|
||||
|
||||
impl<T> ParserCheckpoint for T where T: Clone + Eq + fmt::Debug + 'static {}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct SyntaxFragment<C, T, D> {
|
||||
pub range: TextRange,
|
||||
pub start_checkpoint: C,
|
||||
pub end_checkpoint: C,
|
||||
pub tree: T,
|
||||
pub diagnostics: Vec<D>,
|
||||
pub grammar_revision: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ParseTree<T> {
|
||||
pub root: T,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ParseInvalidation<C> {
|
||||
pub resume_at: usize,
|
||||
pub restart_checkpoint: C,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ParseResult<C, T, D> {
|
||||
pub tree: ParseTree<T>,
|
||||
pub fragments: Vec<SyntaxFragment<C, T, D>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ParseError {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl ParseError {
|
||||
pub fn new(message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IncrementalParser {
|
||||
type Checkpoint: ParserCheckpoint;
|
||||
type Tree: Clone + fmt::Debug + 'static;
|
||||
type Diagnostic: Clone + fmt::Debug + 'static;
|
||||
|
||||
fn grammar_revision(&self) -> u64;
|
||||
|
||||
fn initial_checkpoint(&self) -> Self::Checkpoint;
|
||||
|
||||
fn invalidate(
|
||||
&self,
|
||||
delta_range: TextRange,
|
||||
previous_fragments: &[SyntaxFragment<Self::Checkpoint, Self::Tree, Self::Diagnostic>],
|
||||
) -> ParseInvalidation<Self::Checkpoint>;
|
||||
|
||||
fn parse(
|
||||
&self,
|
||||
cursor: RopeCursor,
|
||||
fragments: &[SyntaxFragment<Self::Checkpoint, Self::Tree, Self::Diagnostic>],
|
||||
invalidation: ParseInvalidation<Self::Checkpoint>,
|
||||
) -> Result<ParseResult<Self::Checkpoint, Self::Tree, Self::Diagnostic>, ParseError>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::cell::Cell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use ruin_reactivity::Reactor;
|
||||
|
||||
use super::{
|
||||
IncrementalParser, ParseError, ParseInvalidation, ParseResult, ParseTree, SyntaxFragment,
|
||||
};
|
||||
use crate::{ReactiveRope, RopeCursor, TextRange};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct DummyParser {
|
||||
parse_calls: Rc<Cell<usize>>,
|
||||
}
|
||||
|
||||
impl IncrementalParser for DummyParser {
|
||||
type Checkpoint = u8;
|
||||
type Tree = String;
|
||||
type Diagnostic = String;
|
||||
|
||||
fn grammar_revision(&self) -> u64 {
|
||||
7
|
||||
}
|
||||
|
||||
fn initial_checkpoint(&self) -> Self::Checkpoint {
|
||||
0
|
||||
}
|
||||
|
||||
fn invalidate(
|
||||
&self,
|
||||
delta_range: TextRange,
|
||||
_previous_fragments: &[SyntaxFragment<Self::Checkpoint, Self::Tree, Self::Diagnostic>],
|
||||
) -> ParseInvalidation<Self::Checkpoint> {
|
||||
ParseInvalidation {
|
||||
resume_at: delta_range.start.saturating_sub(1),
|
||||
restart_checkpoint: 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(
|
||||
&self,
|
||||
mut cursor: RopeCursor,
|
||||
_fragments: &[SyntaxFragment<Self::Checkpoint, Self::Tree, Self::Diagnostic>],
|
||||
invalidation: ParseInvalidation<Self::Checkpoint>,
|
||||
) -> Result<ParseResult<Self::Checkpoint, Self::Tree, Self::Diagnostic>, ParseError> {
|
||||
self.parse_calls.set(self.parse_calls.get() + 1);
|
||||
let text = cursor.read_bytes(usize::MAX);
|
||||
Ok(ParseResult {
|
||||
tree: ParseTree { root: text.clone() },
|
||||
fragments: vec![SyntaxFragment {
|
||||
range: TextRange::new(invalidation.resume_at, invalidation.resume_at + text.len()),
|
||||
start_checkpoint: invalidation.restart_checkpoint,
|
||||
end_checkpoint: 2,
|
||||
tree: text,
|
||||
diagnostics: Vec::new(),
|
||||
grammar_revision: self.grammar_revision(),
|
||||
}],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_error_constructor_sets_message() {
|
||||
let error = ParseError::new("bad");
|
||||
assert_eq!(error.message, "bad");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parser_trait_round_trip_produces_fragments() {
|
||||
let reactor = Reactor::new();
|
||||
let rope = ReactiveRope::from_text(&reactor, "{\"a\":1}");
|
||||
let parser = DummyParser {
|
||||
parse_calls: Rc::new(Cell::new(0)),
|
||||
};
|
||||
|
||||
let invalidation = parser.invalidate(TextRange::new(3, 4), &[]);
|
||||
assert_eq!(invalidation.resume_at, 2);
|
||||
assert_eq!(invalidation.restart_checkpoint, 1);
|
||||
|
||||
let result = parser
|
||||
.parse(rope.cursor(0), &[], invalidation)
|
||||
.expect("dummy parse should succeed");
|
||||
|
||||
assert_eq!(result.tree.root, "{\"a\":1}");
|
||||
assert_eq!(result.fragments.len(), 1);
|
||||
assert_eq!(result.fragments[0].grammar_revision, 7);
|
||||
assert_eq!(parser.parse_calls.get(), 1);
|
||||
}
|
||||
}
|
||||
720
lib/reactivity_parsing/src/rope.rs
Normal file
720
lib/reactivity_parsing/src/rope.rs
Normal file
@@ -0,0 +1,720 @@
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::cmp::min;
|
||||
use std::collections::BTreeSet;
|
||||
use std::rc::Rc;
|
||||
|
||||
use ruin_reactivity::{Event, Reactor, Source, event_in, source_in};
|
||||
|
||||
use crate::delta::{EditDelta, RopeEdit, TextRange};
|
||||
use crate::metrics::{TextMetrics, TextPoint};
|
||||
|
||||
pub const DEFAULT_LEAF_TARGET_BYTES: usize = 1024;
|
||||
const DEFAULT_LEAF_MAX_BYTES: usize = DEFAULT_LEAF_TARGET_BYTES * 2;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ReactiveRope {
|
||||
inner: Rc<RopeInner>,
|
||||
}
|
||||
|
||||
pub struct RopeBuilder {
|
||||
reactor: Reactor,
|
||||
leaf_target_bytes: usize,
|
||||
chunks: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RopeChunk {
|
||||
text: Rc<str>,
|
||||
pub start: TextPoint,
|
||||
pub metrics: TextMetrics,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RopeCursor {
|
||||
rope: ReactiveRope,
|
||||
next_offset: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct RopeSnapshotMetrics {
|
||||
pub leaves: usize,
|
||||
pub text: TextMetrics,
|
||||
pub revision: u64,
|
||||
}
|
||||
|
||||
struct RopeInner {
|
||||
reactor: Reactor,
|
||||
structure: Source,
|
||||
revision: Cell<u64>,
|
||||
leaf_target_bytes: usize,
|
||||
leaves: RefCell<Vec<Rc<Leaf>>>,
|
||||
edit_events: Event<EditDelta>,
|
||||
}
|
||||
|
||||
struct Leaf {
|
||||
source: Source,
|
||||
text: RefCell<Rc<str>>,
|
||||
metrics: Cell<TextMetrics>,
|
||||
tail_chars: Cell<usize>,
|
||||
}
|
||||
|
||||
impl RopeBuilder {
|
||||
pub fn new(reactor: &Reactor) -> Self {
|
||||
Self {
|
||||
reactor: reactor.clone(),
|
||||
leaf_target_bytes: DEFAULT_LEAF_TARGET_BYTES,
|
||||
chunks: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn leaf_target_bytes(mut self, bytes: usize) -> Self {
|
||||
self.leaf_target_bytes = bytes.max(32);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn push(mut self, text: impl Into<String>) -> Self {
|
||||
self.chunks.push(text.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> ReactiveRope {
|
||||
ReactiveRope::from_chunks(self.reactor, self.leaf_target_bytes, self.chunks)
|
||||
}
|
||||
}
|
||||
|
||||
impl ReactiveRope {
|
||||
pub fn new(reactor: &Reactor) -> Self {
|
||||
RopeBuilder::new(reactor).build()
|
||||
}
|
||||
|
||||
pub fn from_text(reactor: &Reactor, text: impl AsRef<str>) -> Self {
|
||||
RopeBuilder::new(reactor).push(text.as_ref()).build()
|
||||
}
|
||||
|
||||
fn from_chunks(reactor: Reactor, leaf_target_bytes: usize, chunks: Vec<String>) -> Self {
|
||||
let structure = source_in(&reactor);
|
||||
let edit_events = event_in(&reactor);
|
||||
let mut leaves = Vec::new();
|
||||
for chunk in chunks {
|
||||
Self::append_chunk_into(&reactor, leaf_target_bytes, &mut leaves, &chunk);
|
||||
}
|
||||
if leaves.is_empty() {
|
||||
leaves.push(Rc::new(Leaf::new(&reactor, String::new())));
|
||||
}
|
||||
|
||||
Self {
|
||||
inner: Rc::new(RopeInner {
|
||||
reactor,
|
||||
structure,
|
||||
revision: Cell::new(0),
|
||||
leaf_target_bytes,
|
||||
leaves: RefCell::new(leaves),
|
||||
edit_events,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn edit_events(&self) -> Event<EditDelta> {
|
||||
self.inner.edit_events.clone()
|
||||
}
|
||||
|
||||
pub fn reactor(&self) -> Reactor {
|
||||
self.inner.reactor.clone()
|
||||
}
|
||||
|
||||
pub fn snapshot_metrics(&self) -> RopeSnapshotMetrics {
|
||||
self.inner.structure.observe();
|
||||
RopeSnapshotMetrics {
|
||||
leaves: self.inner.leaves.borrow().len(),
|
||||
text: self.total_metrics(),
|
||||
revision: self.inner.revision.get(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len_bytes(&self) -> usize {
|
||||
self.total_metrics().bytes
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len_bytes() == 0
|
||||
}
|
||||
|
||||
pub fn to_string(&self) -> String {
|
||||
self.slice(TextRange::new(0, self.len_bytes()))
|
||||
}
|
||||
|
||||
pub fn point_at(&self, offset: usize) -> TextPoint {
|
||||
self.inner.structure.observe();
|
||||
let offset = offset.min(self.len_bytes());
|
||||
let mut point = TextPoint::origin();
|
||||
let leaves = self.inner.leaves.borrow();
|
||||
let mut remaining = offset;
|
||||
for leaf in &*leaves {
|
||||
let bytes = leaf.metrics.get().bytes;
|
||||
if remaining > bytes {
|
||||
remaining -= bytes;
|
||||
point.byte += bytes;
|
||||
point.char_offset += leaf.metrics.get().chars;
|
||||
point.utf16_offset += leaf.metrics.get().utf16;
|
||||
point.line += leaf.metrics.get().line_breaks;
|
||||
if leaf.metrics.get().line_breaks == 0 {
|
||||
point.column += leaf.tail_chars.get();
|
||||
} else {
|
||||
point.column = leaf.tail_chars.get();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let text = leaf.read();
|
||||
point = point.advance(&text[..remaining]);
|
||||
return point;
|
||||
}
|
||||
point
|
||||
}
|
||||
|
||||
pub fn slice(&self, range: TextRange) -> String {
|
||||
self.inner.structure.observe();
|
||||
self.assert_char_boundary(range.start);
|
||||
self.assert_char_boundary(range.end);
|
||||
assert!(range.start <= range.end, "invalid text range");
|
||||
assert!(range.end <= self.len_bytes(), "range out of bounds");
|
||||
|
||||
let mut out = String::with_capacity(range.len());
|
||||
let mut cursor = 0usize;
|
||||
let leaves = self.inner.leaves.borrow();
|
||||
for leaf in &*leaves {
|
||||
let metrics = leaf.metrics.get();
|
||||
let end = cursor + metrics.bytes;
|
||||
if end <= range.start {
|
||||
cursor = end;
|
||||
continue;
|
||||
}
|
||||
if cursor >= range.end {
|
||||
break;
|
||||
}
|
||||
let text = leaf.read();
|
||||
let start_in_leaf = range.start.saturating_sub(cursor);
|
||||
let end_in_leaf = min(metrics.bytes, range.end - cursor);
|
||||
out.push_str(&text[start_in_leaf..end_in_leaf]);
|
||||
cursor = end;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn chunk_at(&self, offset: usize) -> Option<RopeChunk> {
|
||||
self.inner.structure.observe();
|
||||
if offset >= self.len_bytes() {
|
||||
return None;
|
||||
}
|
||||
let mut start = TextPoint::origin();
|
||||
let leaves = self.inner.leaves.borrow();
|
||||
let mut cursor = 0usize;
|
||||
for leaf in &*leaves {
|
||||
let metrics = leaf.metrics.get();
|
||||
let end = cursor + metrics.bytes;
|
||||
if offset < end {
|
||||
let text = leaf.read();
|
||||
leaf.source.observe();
|
||||
return Some(RopeChunk {
|
||||
text,
|
||||
start,
|
||||
metrics,
|
||||
});
|
||||
}
|
||||
start.byte += metrics.bytes;
|
||||
start.char_offset += metrics.chars;
|
||||
start.utf16_offset += metrics.utf16;
|
||||
start.line += metrics.line_breaks;
|
||||
if metrics.line_breaks == 0 {
|
||||
start.column += leaf.tail_chars.get();
|
||||
} else {
|
||||
start.column = leaf.tail_chars.get();
|
||||
}
|
||||
cursor = end;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn cursor(&self, offset: usize) -> RopeCursor {
|
||||
RopeCursor {
|
||||
rope: self.clone(),
|
||||
next_offset: offset.min(self.len_bytes()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn edit(&self, edit: RopeEdit) -> EditDelta {
|
||||
let delta = self.apply_edit(edit);
|
||||
self.inner.edit_events.emit(delta.clone());
|
||||
delta
|
||||
}
|
||||
|
||||
pub fn edit_batch(&self, edits: impl IntoIterator<Item = RopeEdit>) -> Vec<EditDelta> {
|
||||
let mut deltas = Vec::new();
|
||||
for edit in edits {
|
||||
let delta = self.apply_edit(edit);
|
||||
self.inner.edit_events.emit(delta.clone());
|
||||
deltas.push(delta);
|
||||
}
|
||||
deltas
|
||||
}
|
||||
|
||||
fn apply_edit(&self, edit: RopeEdit) -> EditDelta {
|
||||
assert!(edit.range.start <= edit.range.end, "invalid edit range");
|
||||
assert!(edit.range.end <= self.len_bytes(), "edit range out of bounds");
|
||||
self.assert_char_boundary(edit.range.start);
|
||||
self.assert_char_boundary(edit.range.end);
|
||||
assert!(
|
||||
edit.insert.is_char_boundary(edit.insert.len()),
|
||||
"replacement text must be valid UTF-8"
|
||||
);
|
||||
|
||||
let old_start = self.point_at(edit.range.start);
|
||||
let old_end = self.point_at(edit.range.end);
|
||||
let removed_text = self.slice(edit.range);
|
||||
let removed = TextMetrics::from_text(&removed_text);
|
||||
let inserted = TextMetrics::from_text(&edit.insert);
|
||||
|
||||
let mut leaves = self.inner.leaves.borrow_mut();
|
||||
let mut cursor = 0usize;
|
||||
let mut first_index = None;
|
||||
let mut last_index = None;
|
||||
let mut prefix = String::new();
|
||||
let mut suffix = String::new();
|
||||
|
||||
for (index, leaf) in leaves.iter().enumerate() {
|
||||
let text = leaf.read();
|
||||
let end = cursor + text.len();
|
||||
if first_index.is_none() && edit.range.start <= end {
|
||||
first_index = Some(index);
|
||||
let split = edit.range.start.saturating_sub(cursor);
|
||||
prefix.push_str(&text[..split]);
|
||||
}
|
||||
if first_index.is_some() && edit.range.end <= end {
|
||||
last_index = Some(index);
|
||||
let split = edit.range.end.saturating_sub(cursor);
|
||||
suffix.push_str(&text[split..]);
|
||||
break;
|
||||
}
|
||||
cursor = end;
|
||||
}
|
||||
|
||||
let first_index = first_index.unwrap_or_else(|| leaves.len().saturating_sub(1));
|
||||
let last_index = last_index.unwrap_or(first_index);
|
||||
|
||||
let mut replacement = prefix;
|
||||
replacement.push_str(&edit.insert);
|
||||
replacement.push_str(&suffix);
|
||||
|
||||
let structure_changed = last_index > first_index || replacement.len() > DEFAULT_LEAF_MAX_BYTES;
|
||||
if !structure_changed {
|
||||
if let Some(leaf) = leaves.get(first_index) {
|
||||
leaf.set_text(replacement);
|
||||
leaf.source.trigger();
|
||||
}
|
||||
} else {
|
||||
let mut replacement_leaves = Vec::new();
|
||||
Self::append_chunk_into(
|
||||
&self.inner.reactor,
|
||||
self.inner.leaf_target_bytes,
|
||||
&mut replacement_leaves,
|
||||
&replacement,
|
||||
);
|
||||
if replacement_leaves.is_empty() {
|
||||
replacement_leaves.push(Rc::new(Leaf::new(&self.inner.reactor, String::new())));
|
||||
}
|
||||
|
||||
let removed_span = leaves.splice(first_index..=last_index, replacement_leaves);
|
||||
let mut touched = BTreeSet::new();
|
||||
for leaf in removed_span {
|
||||
touched.insert(leaf.source.id());
|
||||
}
|
||||
for leaf in leaves.iter().skip(first_index) {
|
||||
touched.insert(leaf.source.id());
|
||||
}
|
||||
for leaf in &*leaves {
|
||||
if touched.contains(&leaf.source.id()) {
|
||||
leaf.source.trigger();
|
||||
}
|
||||
}
|
||||
self.inner.structure.trigger();
|
||||
}
|
||||
|
||||
self.inner.revision.set(self.inner.revision.get().wrapping_add(1));
|
||||
|
||||
let new_end = old_start.advance(&edit.insert);
|
||||
drop(leaves);
|
||||
|
||||
EditDelta {
|
||||
range: edit.range,
|
||||
removed_text,
|
||||
inserted_text: edit.insert,
|
||||
removed,
|
||||
inserted,
|
||||
start: old_start,
|
||||
old_end,
|
||||
new_end,
|
||||
}
|
||||
}
|
||||
|
||||
fn total_metrics(&self) -> TextMetrics {
|
||||
self.inner
|
||||
.leaves
|
||||
.borrow()
|
||||
.iter()
|
||||
.fold(TextMetrics::default(), |sum, leaf| sum + leaf.metrics.get())
|
||||
}
|
||||
|
||||
fn append_chunk_into(
|
||||
reactor: &Reactor,
|
||||
leaf_target_bytes: usize,
|
||||
out: &mut Vec<Rc<Leaf>>,
|
||||
text: &str,
|
||||
) {
|
||||
if text.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let max_bytes = (leaf_target_bytes.max(32)) * 2;
|
||||
let mut start = 0usize;
|
||||
while start < text.len() {
|
||||
let mut end = min(text.len(), start + max_bytes);
|
||||
while end > start && !text.is_char_boundary(end) {
|
||||
end -= 1;
|
||||
}
|
||||
if end == start {
|
||||
end = text.len();
|
||||
}
|
||||
out.push(Rc::new(Leaf::new(reactor, text[start..end].to_string())));
|
||||
start = end;
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_char_boundary(&self, offset: usize) {
|
||||
if offset == self.len_bytes() {
|
||||
return;
|
||||
}
|
||||
let leaves = self.inner.leaves.borrow();
|
||||
let mut cursor = 0usize;
|
||||
for leaf in &*leaves {
|
||||
let bytes = leaf.metrics.get().bytes;
|
||||
let end = cursor + bytes;
|
||||
if offset < end {
|
||||
let text = leaf.read();
|
||||
assert!(text.is_char_boundary(offset - cursor), "offset must be a char boundary");
|
||||
return;
|
||||
}
|
||||
cursor = end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RopeChunk {
|
||||
pub fn text(&self) -> &str {
|
||||
&self.text
|
||||
}
|
||||
}
|
||||
|
||||
impl RopeCursor {
|
||||
pub fn offset(&self) -> usize {
|
||||
self.next_offset
|
||||
}
|
||||
|
||||
pub fn peek_chunk(&self) -> Option<RopeChunk> {
|
||||
let chunk = self.rope.chunk_at(self.next_offset)?;
|
||||
let local = self.next_offset.saturating_sub(chunk.start.byte);
|
||||
if local == 0 {
|
||||
return Some(chunk);
|
||||
}
|
||||
|
||||
let partial = &chunk.text()[local..];
|
||||
Some(RopeChunk {
|
||||
text: Rc::from(partial),
|
||||
start: self.rope.point_at(self.next_offset),
|
||||
metrics: TextMetrics::from_text(partial),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn next_chunk(&mut self) -> Option<RopeChunk> {
|
||||
let chunk = self.peek_chunk()?;
|
||||
let end = chunk.start.byte + chunk.metrics.bytes;
|
||||
self.next_offset = end;
|
||||
Some(chunk)
|
||||
}
|
||||
|
||||
pub fn read_bytes(&mut self, len: usize) -> String {
|
||||
let start = self.next_offset;
|
||||
let end = min(self.rope.len_bytes(), start + len);
|
||||
let slice = self.rope.slice(TextRange::new(start, end));
|
||||
self.next_offset = end;
|
||||
slice
|
||||
}
|
||||
}
|
||||
|
||||
impl Leaf {
|
||||
fn new(reactor: &Reactor, text: String) -> Self {
|
||||
let text: Rc<str> = Rc::from(text);
|
||||
let metrics = TextMetrics::from_text(&text);
|
||||
let tail_chars = tail_chars(&text);
|
||||
Self {
|
||||
source: source_in(reactor),
|
||||
text: RefCell::new(text),
|
||||
metrics: Cell::new(metrics),
|
||||
tail_chars: Cell::new(tail_chars),
|
||||
}
|
||||
}
|
||||
|
||||
fn read(&self) -> Rc<str> {
|
||||
self.source.observe();
|
||||
self.text.borrow().clone()
|
||||
}
|
||||
|
||||
fn set_text(&self, text: String) {
|
||||
let text: Rc<str> = Rc::from(text);
|
||||
self.metrics.set(TextMetrics::from_text(&text));
|
||||
self.tail_chars.set(tail_chars(&text));
|
||||
*self.text.borrow_mut() = text;
|
||||
}
|
||||
}
|
||||
|
||||
fn tail_chars(text: &str) -> usize {
|
||||
match text.rfind('\n') {
|
||||
Some(index) => text[index + 1..].chars().count(),
|
||||
None => text.chars().count(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::cell::Cell as Counter;
|
||||
use std::cell::RefCell;
|
||||
use std::panic::{AssertUnwindSafe, catch_unwind};
|
||||
use std::rc::Rc;
|
||||
|
||||
use ruin_reactivity::{Reactor, thunk_in};
|
||||
|
||||
use crate::{Anchor, AnchorBias, ReactiveRope, RopeBuilder, RopeEdit, TextRange};
|
||||
|
||||
#[test]
|
||||
fn edits_preserve_text_and_metrics() {
|
||||
let reactor = Reactor::new();
|
||||
let rope = ReactiveRope::from_text(&reactor, "hello\nworld");
|
||||
|
||||
let delta = rope.edit(RopeEdit::replace(TextRange::new(5, 6), ", "));
|
||||
|
||||
assert_eq!(rope.to_string(), "hello, world");
|
||||
assert_eq!(delta.removed_text, "\n");
|
||||
assert_eq!(delta.inserted_text, ", ");
|
||||
assert_eq!(rope.snapshot_metrics().text.bytes, "hello, world".len());
|
||||
assert_eq!(rope.snapshot_metrics().text.line_breaks, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cursor_walks_tokens_across_leaf_boundaries() {
|
||||
let reactor = Reactor::new();
|
||||
let rope = RopeBuilder::new(&reactor)
|
||||
.leaf_target_bytes(4)
|
||||
.push("alpha")
|
||||
.push("beta")
|
||||
.build();
|
||||
let mut cursor = rope.cursor(3);
|
||||
|
||||
assert_eq!(cursor.read_bytes(4), "habe");
|
||||
let rest = cursor.next_chunk().expect("remaining chunk");
|
||||
assert_eq!(rest.text(), "ta");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anchors_map_through_insertions() {
|
||||
let reactor = Reactor::new();
|
||||
let rope = ReactiveRope::from_text(&reactor, "abcdef");
|
||||
let mut left = Anchor::new(3, AnchorBias::Left);
|
||||
let mut right = Anchor::new(3, AnchorBias::Right);
|
||||
|
||||
let delta = rope.edit(RopeEdit::insert(3, "XYZ"));
|
||||
left.map_through(&delta);
|
||||
right.map_through(&delta);
|
||||
|
||||
assert_eq!(left.byte, 3);
|
||||
assert_eq!(right.byte, 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disjoint_leaf_reads_do_not_recompute_on_local_edit() {
|
||||
let reactor = Reactor::new();
|
||||
let rope = RopeBuilder::new(&reactor)
|
||||
.leaf_target_bytes(64)
|
||||
.push("a".repeat(80))
|
||||
.push("b".repeat(80))
|
||||
.build();
|
||||
|
||||
let first_runs = Rc::new(Counter::new(0usize));
|
||||
let second_runs = Rc::new(Counter::new(0usize));
|
||||
|
||||
let first = thunk_in(&reactor, {
|
||||
let rope = rope.clone();
|
||||
let first_runs = Rc::clone(&first_runs);
|
||||
move || {
|
||||
first_runs.set(first_runs.get() + 1);
|
||||
rope.slice(TextRange::new(0, 10))
|
||||
}
|
||||
});
|
||||
|
||||
let second = thunk_in(&reactor, {
|
||||
let rope = rope.clone();
|
||||
let second_runs = Rc::clone(&second_runs);
|
||||
move || {
|
||||
second_runs.set(second_runs.get() + 1);
|
||||
rope.slice(TextRange::new(90, 100))
|
||||
}
|
||||
});
|
||||
|
||||
assert_eq!(first.get(), "aaaaaaaaaa");
|
||||
assert_eq!(second.get(), "bbbbbbbbbb");
|
||||
rope.edit(RopeEdit::replace(TextRange::new(2, 4), "zz"));
|
||||
|
||||
assert_eq!(first.get(), "aazzaaaaaa");
|
||||
assert_eq!(second.get(), "bbbbbbbbbb");
|
||||
assert_eq!(first_runs.get(), 2);
|
||||
assert_eq!(second_runs.get(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn point_at_tracks_multibyte_offsets() {
|
||||
let reactor = Reactor::new();
|
||||
let rope = ReactiveRope::from_text(&reactor, "a\né🙂");
|
||||
|
||||
assert_eq!(rope.point_at(0).byte, 0);
|
||||
assert_eq!(rope.point_at(2).line, 1);
|
||||
assert_eq!(rope.point_at(4).char_offset, 3);
|
||||
assert_eq!(rope.point_at("a\né🙂".len()).utf16_offset, 5);
|
||||
assert_eq!(rope.point_at("a\né".len()).column, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn point_at_tracks_column_across_leaves() {
|
||||
let reactor = Reactor::new();
|
||||
let rope = RopeBuilder::new(&reactor)
|
||||
.leaf_target_bytes(4)
|
||||
.push("ab\n")
|
||||
.push("cde")
|
||||
.build();
|
||||
|
||||
let point = rope.point_at("ab\ncd".len());
|
||||
assert_eq!(point.line, 1);
|
||||
assert_eq!(point.column, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chunk_at_and_peek_chunk_respect_boundaries() {
|
||||
let reactor = Reactor::new();
|
||||
let rope = RopeBuilder::new(&reactor)
|
||||
.leaf_target_bytes(4)
|
||||
.push("abcd")
|
||||
.push("efgh")
|
||||
.build();
|
||||
|
||||
let first = rope.chunk_at(0).expect("chunk at start");
|
||||
assert_eq!(first.text(), "abcd");
|
||||
|
||||
let second = rope.chunk_at(4).expect("chunk at split");
|
||||
assert_eq!(second.text(), "efgh");
|
||||
assert!(rope.chunk_at(8).is_none());
|
||||
|
||||
let mut cursor = rope.cursor(2);
|
||||
let partial = cursor.peek_chunk().expect("partial chunk");
|
||||
assert_eq!(partial.text(), "cd");
|
||||
let advanced = cursor.next_chunk().expect("advanced chunk");
|
||||
assert_eq!(advanced.text(), "cd");
|
||||
assert_eq!(cursor.offset(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cursor_stops_at_end_of_document() {
|
||||
let reactor = Reactor::new();
|
||||
let rope = RopeBuilder::new(&reactor)
|
||||
.leaf_target_bytes(4)
|
||||
.push("abcd")
|
||||
.push("ef")
|
||||
.build();
|
||||
let mut cursor = rope.cursor(0);
|
||||
let mut chunks = Vec::new();
|
||||
while let Some(chunk) = cursor.next_chunk() {
|
||||
chunks.push(chunk.text().to_string());
|
||||
}
|
||||
|
||||
assert_eq!(chunks, vec!["abcd".to_string(), "ef".to_string()]);
|
||||
assert!(cursor.next_chunk().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_edits_return_deltas_and_increment_revision() {
|
||||
let reactor = Reactor::new();
|
||||
let rope = ReactiveRope::from_text(&reactor, "abcdef");
|
||||
let before = rope.snapshot_metrics().revision;
|
||||
|
||||
let deltas = rope.edit_batch([
|
||||
RopeEdit::replace(TextRange::new(1, 3), "ZZ"),
|
||||
RopeEdit::delete(TextRange::new(4, 6)),
|
||||
]);
|
||||
|
||||
assert_eq!(deltas.len(), 2);
|
||||
assert_eq!(rope.to_string(), "aZZd");
|
||||
assert_eq!(rope.snapshot_metrics().revision, before + 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_to_empty_preserves_valid_empty_rope() {
|
||||
let reactor = Reactor::new();
|
||||
let rope = ReactiveRope::from_text(&reactor, "hello");
|
||||
|
||||
let delta = rope.edit(RopeEdit::delete(TextRange::new(0, 5)));
|
||||
|
||||
assert_eq!(delta.removed_text, "hello");
|
||||
assert!(rope.is_empty());
|
||||
assert_eq!(rope.to_string(), "");
|
||||
assert_eq!(rope.snapshot_metrics().leaves, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn structure_changing_edit_updates_leaf_count() {
|
||||
let reactor = Reactor::new();
|
||||
let rope = RopeBuilder::new(&reactor)
|
||||
.leaf_target_bytes(8)
|
||||
.push("abcdefgh")
|
||||
.push("ijklmnop")
|
||||
.build();
|
||||
let before = rope.snapshot_metrics();
|
||||
|
||||
rope.edit(RopeEdit::insert(8, "QRSTUVWXYZ0123456789"));
|
||||
let after = rope.snapshot_metrics();
|
||||
|
||||
assert!(after.leaves >= before.leaves);
|
||||
assert!(after.text.bytes > before.text.bytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_char_boundary_panics() {
|
||||
let reactor = Reactor::new();
|
||||
let rope = ReactiveRope::from_text(&reactor, "é");
|
||||
|
||||
let panic = catch_unwind(AssertUnwindSafe(|| {
|
||||
let _ = rope.slice(TextRange::new(1, 2));
|
||||
}));
|
||||
|
||||
assert!(panic.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_events_emit_immediately() {
|
||||
let reactor = Reactor::new();
|
||||
let rope = ReactiveRope::from_text(&reactor, "abc");
|
||||
let seen = Rc::new(RefCell::new(Vec::new()));
|
||||
let _sub = rope.edit_events().subscribe({
|
||||
let seen = Rc::clone(&seen);
|
||||
move |delta| seen.borrow_mut().push((delta.range, delta.inserted_text.clone()))
|
||||
});
|
||||
|
||||
rope.edit(RopeEdit::insert(1, "Z"));
|
||||
|
||||
assert_eq!(&*seen.borrow(), &[(TextRange::new(1, 1), "Z".to_string())]);
|
||||
}
|
||||
}
|
||||
2050
lib/reactivity_parsing/src/tokentree.rs
Normal file
2050
lib/reactivity_parsing/src/tokentree.rs
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user