use webassembly with the server acting only as an intermediate
This commit is contained in:
parent
9aa1130c35
commit
7d0ef62c42
29 changed files with 850 additions and 325 deletions
7
dist_text/Cargo.toml
Normal file
7
dist_text/Cargo.toml
Normal file
|
@ -0,0 +1,7 @@
|
|||
[package]
|
||||
name = "dist_text"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
crdts = "7.3.0"
|
3
dist_text/src/lib.rs
Normal file
3
dist_text/src/lib.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub mod text;
|
||||
|
||||
pub use crdts;
|
180
dist_text/src/text.rs
Normal file
180
dist_text/src/text.rs
Normal file
|
@ -0,0 +1,180 @@
|
|||
use std::fmt::{Display, Formatter};
|
||||
use crdts::{CmRDT, List};
|
||||
use crdts::list::Op;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Text {
|
||||
inner: List<u16, String>,
|
||||
}
|
||||
|
||||
impl Default for Text {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
inner: List::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Text {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let str_bytes: Vec<u16> = self.inner.to_owned().read_into();
|
||||
let str_repr = String::from_utf16_lossy(&str_bytes);
|
||||
write!(f, "{}", str_repr)
|
||||
}
|
||||
}
|
||||
|
||||
impl Text {
|
||||
pub fn apply_ops(&mut self, ops: Vec<Op<u16, String>>) {
|
||||
ops.iter().for_each(move |op| self.inner.apply(op.to_owned()));
|
||||
}
|
||||
|
||||
pub fn insert_linebreak(&mut self, start: usize, end: usize, src: &str) -> Vec<Op<u16, String>> {
|
||||
self.insert(start, end, "\n", src)
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, start: usize, end: usize, data: &str, src: &str) -> Vec<Op<u16, String>> {
|
||||
let mut ops: Vec<Op<u16, String>> = Vec::new();
|
||||
|
||||
if start < end {
|
||||
let mut delete_ops = self.delete(start, end, src);
|
||||
ops.append(&mut delete_ops);
|
||||
}
|
||||
|
||||
let data: Vec<u16> = data.encode_utf16().collect();
|
||||
|
||||
let mut insert_idx = start;
|
||||
for byte in data.iter() {
|
||||
let op = self.inner.insert_index(insert_idx, *byte, src.to_owned());
|
||||
self.inner.apply(op.to_owned());
|
||||
insert_idx += 1;
|
||||
|
||||
ops.push(op);
|
||||
}
|
||||
|
||||
ops
|
||||
}
|
||||
|
||||
pub fn delete_backward(&mut self, start: usize, end: usize, src: &str) -> Vec<Op<u16, String>> {
|
||||
if (start == 0 && start == end) || end > self.inner.len() { return Vec::new(); }
|
||||
|
||||
let (end, start) = if start == end { (start, start - 1) } else { (end, start) };
|
||||
self.delete(start, end, src)
|
||||
}
|
||||
|
||||
pub fn delete_forward(&mut self, start: usize, end: usize, src: &str) -> Vec<Op<u16, String>> {
|
||||
if start >= self.inner.len() { return Vec::new(); }
|
||||
|
||||
let end = if start == end { start + 1 } else { end };
|
||||
self.delete(start, end, src)
|
||||
}
|
||||
|
||||
pub fn delete(&mut self, start: usize, end: usize, src: &str) -> Vec<Op<u16, String>> {
|
||||
let mut start_idx = start;
|
||||
let mut end_idx = end;
|
||||
|
||||
while let Some(byte) = self.inner.position(start_idx) {
|
||||
if !matches!(byte, 0xDC00..=0xDFFF) {
|
||||
break;
|
||||
}
|
||||
start_idx -= 1;
|
||||
}
|
||||
|
||||
if let Some(end_byte) = self.inner.position(end_idx - 1) {
|
||||
if end_idx < self.inner.len() && end_byte > &0xD800 && end_byte < &0xDC00 {
|
||||
end_idx += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let mut ops: Vec<Op<u16, String>> = Vec::new();
|
||||
|
||||
for _ in start_idx..end_idx {
|
||||
if let Some(op) = self.inner.delete_index(start_idx, src.to_owned()) {
|
||||
self.inner.apply(op.to_owned());
|
||||
ops.push(op);
|
||||
}
|
||||
}
|
||||
|
||||
ops
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::text::Text;
|
||||
|
||||
#[test]
|
||||
fn insert_delete() {
|
||||
let mut t = Text::new();
|
||||
t.insert(0, 0, "Hello", "A");
|
||||
t.insert(5, 5, "world!", "A");
|
||||
t.insert(5, 5, ", ", "B");
|
||||
|
||||
assert_eq!(t.to_string(), "Hello, world!");
|
||||
|
||||
t.insert(7, 12, "🌍", "A");
|
||||
assert_eq!(t.to_string(), "Hello, 🌍!");
|
||||
|
||||
t.delete(8, 9, "B");
|
||||
t.insert(7, 7, "🚀", "C");
|
||||
assert_eq!(t.to_string(), "Hello, 🚀!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backspace() {
|
||||
let mut t = Text::new();
|
||||
t.insert(0, 0, "Hello", "A");
|
||||
|
||||
t.delete_backward(5, 5, "A");
|
||||
assert_eq!(t.to_string(), "Hell");
|
||||
|
||||
t.delete_backward(0, 4, "A");
|
||||
assert_eq!(t.to_string(), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete() {
|
||||
let mut t = Text::new();
|
||||
t.insert(0, 0, "Hello", "A");
|
||||
|
||||
t.delete_forward(4, 4, "A");
|
||||
assert_eq!(t.to_string(), "Hell");
|
||||
|
||||
t.delete_forward(0, 4, "A");
|
||||
assert_eq!(t.to_string(), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linebreak() {
|
||||
let mut t = Text::new();
|
||||
t.insert(0, 0, "Hello, world!", "A");
|
||||
t.insert_linebreak(6, 6, "A");
|
||||
|
||||
assert_eq!(t.to_string(), "Hello,\n world!");
|
||||
|
||||
t.delete_backward(7, 7, "B");
|
||||
t.insert_linebreak(6, 7, "A");
|
||||
|
||||
assert_eq!(t.to_string(), "Hello,\nworld!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_ops() {
|
||||
let mut t_a = Text::new();
|
||||
let ops_a = t_a.insert(0, 0, "Hello, world!", "A");
|
||||
|
||||
let mut t_b = Text::new();
|
||||
t_b.apply_ops(ops_a);
|
||||
|
||||
assert_eq!(t_b.to_string(), "Hello, world!");
|
||||
|
||||
let ops_a = t_a.insert(7,7, "distributed ", "A");
|
||||
t_b.apply_ops(ops_a);
|
||||
|
||||
let ops_b = t_b.insert(7,7, "cruel ", "B");
|
||||
t_a.apply_ops(ops_b);
|
||||
|
||||
|
||||
assert_eq!(t_a.to_string(), "Hello, cruel distributed world!");
|
||||
assert_eq!(t_b.to_string(), "Hello, cruel distributed world!");
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue