786 lines
24 KiB
Rust
786 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 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>()
|
|
}
|