Lots of owork on a calculator example, transparency, etc.
This commit is contained in:
@@ -5,7 +5,12 @@ edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
ruin_runtime = { package = "ruin-runtime", path = "../runtime" }
|
||||
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
||||
tracing = { version = "0.1", default-features = false }
|
||||
hashbrown = "0.17"
|
||||
|
||||
[dev-dependencies]
|
||||
tracing-subscriber = { version = "0.3", default-features = false, features = ["env-filter", "fmt", "std"] }
|
||||
tracing-subscriber = { version = "0.3", default-features = false, features = [
|
||||
"env-filter",
|
||||
"fmt",
|
||||
"std",
|
||||
] }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use alloc::rc::Rc;
|
||||
use core::cell::RefCell;
|
||||
|
||||
use crate::{NodeId, Reactor, current, trace_targets};
|
||||
|
||||
@@ -114,7 +114,7 @@ impl<T: PartialEq + 'static> Cell<T> {
|
||||
return None;
|
||||
}
|
||||
|
||||
let previous = std::mem::replace(&mut *current, value);
|
||||
let previous = core::mem::replace(&mut *current, value);
|
||||
drop(current);
|
||||
tracing::debug!(
|
||||
target: trace_targets::CELL,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::rc::{Rc, Weak};
|
||||
use alloc::boxed::Box;
|
||||
use alloc::rc::{Rc, Weak};
|
||||
use core::cell::{Cell, RefCell};
|
||||
|
||||
use crate::reactor::ObserverHook;
|
||||
use crate::{NodeId, Reactor, current, trace_targets};
|
||||
@@ -64,7 +65,7 @@ impl EffectHandle {
|
||||
/// recover an EffectHandle after calling this method, so be sure to call it on an EffectHandle that you don't need
|
||||
/// to later dispose of.
|
||||
pub fn leak(self) {
|
||||
std::mem::forget(self);
|
||||
core::mem::forget(self);
|
||||
}
|
||||
|
||||
/// Disposes the effect immediately.
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
use alloc::boxed::Box;
|
||||
use alloc::rc::Rc;
|
||||
use alloc::vec::Vec;
|
||||
use core::cell::{Cell, RefCell};
|
||||
|
||||
use hashbrown::HashMap;
|
||||
|
||||
use crate::{NodeId, Reactor, current, trace_targets};
|
||||
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
//! single-threaded and designed to live on a runtime-managed thread, while async work feeds it
|
||||
//! from the edges by updating state or emitting events.
|
||||
|
||||
#![feature(thread_local)]
|
||||
#![cfg_attr(not(test), no_std)]
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
pub(crate) mod trace_targets {
|
||||
pub const GRAPH: &str = "ruin_reactivity::graph";
|
||||
pub const CELL: &str = "ruin_reactivity::cell";
|
||||
@@ -18,6 +23,7 @@ mod effect;
|
||||
mod event;
|
||||
mod id;
|
||||
mod reactor;
|
||||
mod source;
|
||||
mod thunk;
|
||||
|
||||
pub use cell::{Cell, cell, cell_in};
|
||||
@@ -25,4 +31,5 @@ pub use effect::{EffectHandle, effect, effect_in};
|
||||
pub use event::{Event, Subscription, event, event_in, on, on_in};
|
||||
pub use id::NodeId;
|
||||
pub use reactor::{ReactCycleError, Reactor, current};
|
||||
pub use source::{Source, source, source_in};
|
||||
pub use thunk::{Memo, Thunk, memo, memo_by, memo_by_in, memo_in, thunk, thunk_in};
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::collections::{BTreeMap, BTreeSet, VecDeque};
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::panic::panic_any;
|
||||
use std::rc::{Rc, Weak};
|
||||
use alloc::boxed::Box;
|
||||
use alloc::collections::VecDeque;
|
||||
use alloc::rc::{Rc, Weak};
|
||||
use alloc::vec::Vec;
|
||||
use core::cell::{Cell, RefCell};
|
||||
use core::error::Error;
|
||||
use core::fmt;
|
||||
|
||||
use hashbrown::{HashMap, HashSet};
|
||||
|
||||
use ruin_runtime::queue_microtask;
|
||||
|
||||
@@ -11,9 +14,8 @@ use crate::{NodeId, trace_targets};
|
||||
|
||||
type Job = Box<dyn FnOnce() + 'static>;
|
||||
|
||||
thread_local! {
|
||||
static CURRENT_REACTOR: RefCell<Weak<ReactorInner>> = const { RefCell::new(Weak::new()) };
|
||||
}
|
||||
#[thread_local]
|
||||
static CURRENT_REACTOR: RefCell<Weak<ReactorInner>> = const { RefCell::new(Weak::new()) };
|
||||
|
||||
/// Returns the current thread's default reactor.
|
||||
///
|
||||
@@ -28,7 +30,7 @@ pub(crate) trait ObserverHook {
|
||||
fn notify(&self);
|
||||
}
|
||||
|
||||
/// Panic payload emitted when the reactor detects a dependency cycle.
|
||||
/// Error type for cycles detected in the reactive graph. Contains the path of nodes that form the cycle.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ReactCycleError {
|
||||
cycle: Vec<NodeId>,
|
||||
@@ -85,26 +87,24 @@ impl Reactor {
|
||||
|
||||
/// Returns the current thread's default reactor.
|
||||
pub fn current() -> Self {
|
||||
CURRENT_REACTOR.with(|slot| {
|
||||
if let Some(inner) = slot.borrow().upgrade() {
|
||||
#[cfg(debug_assertions)]
|
||||
tracing::trace!(
|
||||
target: trace_targets::GRAPH,
|
||||
event = "current_reactor_reuse",
|
||||
"reusing current thread default reactor"
|
||||
);
|
||||
return Self { inner };
|
||||
}
|
||||
|
||||
let reactor = Self::new();
|
||||
*slot.borrow_mut() = Rc::downgrade(&reactor.inner);
|
||||
tracing::debug!(
|
||||
if let Some(inner) = CURRENT_REACTOR.borrow().upgrade() {
|
||||
#[cfg(debug_assertions)]
|
||||
tracing::trace!(
|
||||
target: trace_targets::GRAPH,
|
||||
event = "current_reactor_install",
|
||||
"installed current thread default reactor"
|
||||
event = "current_reactor_reuse",
|
||||
"reusing current thread default reactor"
|
||||
);
|
||||
reactor
|
||||
})
|
||||
return Self { inner };
|
||||
}
|
||||
|
||||
let reactor = Self::new();
|
||||
*CURRENT_REACTOR.borrow_mut() = Rc::downgrade(&reactor.inner);
|
||||
tracing::debug!(
|
||||
target: trace_targets::GRAPH,
|
||||
event = "current_reactor_install",
|
||||
"installed current thread default reactor"
|
||||
);
|
||||
reactor
|
||||
}
|
||||
|
||||
/// Runs `f` in the dependency-tracking scope of `observer`.
|
||||
@@ -143,13 +143,9 @@ impl Reactor {
|
||||
f()
|
||||
}
|
||||
|
||||
/// Records that the currently executing observer depends on `observable`.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics with [`ReactCycleError`] if `observable` is already being computed in the current
|
||||
/// dependency stack.
|
||||
pub fn observe(&self, observable: NodeId) {
|
||||
/// Attempts to record a dependency on `observable` for the currently running observer, returning an
|
||||
/// error if doing so would create a dependency cycle.
|
||||
pub fn try_observe(&self, observable: NodeId) -> Result<(), ReactCycleError> {
|
||||
if self
|
||||
.inner
|
||||
.active_computations
|
||||
@@ -170,12 +166,12 @@ impl Reactor {
|
||||
cycle_len = cycle.len(),
|
||||
"reactive cycle detected"
|
||||
);
|
||||
panic_any(ReactCycleError::new(cycle));
|
||||
return Err(ReactCycleError::new(cycle));
|
||||
}
|
||||
|
||||
let current = self.inner.stack.borrow().last().copied();
|
||||
let Some(observer) = current else {
|
||||
return;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -199,6 +195,18 @@ impl Reactor {
|
||||
.entry(observable)
|
||||
.or_default()
|
||||
.insert(observer);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Records a dependency on `observable` for the currently running observer.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if recording the dependency would create a cycle in the reactive graph.
|
||||
pub fn observe(&self, observable: NodeId) {
|
||||
if let Err(e) = self.try_observe(observable) {
|
||||
panic!("reactive cycle detected: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Notifies dependents of `observable`.
|
||||
@@ -344,11 +352,11 @@ impl fmt::Debug for Reactor {
|
||||
|
||||
struct ReactorInner {
|
||||
next_node: Cell<u64>,
|
||||
dependencies: RefCell<BTreeMap<NodeId, BTreeSet<NodeId>>>,
|
||||
dependents: RefCell<BTreeMap<NodeId, BTreeSet<NodeId>>>,
|
||||
observers: RefCell<BTreeMap<NodeId, Weak<dyn ObserverHook>>>,
|
||||
dependencies: RefCell<HashMap<NodeId, HashSet<NodeId>>>,
|
||||
dependents: RefCell<HashMap<NodeId, HashSet<NodeId>>>,
|
||||
observers: RefCell<HashMap<NodeId, Weak<dyn ObserverHook>>>,
|
||||
stack: RefCell<Vec<NodeId>>,
|
||||
active_computations: RefCell<BTreeSet<NodeId>>,
|
||||
active_computations: RefCell<HashSet<NodeId>>,
|
||||
pending_jobs: RefCell<VecDeque<Job>>,
|
||||
flush_scheduled: Cell<bool>,
|
||||
}
|
||||
@@ -357,11 +365,11 @@ impl ReactorInner {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
next_node: Cell::new(1),
|
||||
dependencies: RefCell::new(BTreeMap::new()),
|
||||
dependents: RefCell::new(BTreeMap::new()),
|
||||
observers: RefCell::new(BTreeMap::new()),
|
||||
dependencies: RefCell::new(HashMap::new()),
|
||||
dependents: RefCell::new(HashMap::new()),
|
||||
observers: RefCell::new(HashMap::new()),
|
||||
stack: RefCell::new(Vec::new()),
|
||||
active_computations: RefCell::new(BTreeSet::new()),
|
||||
active_computations: RefCell::new(HashSet::new()),
|
||||
pending_jobs: RefCell::new(VecDeque::new()),
|
||||
flush_scheduled: Cell::new(false),
|
||||
}
|
||||
@@ -421,7 +429,7 @@ mod tests {
|
||||
|
||||
use ruin_runtime::{queue_task, run};
|
||||
|
||||
use super::{ReactCycleError, Reactor, current};
|
||||
use super::{Reactor, current};
|
||||
|
||||
#[test]
|
||||
fn current_reactor_is_thread_local_singleton() {
|
||||
@@ -436,7 +444,11 @@ mod tests {
|
||||
let observer = reactor.allocate_node();
|
||||
let observable = reactor.allocate_node();
|
||||
|
||||
reactor.run_in_context(observer, || reactor.observe(observable));
|
||||
reactor.run_in_context(observer, || {
|
||||
reactor
|
||||
.try_observe(observable)
|
||||
.expect("should not detect cycle")
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
reactor.inner.dependencies.borrow().get(&observer),
|
||||
@@ -449,7 +461,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cycle_detection_panics_with_structured_error() {
|
||||
fn cycle_detection_panics_with_expected() {
|
||||
let reactor = Reactor::new();
|
||||
let a = reactor.allocate_node();
|
||||
let b = reactor.allocate_node();
|
||||
@@ -457,15 +469,26 @@ mod tests {
|
||||
let panic = catch_unwind(AssertUnwindSafe(|| {
|
||||
reactor.run_in_context(a, || {
|
||||
reactor.observe(b);
|
||||
reactor.run_in_context(b, || reactor.observe(a));
|
||||
reactor.run_in_context(b, || {
|
||||
reactor.observe(a);
|
||||
});
|
||||
});
|
||||
}))
|
||||
.expect_err("cycle should panic");
|
||||
|
||||
let error = panic
|
||||
.downcast::<ReactCycleError>()
|
||||
.expect("panic payload should be ReactCycleError");
|
||||
assert_eq!(error.cycle(), &[a, b, a]);
|
||||
let Some(cycle_error) = panic.downcast_ref::<String>() else {
|
||||
panic!("panic should be a string");
|
||||
};
|
||||
|
||||
assert!(
|
||||
cycle_error.contains("reactive cycle detected"),
|
||||
"panic should indicate cycle detected"
|
||||
);
|
||||
|
||||
assert!(
|
||||
cycle_error.contains("1 -> 2 -> 1"),
|
||||
"panic should include cycle path"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
70
lib/reactivity/src/source.rs
Normal file
70
lib/reactivity/src/source.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
use alloc::rc::Rc;
|
||||
|
||||
use crate::{NodeId, Reactor, current, trace_targets};
|
||||
|
||||
/// Creates a low-level reactive source node in the current reactor.
|
||||
pub fn source() -> Source {
|
||||
current().source()
|
||||
}
|
||||
|
||||
/// Creates a low-level reactive source node associated with `reactor`.
|
||||
pub fn source_in(reactor: &Reactor) -> Source {
|
||||
reactor.source()
|
||||
}
|
||||
|
||||
/// Low-level observable source node.
|
||||
///
|
||||
/// `Source` is useful for advanced data structures that want precise control over when reads
|
||||
/// observe and writes trigger invalidation without storing their state in a [`crate::Cell`].
|
||||
#[derive(Clone)]
|
||||
pub struct Source {
|
||||
inner: Rc<SourceInner>,
|
||||
}
|
||||
|
||||
impl Reactor {
|
||||
/// Creates a low-level source node associated with this reactor.
|
||||
pub fn source(&self) -> Source {
|
||||
Source::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Source {
|
||||
fn new(reactor: Reactor) -> Self {
|
||||
let id = reactor.allocate_node();
|
||||
tracing::debug!(
|
||||
target: trace_targets::GRAPH,
|
||||
event = "create_source",
|
||||
node_id = id.0,
|
||||
"created low-level reactive source"
|
||||
);
|
||||
Self {
|
||||
inner: Rc::new(SourceInner { reactor, id }),
|
||||
}
|
||||
}
|
||||
|
||||
/// Records a dependency on this source for the currently running observer.
|
||||
pub fn observe(&self) {
|
||||
self.inner.reactor.observe(self.inner.id);
|
||||
}
|
||||
|
||||
/// Triggers this source's dependents.
|
||||
pub fn trigger(&self) {
|
||||
self.inner.reactor.trigger(self.inner.id);
|
||||
}
|
||||
|
||||
/// Returns the source node id.
|
||||
pub fn id(&self) -> NodeId {
|
||||
self.inner.id
|
||||
}
|
||||
}
|
||||
|
||||
struct SourceInner {
|
||||
reactor: Reactor,
|
||||
id: NodeId,
|
||||
}
|
||||
|
||||
impl Drop for SourceInner {
|
||||
fn drop(&mut self) {
|
||||
self.reactor.dispose(self.id);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::rc::Rc;
|
||||
use alloc::boxed::Box;
|
||||
use alloc::rc::Rc;
|
||||
use core::cell::{Cell, RefCell};
|
||||
|
||||
use crate::reactor::ObserverHook;
|
||||
use crate::{NodeId, Reactor, current, trace_targets};
|
||||
@@ -428,9 +429,13 @@ mod tests {
|
||||
.expect_err("mutually recursive thunks should panic");
|
||||
|
||||
let error = panic
|
||||
.downcast::<crate::ReactCycleError>()
|
||||
.expect("panic payload should be ReactCycleError");
|
||||
assert_eq!(error.cycle().len(), 3);
|
||||
.downcast_ref::<String>()
|
||||
.expect("panic should be a string");
|
||||
|
||||
assert!(
|
||||
error.contains("reactive cycle detected"),
|
||||
"panic should indicate cycle detected"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
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
@@ -14,7 +14,9 @@ use ruin_ui::{
|
||||
use ruin_ui_platform_wayland::start_wayland_ui;
|
||||
|
||||
use crate::Result;
|
||||
use crate::context::{RenderState, render_with_context};
|
||||
use crate::context::{
|
||||
InteractionSnapshot, RenderState, render_with_context, render_with_context_and_interaction,
|
||||
};
|
||||
use crate::input::{
|
||||
EventBindings, InputState, ShortcutBinding, TextSelection, TextSelectionDrag,
|
||||
TextSelectionState, apply_text_selection_overlay, focused_element_for_pointer,
|
||||
@@ -58,6 +60,11 @@ impl Window {
|
||||
self.spec = self.spec.base_color(color);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn resizable(mut self, resizable: bool) -> Self {
|
||||
self.spec.resizable = resizable;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Window {
|
||||
@@ -160,7 +167,10 @@ impl<M: Mountable> MountedApp<M> {
|
||||
let shortcuts = Rc::new(RefCell::new(Vec::<ShortcutBinding>::new()));
|
||||
let current_title = Rc::new(RefCell::new(None::<String>));
|
||||
let last_min_size = Rc::new(RefCell::new(None::<UiSize>));
|
||||
let last_fixed_size = Rc::new(RefCell::new(None::<UiSize>));
|
||||
let explicit_min_size = app_window.spec.min_inner_size;
|
||||
let explicit_max_size = app_window.spec.max_inner_size;
|
||||
let window_resizable = app_window.spec.resizable;
|
||||
let mut input_state = InputState::new();
|
||||
let mut pointer_router = PointerRouter::new();
|
||||
|
||||
@@ -174,18 +184,31 @@ impl<M: Mountable> MountedApp<M> {
|
||||
let shortcuts = Rc::clone(&shortcuts);
|
||||
let current_title = Rc::clone(¤t_title);
|
||||
let last_min_size = Rc::clone(&last_min_size);
|
||||
let last_fixed_size = Rc::clone(&last_fixed_size);
|
||||
let root = Rc::clone(&root);
|
||||
let render_state = Rc::clone(&render_state);
|
||||
let text_selection = Rc::clone(&input_state.text_selection);
|
||||
let hovered_elements = input_state.hovered_elements.clone();
|
||||
let pressed_elements = input_state.pressed_elements.clone();
|
||||
let interaction_version = input_state.interaction_version.clone();
|
||||
move || {
|
||||
let viewport = viewport.get();
|
||||
let version = scene_version.get().wrapping_add(1);
|
||||
scene_version.set(version);
|
||||
let _ = text_selection.version.get();
|
||||
let _ = interaction_version.get();
|
||||
|
||||
let t_effect = Instant::now();
|
||||
let interaction_snapshot = Rc::new(InteractionSnapshot {
|
||||
hovered: hovered_elements.get(),
|
||||
pressed: pressed_elements.get(),
|
||||
});
|
||||
|
||||
let render_output = render_with_context(Rc::clone(&render_state), || root.render());
|
||||
let render_output = render_with_context_and_interaction(
|
||||
Rc::clone(&render_state),
|
||||
interaction_snapshot,
|
||||
|| root.render(),
|
||||
);
|
||||
|
||||
if render_output.side_effects.window_title != *current_title.borrow() {
|
||||
if let Some(title) = &render_output.side_effects.window_title {
|
||||
@@ -211,7 +234,6 @@ impl<M: Mountable> MountedApp<M> {
|
||||
);
|
||||
let layout_us = t_layout.elapsed().as_micros();
|
||||
|
||||
// Compute and send min-size hint: max of layout min and explicit window min.
|
||||
let desired_min = UiSize::new(
|
||||
root_min_size
|
||||
.width
|
||||
@@ -220,10 +242,31 @@ impl<M: Mountable> MountedApp<M> {
|
||||
.height
|
||||
.max(explicit_min_size.map_or(0.0, |s| s.height)),
|
||||
);
|
||||
let current_min = *last_min_size.borrow();
|
||||
if current_min != Some(desired_min) {
|
||||
let _ = window.update(WindowUpdate::new().min_inner_size(Some(desired_min)));
|
||||
*last_min_size.borrow_mut() = Some(desired_min);
|
||||
if window_resizable {
|
||||
let current_min = *last_min_size.borrow();
|
||||
if current_min != Some(desired_min) {
|
||||
let _ = window.update(WindowUpdate::new().min_inner_size(Some(desired_min)));
|
||||
*last_min_size.borrow_mut() = Some(desired_min);
|
||||
}
|
||||
} else {
|
||||
let desired_fixed = UiSize::new(
|
||||
desired_min
|
||||
.width
|
||||
.min(explicit_max_size.map_or(f32::INFINITY, |s| s.width)),
|
||||
desired_min
|
||||
.height
|
||||
.min(explicit_max_size.map_or(f32::INFINITY, |s| s.height)),
|
||||
);
|
||||
let current_fixed = *last_fixed_size.borrow();
|
||||
if current_fixed != Some(desired_fixed) {
|
||||
let _ = window.update(
|
||||
WindowUpdate::new()
|
||||
.requested_inner_size(Some(desired_fixed))
|
||||
.min_inner_size(Some(desired_fixed))
|
||||
.max_inner_size(Some(desired_fixed)),
|
||||
);
|
||||
*last_fixed_size.borrow_mut() = Some(desired_fixed);
|
||||
}
|
||||
}
|
||||
let effect_us = t_effect.elapsed().as_micros();
|
||||
|
||||
@@ -355,10 +398,15 @@ impl<M: Mountable> MountedApp<M> {
|
||||
.last()
|
||||
.map(|target| target.cursor)
|
||||
.unwrap_or(ruin_ui::CursorIcon::Default);
|
||||
let hovered_ids = Self::interaction_ids(pointer_router.hovered_targets());
|
||||
let pressed_ids = Self::interaction_ids(pointer_router.pressed_targets());
|
||||
if next_cursor != input_state.current_cursor {
|
||||
input_state.current_cursor = next_cursor;
|
||||
window.set_cursor_icon(next_cursor)?;
|
||||
}
|
||||
if Self::update_interaction_state(input_state, hovered_ids, pressed_ids) {
|
||||
input_state.interaction_version.update(|value| *value += 1);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -458,6 +506,32 @@ impl<M: Mountable> MountedApp<M> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn interaction_ids(targets: &[ruin_ui::HitTarget]) -> Vec<ruin_ui::ElementId> {
|
||||
targets.iter().filter_map(|target| target.element_id).collect()
|
||||
}
|
||||
|
||||
fn update_interaction_state(
|
||||
input_state: &mut InputState,
|
||||
hovered_ids: Vec<ruin_ui::ElementId>,
|
||||
pressed_ids: Vec<ruin_ui::ElementId>,
|
||||
) -> bool {
|
||||
let previous_hovered = input_state.hovered_elements.get();
|
||||
let previous_pressed = input_state.pressed_elements.get();
|
||||
let hovered_changed = input_state.hovered_elements.set(hovered_ids.clone()).is_some();
|
||||
let pressed_changed = input_state.pressed_elements.set(pressed_ids.clone()).is_some();
|
||||
if hovered_changed || pressed_changed {
|
||||
tracing::trace!(
|
||||
target: "ruin_app::interaction",
|
||||
hovered_before = ?previous_hovered,
|
||||
hovered_after = ?hovered_ids,
|
||||
pressed_before = ?previous_pressed,
|
||||
pressed_after = ?pressed_ids,
|
||||
"interaction state changed"
|
||||
);
|
||||
}
|
||||
hovered_changed || pressed_changed
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
|
||||
@@ -29,6 +29,12 @@ pub(crate) struct RenderState {
|
||||
pub(crate) next_element_id: StdCell<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub(crate) struct InteractionSnapshot {
|
||||
pub(crate) hovered: Vec<ElementId>,
|
||||
pub(crate) pressed: Vec<ElementId>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct RenderSideEffects {
|
||||
pub(crate) window_title: Option<String>,
|
||||
@@ -42,6 +48,7 @@ pub(crate) struct RenderContext {
|
||||
pub(crate) element_index: Rc<StdCell<usize>>,
|
||||
pub(crate) side_effects: Rc<RefCell<RenderSideEffects>>,
|
||||
pub(crate) context_entries: Rc<Vec<ContextEntry>>,
|
||||
pub(crate) interaction: Rc<InteractionSnapshot>,
|
||||
}
|
||||
|
||||
impl RenderContext {
|
||||
@@ -52,6 +59,7 @@ impl RenderContext {
|
||||
element_index: Rc::clone(&self.element_index),
|
||||
side_effects: Rc::clone(&self.side_effects),
|
||||
context_entries,
|
||||
interaction: Rc::clone(&self.interaction),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,6 +76,14 @@ thread_local! {
|
||||
pub(crate) fn render_with_context(
|
||||
state: Rc<RenderState>,
|
||||
render: impl FnOnce() -> View,
|
||||
) -> RenderOutput {
|
||||
render_with_context_and_interaction(state, Rc::new(InteractionSnapshot::default()), render)
|
||||
}
|
||||
|
||||
pub(crate) fn render_with_context_and_interaction(
|
||||
state: Rc<RenderState>,
|
||||
interaction: Rc<InteractionSnapshot>,
|
||||
render: impl FnOnce() -> View,
|
||||
) -> RenderOutput {
|
||||
let context = RenderContext {
|
||||
state,
|
||||
@@ -75,6 +91,7 @@ pub(crate) fn render_with_context(
|
||||
element_index: Rc::new(StdCell::new(0)),
|
||||
side_effects: Rc::new(RefCell::new(RenderSideEffects::default())),
|
||||
context_entries: Rc::new(Vec::new()),
|
||||
interaction,
|
||||
};
|
||||
|
||||
let view = with_render_context(context.clone(), render);
|
||||
|
||||
@@ -155,6 +155,11 @@ pub fn use_window_title(compute: impl FnOnce() -> String) {
|
||||
pub enum Key {
|
||||
Character(char),
|
||||
Enter,
|
||||
Backspace,
|
||||
Escape,
|
||||
Delete,
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
ArrowUp,
|
||||
ArrowDown,
|
||||
Home,
|
||||
@@ -169,6 +174,11 @@ impl Key {
|
||||
.next()
|
||||
.is_some_and(|actual| actual.eq_ignore_ascii_case(expected)),
|
||||
(Self::Enter, KeyboardKey::Enter) => true,
|
||||
(Self::Backspace, KeyboardKey::Backspace) => true,
|
||||
(Self::Escape, KeyboardKey::Escape) => true,
|
||||
(Self::Delete, KeyboardKey::Delete) => true,
|
||||
(Self::ArrowLeft, KeyboardKey::ArrowLeft) => true,
|
||||
(Self::ArrowRight, KeyboardKey::ArrowRight) => true,
|
||||
(Self::ArrowUp, KeyboardKey::ArrowUp) => true,
|
||||
(Self::ArrowDown, KeyboardKey::ArrowDown) => true,
|
||||
(Self::Home, KeyboardKey::Home) => true,
|
||||
@@ -209,12 +219,27 @@ impl Shortcut {
|
||||
}
|
||||
|
||||
pub(crate) fn matches(&self, event: &KeyboardEvent) -> bool {
|
||||
event.kind == KeyboardEventKind::Pressed
|
||||
&& self.key.matches(event)
|
||||
&& event.modifiers.control == self.control
|
||||
&& event.modifiers.shift == self.shift
|
||||
&& event.modifiers.alt == self.alt
|
||||
&& event.modifiers.super_key == self.super_key
|
||||
if event.kind != KeyboardEventKind::Pressed {
|
||||
return false;
|
||||
}
|
||||
if !self.key.matches(event) {
|
||||
return false;
|
||||
}
|
||||
if event.modifiers.control != self.control {
|
||||
return false;
|
||||
}
|
||||
if event.modifiers.alt != self.alt {
|
||||
return false;
|
||||
}
|
||||
if event.modifiers.super_key != self.super_key {
|
||||
return false;
|
||||
}
|
||||
// For character shortcuts the produced character already encodes the shift
|
||||
// state, so don't additionally require that Shift is (un)held.
|
||||
if !matches!(self.key, Key::Character(_)) && event.modifiers.shift != self.shift {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,6 +248,12 @@ pub struct FocusScope {
|
||||
pub(crate) element_id: Signal<Option<ElementId>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub struct InteractionState {
|
||||
pub hovered: bool,
|
||||
pub pressed: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ShortcutScope {
|
||||
Application,
|
||||
@@ -296,6 +327,18 @@ pub fn use_widget_ref<T: 'static>() -> WidgetRef<T> {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn use_interaction_state<T: 'static>(widget_ref: WidgetRef<T>) -> InteractionState {
|
||||
with_render_context_state(|context| {
|
||||
let Some(element_id) = widget_ref.element_id() else {
|
||||
return InteractionState::default();
|
||||
};
|
||||
InteractionState {
|
||||
hovered: context.interaction.hovered.contains(&element_id),
|
||||
pressed: context.interaction.pressed.contains(&element_id),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn use_shortcut(shortcut: Shortcut, scope: ShortcutScope, action: impl Fn() + 'static) {
|
||||
use_shortcut_with_context(shortcut, scope, move |_| action());
|
||||
}
|
||||
@@ -371,4 +414,3 @@ where
|
||||
|slot: &mut ResourceSlot<T, E>| slot.resource.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -124,6 +124,9 @@ pub(crate) struct InputState {
|
||||
pub(crate) current_cursor: CursorIcon,
|
||||
pub(crate) focused_element: Option<ElementId>,
|
||||
pub(crate) text_selection: Rc<TextSelectionState>,
|
||||
pub(crate) hovered_elements: Signal<Vec<ElementId>>,
|
||||
pub(crate) pressed_elements: Signal<Vec<ElementId>>,
|
||||
pub(crate) interaction_version: Signal<u64>,
|
||||
}
|
||||
|
||||
impl InputState {
|
||||
@@ -132,6 +135,9 @@ impl InputState {
|
||||
current_cursor: CursorIcon::Default,
|
||||
focused_element: None,
|
||||
text_selection: Rc::new(TextSelectionState::new()),
|
||||
hovered_elements: Signal::new(Vec::new()),
|
||||
pressed_elements: Signal::new(Vec::new()),
|
||||
interaction_version: Signal::new(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,9 +23,10 @@ pub mod view;
|
||||
pub use app::{App, Component, ContextKey, MountedApp, Mountable, Window, __render_mountable_for_test};
|
||||
pub use converters::{IntoBorder, IntoEdges, IntoShadow};
|
||||
pub use hooks::{
|
||||
BlockWidget, FocusScope, Key, Memo, Resource, ResourceState, ScrollBoxWidget, Shortcut,
|
||||
ShortcutScope, Signal, WidgetRef, provide, use_context, use_effect, use_memo, use_resource,
|
||||
use_shortcut, use_shortcut_with_context, use_signal, use_widget_ref, use_window_title,
|
||||
BlockWidget, FocusScope, InteractionState, Key, Memo, Resource, ResourceState,
|
||||
ScrollBoxWidget, Shortcut, ShortcutScope, Signal, WidgetRef, provide, use_context,
|
||||
use_effect, use_interaction_state, use_memo, use_resource, use_shortcut,
|
||||
use_shortcut_with_context, use_signal, use_widget_ref, use_window_title,
|
||||
};
|
||||
pub use primitives::{
|
||||
ButtonBuilder, ContainerBuilder, ContainerProps, FontWeight, ScrollBoxBuilder, TextBuilder,
|
||||
@@ -59,19 +60,20 @@ pub type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
||||
pub mod prelude {
|
||||
pub use crate::{
|
||||
App, BlockWidget, ButtonBuilder, ChildViews, Children, Component, ContainerBuilder,
|
||||
ContextKey, FocusScope, FontWeight, IntoBorder, IntoEdges, IntoView, Key, Memo, Mountable,
|
||||
Pending, Ready, Resource, ResourceState, Result, ScrollBoxBuilder, ScrollBoxWidget,
|
||||
Shortcut, ShortcutScope, Signal, TextBuilder, TextChildren, TextRole, TextValue, View,
|
||||
WidgetRef, Window, block, button, colors, column, component, context_provider, provide,
|
||||
row, scroll_box, surfaces, text, use_context, use_effect, use_memo, use_resource,
|
||||
use_shortcut, use_shortcut_with_context, use_signal, use_widget_ref, use_window_title,
|
||||
view,
|
||||
ContextKey, FocusScope, FontWeight, InteractionState, IntoBorder, IntoEdges, IntoView,
|
||||
Key, Memo, Mountable, Pending, Ready, Resource, ResourceState, Result, ScrollBoxBuilder,
|
||||
ScrollBoxWidget, Shortcut, ShortcutScope, Signal, TextBuilder, TextChildren, TextRole,
|
||||
TextValue, View, WidgetRef, Window, block, button, colors, column, component,
|
||||
context_provider, provide, row, scroll_box, surfaces, text, use_context, use_effect,
|
||||
use_interaction_state, use_memo, use_resource, use_shortcut, use_shortcut_with_context,
|
||||
use_signal, use_widget_ref, use_window_title, view,
|
||||
};
|
||||
pub use ruin_ui::{
|
||||
AlignItems, Border, Color, CursorIcon, Edges, Element, ElementId, InteractionTree,
|
||||
PointerButton, PointerEventKind, RoutedPointerEvent, RoutedPointerEventKind,
|
||||
ScrollbarStyle, TextFontFamily, TextStyle, TextWrap, UiSize,
|
||||
};
|
||||
pub use crate::PartialTextStyle;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -84,7 +86,9 @@ mod tests {
|
||||
use ruin_ui::{KeyboardModifiers, Point, WindowSpec};
|
||||
|
||||
use super::*;
|
||||
use crate::context::{RenderState, render_with_context};
|
||||
use crate::context::{
|
||||
InteractionSnapshot, RenderState, render_with_context, render_with_context_and_interaction,
|
||||
};
|
||||
use crate::input::{
|
||||
InputState, KeyHandler, focused_element_for_pointer, scroll_handler_for_event,
|
||||
};
|
||||
@@ -177,6 +181,53 @@ mod tests {
|
||||
assert!(debug.contains("body child"), "{debug}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interaction_state_tracks_hover_and_press_for_widget_refs() {
|
||||
let render_state = Rc::new(RenderState::default());
|
||||
let hovered = Rc::new(RefCell::new(None::<bool>));
|
||||
let pressed = Rc::new(RefCell::new(None::<bool>));
|
||||
let widget_ref_slot = Rc::new(RefCell::new(None::<WidgetRef<BlockWidget>>));
|
||||
|
||||
let _ = render_with_context(Rc::clone(&render_state), {
|
||||
let hovered = Rc::clone(&hovered);
|
||||
let pressed = Rc::clone(&pressed);
|
||||
let widget_ref_slot = Rc::clone(&widget_ref_slot);
|
||||
move || {
|
||||
let widget_ref = use_widget_ref::<BlockWidget>();
|
||||
*hovered.borrow_mut() = Some(use_interaction_state(widget_ref.clone()).hovered);
|
||||
*pressed.borrow_mut() = Some(use_interaction_state(widget_ref.clone()).pressed);
|
||||
*widget_ref_slot.borrow_mut() = Some(widget_ref.clone());
|
||||
block().widget_ref(widget_ref).children(())
|
||||
}
|
||||
});
|
||||
|
||||
let widget_ref = widget_ref_slot.borrow().clone().expect("widget ref should be set");
|
||||
let element_id = widget_ref
|
||||
.element_id()
|
||||
.expect("widget ref should have an element id after render");
|
||||
let _ = render_with_context_and_interaction(
|
||||
render_state,
|
||||
Rc::new(InteractionSnapshot {
|
||||
hovered: vec![element_id],
|
||||
pressed: vec![element_id],
|
||||
}),
|
||||
{
|
||||
let hovered = Rc::clone(&hovered);
|
||||
let pressed = Rc::clone(&pressed);
|
||||
let widget_ref_slot = Rc::clone(&widget_ref_slot);
|
||||
move || {
|
||||
let widget_ref = widget_ref_slot.borrow().clone().expect("widget ref should persist");
|
||||
*hovered.borrow_mut() = Some(use_interaction_state(widget_ref.clone()).hovered);
|
||||
*pressed.borrow_mut() = Some(use_interaction_state(widget_ref.clone()).pressed);
|
||||
block().widget_ref(widget_ref).children(())
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(*hovered.borrow(), Some(true));
|
||||
assert_eq!(*pressed.borrow(), Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_dispatch_prefers_the_nearest_focused_ancestor_handler() {
|
||||
use ruin_ui::{ElementId, KeyboardEvent, KeyboardEventKind, KeyboardKey};
|
||||
|
||||
@@ -31,6 +31,11 @@ pub fn button() -> ButtonBuilder {
|
||||
impl ButtonBuilder {
|
||||
impl_props_methods!();
|
||||
|
||||
pub fn shadow(mut self, shadow: impl crate::converters::IntoShadow) -> Self {
|
||||
self.props = self.props.shadow(shadow);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_press(mut self, handler: impl Fn(&RoutedPointerEvent) + 'static) -> Self {
|
||||
self.on_press = Some(Rc::new(handler));
|
||||
self
|
||||
@@ -44,6 +49,9 @@ impl ButtonBuilder {
|
||||
pub fn children(mut self, children: impl Children) -> View {
|
||||
let id = allocate_element_id();
|
||||
self.props.element = self.props.element.id(id);
|
||||
if let Some(widget_ref) = &self.props.widget_ref {
|
||||
let _ = widget_ref.set(Some(id));
|
||||
}
|
||||
let had_style = self.props.partial_text_style.is_some();
|
||||
if let Some(style) = self.props.partial_text_style.take() {
|
||||
push_text_style(style);
|
||||
|
||||
@@ -128,16 +128,6 @@ pub fn block() -> ContainerBuilder {
|
||||
impl ContainerBuilder {
|
||||
impl_props_methods!();
|
||||
|
||||
pub fn align_items(mut self, align: AlignItems) -> Self {
|
||||
self.props = self.props.align_items(align);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn align_self(mut self, align: AlignItems) -> Self {
|
||||
self.props = self.props.align_self(align);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn shadow(mut self, shadow: impl IntoShadow) -> Self {
|
||||
self.props = self.props.shadow(shadow);
|
||||
self
|
||||
|
||||
@@ -45,6 +45,14 @@ macro_rules! impl_props_methods {
|
||||
self.props = self.props.flex(flex);
|
||||
self
|
||||
}
|
||||
pub fn align_items(mut self, align: ruin_ui::AlignItems) -> Self {
|
||||
self.props = self.props.align_items(align);
|
||||
self
|
||||
}
|
||||
pub fn align_self(mut self, align: ruin_ui::AlignItems) -> Self {
|
||||
self.props = self.props.align_self(align);
|
||||
self
|
||||
}
|
||||
pub fn widget_ref<T>(mut self, widget_ref: $crate::hooks::WidgetRef<T>) -> Self {
|
||||
self.props = self.props.widget_ref(widget_ref);
|
||||
self
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# RUIN - Runtime
|
||||
|
||||
The RUIN runtime is an event-loop-per-thread, non-workstealing async executor implemented on top of Linux io_uring
|
||||
with
|
||||
@@ -8,11 +8,12 @@
|
||||
//! The public surface intentionally mirrors `std::fs` where that shape makes sense, while using
|
||||
//! async methods for operations that may block the caller.
|
||||
|
||||
use alloc::sync::Arc;
|
||||
|
||||
use std::ffi::OsStr;
|
||||
use std::io;
|
||||
use std::os::fd::{AsRawFd, OwnedFd};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::op::fs::{
|
||||
FileType as RawFileType, FsOp, MetadataTarget, OpenOptions as OpOpenOptions,
|
||||
|
||||
@@ -44,6 +44,7 @@ pub mod net;
|
||||
pub mod op;
|
||||
#[doc(hidden)]
|
||||
pub mod platform;
|
||||
pub mod stdio;
|
||||
#[doc(hidden)]
|
||||
pub mod sys;
|
||||
pub mod time;
|
||||
|
||||
@@ -3,14 +3,15 @@
|
||||
//! The public surface follows the general shape of `std::net`, but uses async methods for socket
|
||||
//! operations that would otherwise block the caller.
|
||||
|
||||
use std::future::Future;
|
||||
use core::future::Future;
|
||||
use core::pin::Pin;
|
||||
use core::task::{Context, Poll};
|
||||
use core::time::Duration;
|
||||
|
||||
use std::io;
|
||||
use std::net::{Shutdown, SocketAddr, ToSocketAddrs};
|
||||
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
|
||||
use std::pin::Pin;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::task::{Context, Poll};
|
||||
use std::time::Duration;
|
||||
|
||||
use hyper::rt::{Read as HyperRead, ReadBufCursor, Write as HyperWrite};
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
//! Public runtime driver primitives.
|
||||
//! RUIN Runtime Driver for Linux x86_64.
|
||||
//!
|
||||
//!
|
||||
|
||||
use std::cell::Cell;
|
||||
use std::cell::RefCell;
|
||||
@@ -88,12 +90,21 @@ pub struct ReadyEvents {
|
||||
|
||||
/// Low-level Linux runtime driver backed by `io_uring`.
|
||||
pub struct Driver {
|
||||
/// The `io_uring` instance driving this runtime thread.
|
||||
ring: IoUring,
|
||||
/// Shared notifier that other threads can use to wake this runtime thread.
|
||||
notifier: Arc<NotifierInner>,
|
||||
/// Next sequence number for generated completion tokens.
|
||||
next_token: Cell<u64>,
|
||||
/// The token of the currently active timer, if any timer is armed.
|
||||
active_timer_token: Cell<Option<u64>>,
|
||||
/// Accumulated count of pending wake notifications that have not yet been triggered.
|
||||
pending_wakes: Cell<u64>,
|
||||
/// Accumulated count of pending timer expirations that have not yet been triggered.
|
||||
pending_timers: Cell<u64>,
|
||||
/// Map of active completion tokens to associated handlers. When a CQE is received with a token in this map, the
|
||||
/// corresponding handler will be invoked with the CQE and removed from the map. This is the core mechanism by which
|
||||
/// async operations are tracked and dispatched to their continuations.
|
||||
completions: RefCell<HashMap<u64, CompletionHandler>>,
|
||||
}
|
||||
|
||||
|
||||
191
lib/runtime/src/stdio.rs
Normal file
191
lib/runtime/src/stdio.rs
Normal file
@@ -0,0 +1,191 @@
|
||||
//! Async standard-input helpers.
|
||||
|
||||
use std::cell::Cell;
|
||||
use std::io;
|
||||
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
|
||||
use std::thread;
|
||||
|
||||
use crate::op::completion::completion_for_current_thread;
|
||||
use crate::platform::linux_x86_64::runtime::with_current_driver;
|
||||
use crate::platform::linux_x86_64::uring::{IORING_OP_READ, IoUringCqe, IoUringSqe};
|
||||
|
||||
thread_local! {
|
||||
static STDIN_URING_SUPPORTED: Cell<Option<bool>> = const { Cell::new(None) };
|
||||
}
|
||||
|
||||
const FILE_CURSOR: u64 = u64::MAX;
|
||||
const READ_CHUNK_BYTES: usize = 1024;
|
||||
|
||||
/// Async line-oriented stdin reader.
|
||||
///
|
||||
/// The reader is `io_uring`-first. When the active stdin fd rejects `IORING_OP_READ`, the module
|
||||
/// falls back to a helper-thread blocking read on the same duplicated fd.
|
||||
pub struct Stdin {
|
||||
fd: OwnedFd,
|
||||
buffer: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Opens an async stdin reader.
|
||||
pub fn stdin() -> io::Result<Stdin> {
|
||||
let raw = cvt(unsafe { libc::fcntl(libc::STDIN_FILENO, libc::F_DUPFD_CLOEXEC, 0) })?;
|
||||
Ok(Stdin {
|
||||
fd: unsafe { OwnedFd::from_raw_fd(raw) },
|
||||
buffer: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
impl Stdin {
|
||||
/// Reads a single UTF-8 line, including the trailing newline when present.
|
||||
///
|
||||
/// Returns `Ok(None)` on EOF.
|
||||
pub async fn read_line(&mut self) -> io::Result<Option<String>> {
|
||||
loop {
|
||||
if let Some(index) = self.buffer.iter().position(|byte| *byte == b'\n') {
|
||||
let line = self.buffer.drain(..=index).collect::<Vec<_>>();
|
||||
return decode_line(line).map(Some);
|
||||
}
|
||||
|
||||
let chunk = self.read_chunk().await?;
|
||||
if chunk.is_empty() {
|
||||
if self.buffer.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
let line = std::mem::take(&mut self.buffer);
|
||||
return decode_line(line).map(Some);
|
||||
}
|
||||
|
||||
self.buffer.extend_from_slice(&chunk);
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_chunk(&self) -> io::Result<Vec<u8>> {
|
||||
let fd = self.fd.as_raw_fd();
|
||||
let support = STDIN_URING_SUPPORTED.with(Cell::get);
|
||||
if support != Some(false) {
|
||||
match submit_uring_read(fd, READ_CHUNK_BYTES).await {
|
||||
Ok(bytes) => {
|
||||
STDIN_URING_SUPPORTED.with(|state| state.set(Some(true)));
|
||||
return Ok(bytes);
|
||||
}
|
||||
Err(error) if should_fallback_to_offload(&error) => {
|
||||
STDIN_URING_SUPPORTED.with(|state| state.set(Some(false)));
|
||||
}
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
offload(move || {
|
||||
let mut buffer = vec![0; READ_CHUNK_BYTES];
|
||||
let read = blocking_read(fd, &mut buffer)?;
|
||||
if read == 0 {
|
||||
buffer.clear();
|
||||
} else {
|
||||
buffer.truncate(read);
|
||||
}
|
||||
Ok(buffer)
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
async fn offload<T: Send + 'static>(
|
||||
task: impl FnOnce() -> io::Result<T> + Send + 'static,
|
||||
) -> io::Result<T> {
|
||||
let (future, handle) = completion_for_current_thread::<io::Result<T>>();
|
||||
thread::Builder::new()
|
||||
.name("ruin-runtime-stdio-offload".into())
|
||||
.spawn(move || handle.complete(task()))
|
||||
.map_err(io::Error::other)?;
|
||||
future.await
|
||||
}
|
||||
|
||||
async fn submit_uring_read(fd: RawFd, len: usize) -> io::Result<Vec<u8>> {
|
||||
let mut buffer = vec![0; len];
|
||||
let ptr = buffer.as_mut_ptr();
|
||||
let capacity = buffer.len();
|
||||
submit_uring(
|
||||
move |sqe| {
|
||||
sqe.opcode = IORING_OP_READ;
|
||||
sqe.fd = fd;
|
||||
sqe.addr = ptr as u64;
|
||||
sqe.len = capacity as u32;
|
||||
sqe.off = FILE_CURSOR;
|
||||
},
|
||||
move |cqe| {
|
||||
let read = cqe_to_result(cqe)? as usize;
|
||||
buffer.truncate(read);
|
||||
Ok(buffer)
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn submit_uring<T: Send + 'static, M>(
|
||||
fill: impl FnOnce(&mut IoUringSqe),
|
||||
map: M,
|
||||
) -> io::Result<T>
|
||||
where
|
||||
M: FnOnce(IoUringCqe) -> io::Result<T> + Send + 'static,
|
||||
{
|
||||
let (future, handle) = completion_for_current_thread::<io::Result<T>>();
|
||||
let callback_handle = handle.clone();
|
||||
let token = with_current_driver(|driver| {
|
||||
driver.submit_operation(fill, move |cqe| {
|
||||
callback_handle.complete(map(cqe));
|
||||
})
|
||||
})?;
|
||||
|
||||
handle.set_cancel(move || {
|
||||
let _ = with_current_driver(|driver| driver.cancel_operation(token));
|
||||
});
|
||||
|
||||
future.await
|
||||
}
|
||||
|
||||
fn blocking_read(fd: RawFd, buffer: &mut [u8]) -> io::Result<usize> {
|
||||
loop {
|
||||
let read = unsafe {
|
||||
libc::read(
|
||||
fd,
|
||||
buffer.as_mut_ptr().cast::<libc::c_void>(),
|
||||
buffer.len(),
|
||||
)
|
||||
};
|
||||
if read >= 0 {
|
||||
return Ok(read as usize);
|
||||
}
|
||||
|
||||
let error = io::Error::last_os_error();
|
||||
if error.kind() == io::ErrorKind::Interrupted {
|
||||
continue;
|
||||
}
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_line(bytes: Vec<u8>) -> io::Result<String> {
|
||||
String::from_utf8(bytes).map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))
|
||||
}
|
||||
|
||||
fn cqe_to_result(cqe: IoUringCqe) -> io::Result<i32> {
|
||||
if cqe.res < 0 {
|
||||
Err(io::Error::from_raw_os_error(-cqe.res))
|
||||
} else {
|
||||
Ok(cqe.res)
|
||||
}
|
||||
}
|
||||
|
||||
fn should_fallback_to_offload(error: &io::Error) -> bool {
|
||||
matches!(
|
||||
error.raw_os_error(),
|
||||
Some(libc::EINVAL | libc::ENOSYS | libc::EOPNOTSUPP)
|
||||
)
|
||||
}
|
||||
|
||||
fn cvt(value: libc::c_int) -> io::Result<libc::c_int> {
|
||||
if value == -1 {
|
||||
Err(io::Error::last_os_error())
|
||||
} else {
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
@@ -3,14 +3,14 @@
|
||||
//! These helpers integrate with the runtime's timer queue and are designed to be used from
|
||||
//! futures scheduled with [`crate::queue_future`] or one of the runtime entry macros.
|
||||
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::fmt;
|
||||
use std::future::{Future, poll_fn};
|
||||
use std::pin::Pin;
|
||||
use std::rc::Rc;
|
||||
use std::task::Waker;
|
||||
use std::task::{Context, Poll};
|
||||
use std::time::Duration;
|
||||
use alloc::rc::Rc;
|
||||
use core::cell::{Cell, RefCell};
|
||||
use core::fmt;
|
||||
use core::future::{Future, poll_fn};
|
||||
use core::pin::Pin;
|
||||
use core::task::Waker;
|
||||
use core::task::{Context, Poll};
|
||||
use core::time::Duration;
|
||||
|
||||
use crate::{clear_timeout, set_timeout};
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ pub struct RoutedPointerEvent {
|
||||
#[derive(Default)]
|
||||
pub struct PointerRouter {
|
||||
hovered: Vec<HitTarget>,
|
||||
pressed: Option<HitTarget>,
|
||||
pressed: Option<Vec<HitTarget>>,
|
||||
}
|
||||
|
||||
impl PointerRouter {
|
||||
@@ -71,7 +71,11 @@ impl PointerRouter {
|
||||
}
|
||||
|
||||
pub fn pressed_target(&self) -> Option<&HitTarget> {
|
||||
self.pressed.as_ref()
|
||||
self.pressed.as_ref().and_then(|targets| targets.last())
|
||||
}
|
||||
|
||||
pub fn pressed_targets(&self) -> &[HitTarget] {
|
||||
self.pressed.as_deref().unwrap_or(&[])
|
||||
}
|
||||
|
||||
pub fn route(
|
||||
@@ -114,7 +118,12 @@ impl PointerRouter {
|
||||
|
||||
match event.kind {
|
||||
PointerEventKind::Move => {
|
||||
if let Some(target) = self.pressed.clone().or(hit_target.clone()) {
|
||||
if let Some(target) = self
|
||||
.pressed
|
||||
.as_ref()
|
||||
.and_then(|targets| targets.last().cloned())
|
||||
.or(hit_target.clone())
|
||||
{
|
||||
routed.push(RoutedPointerEvent {
|
||||
kind: RoutedPointerEventKind::Move,
|
||||
target,
|
||||
@@ -125,7 +134,7 @@ impl PointerRouter {
|
||||
}
|
||||
PointerEventKind::Down { button } => {
|
||||
if let Some(target) = hit_target {
|
||||
self.pressed = Some(target.clone());
|
||||
self.pressed = Some(self.hovered.clone());
|
||||
routed.push(RoutedPointerEvent {
|
||||
kind: RoutedPointerEventKind::Down { button },
|
||||
target,
|
||||
@@ -135,7 +144,12 @@ impl PointerRouter {
|
||||
}
|
||||
}
|
||||
PointerEventKind::Up { button } => {
|
||||
if let Some(target) = self.pressed.take().or(hit_target) {
|
||||
if let Some(target) = self
|
||||
.pressed
|
||||
.take()
|
||||
.and_then(|targets| targets.last().cloned())
|
||||
.or(hit_target)
|
||||
{
|
||||
routed.push(RoutedPointerEvent {
|
||||
kind: RoutedPointerEventKind::Up { button },
|
||||
target,
|
||||
|
||||
@@ -148,6 +148,7 @@ enum InternalBackendEvent {
|
||||
|
||||
struct State {
|
||||
running: bool,
|
||||
fixed_size_request: Option<(u32, u32)>,
|
||||
_connection: Connection,
|
||||
_compositor: wl_compositor::WlCompositor,
|
||||
_surface: wl_surface::WlSurface,
|
||||
@@ -232,6 +233,18 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
fn fixed_size_request_for_spec(spec: &WindowSpec) -> Option<(u32, u32)> {
|
||||
if spec.resizable {
|
||||
return None;
|
||||
}
|
||||
let size = spec
|
||||
.requested_inner_size
|
||||
.or(spec.min_inner_size)
|
||||
.or(spec.max_inner_size)
|
||||
.unwrap_or_else(|| UiSize::new(800.0, 500.0));
|
||||
Some((size.width.max(1.0).round() as u32, size.height.max(1.0).round() as u32))
|
||||
}
|
||||
|
||||
fn wayland_cursor_shape(icon: CursorIcon) -> wp_cursor_shape_device_v1::Shape {
|
||||
match icon {
|
||||
CursorIcon::Default => wp_cursor_shape_device_v1::Shape::Default,
|
||||
@@ -364,6 +377,7 @@ impl WaylandWindow {
|
||||
surface_target,
|
||||
state: State {
|
||||
running: true,
|
||||
fixed_size_request: fixed_size_request_for_spec(&spec),
|
||||
_connection: connection,
|
||||
_compositor: compositor,
|
||||
_surface: surface,
|
||||
@@ -697,6 +711,7 @@ impl WaylandWindow {
|
||||
}
|
||||
|
||||
pub fn apply_spec(&mut self, spec: &WindowSpec) -> Result<(), Box<dyn Error>> {
|
||||
self.state.fixed_size_request = fixed_size_request_for_spec(spec);
|
||||
self.state._toplevel.set_title(spec.title.clone());
|
||||
if let Some(app_id) = spec.app_id.as_ref() {
|
||||
self.state._toplevel.set_app_id(app_id.clone());
|
||||
@@ -732,6 +747,14 @@ impl WaylandWindow {
|
||||
}
|
||||
|
||||
if let Some((width, height)) = self.state.pending_size.take() {
|
||||
trace!(
|
||||
target: "ruin_ui_platform_wayland::resize",
|
||||
width,
|
||||
height,
|
||||
previous_width = self.state.current_size.0,
|
||||
previous_height = self.state.current_size.1,
|
||||
"prepare_frame observed pending size change"
|
||||
);
|
||||
self.state.current_size = (width, height);
|
||||
self.state.needs_redraw = false;
|
||||
return Some(FrameRequest {
|
||||
@@ -742,6 +765,12 @@ impl WaylandWindow {
|
||||
}
|
||||
|
||||
if self.state.needs_redraw {
|
||||
trace!(
|
||||
target: "ruin_ui_platform_wayland::resize",
|
||||
width = self.state.current_size.0,
|
||||
height = self.state.current_size.1,
|
||||
"prepare_frame servicing redraw without size change"
|
||||
);
|
||||
self.state.needs_redraw = false;
|
||||
return Some(FrameRequest {
|
||||
width: self.state.current_size.0,
|
||||
@@ -2101,6 +2130,14 @@ impl Dispatch<xdg_surface::XdgSurface, ()> for State {
|
||||
if let xdg_surface::Event::Configure { serial } = event {
|
||||
xdg_surface.ack_configure(serial);
|
||||
state.configured = true;
|
||||
trace!(
|
||||
target: "ruin_ui_platform_wayland::resize",
|
||||
serial,
|
||||
current_width = state.current_size.0,
|
||||
current_height = state.current_size.1,
|
||||
has_pending_size = state.pending_size.is_some(),
|
||||
"received xdg_surface configure"
|
||||
);
|
||||
state.request_redraw();
|
||||
}
|
||||
}
|
||||
@@ -2153,7 +2190,37 @@ impl Dispatch<xdg_toplevel::XdgToplevel, ()> for State {
|
||||
height,
|
||||
"received Wayland toplevel configure"
|
||||
);
|
||||
state.pending_size = Some((width, height));
|
||||
let next_size = (width, height);
|
||||
if let Some(expected_size) = state.fixed_size_request {
|
||||
if next_size != expected_size {
|
||||
trace!(
|
||||
target: "ruin_ui_platform_wayland::resize",
|
||||
width,
|
||||
height,
|
||||
expected_width = expected_size.0,
|
||||
expected_height = expected_size.1,
|
||||
"ignoring stale toplevel configure for fixed-size window"
|
||||
);
|
||||
state.request_redraw();
|
||||
return;
|
||||
}
|
||||
}
|
||||
let size_changed =
|
||||
next_size != state.current_size && state.pending_size != Some(next_size);
|
||||
trace!(
|
||||
target: "ruin_ui_platform_wayland::resize",
|
||||
width,
|
||||
height,
|
||||
current_width = state.current_size.0,
|
||||
current_height = state.current_size.1,
|
||||
pending_width = state.pending_size.map(|size| size.0),
|
||||
pending_height = state.pending_size.map(|size| size.1),
|
||||
size_changed,
|
||||
"processed Wayland toplevel configure"
|
||||
);
|
||||
if size_changed {
|
||||
state.pending_size = Some(next_size);
|
||||
}
|
||||
state.request_redraw();
|
||||
}
|
||||
_ => {}
|
||||
|
||||
Reference in New Issue
Block a user