Add richtext example using Quill (#145)

* feat: richtext example init

* fix: pass richtext event delta consistency check

* chore: debug history
This commit is contained in:
Zixuan Chen 2023-11-03 16:59:27 +08:00 committed by GitHub
parent da16b8a99d
commit 7a19b49acb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 3645 additions and 695 deletions

View file

@ -45,7 +45,7 @@
"*.rs": "${capture}.excalidraw"
},
"excalidraw.theme": "dark",
"deno.enable": true,
"deno.enable": false ,
"cortex-debug.variableUseNaturalFormat": true,
"[markdown]": {
"editor.defaultFormatter": "darkriszty.markdown-table-prettify"

View file

@ -339,16 +339,6 @@ dependencies = [
"libc",
]
[[package]]
name = "jumprope"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "829c74fe88dda0d2a5425b022b44921574a65c4eb78e6e39a61b40eb416a4ef8"
dependencies = [
"rand",
"str_indices",
]
[[package]]
name = "libc"
version = "0.2.147"
@ -406,7 +396,6 @@ dependencies = [
"getrandom",
"im",
"itertools",
"jumprope",
"loro-common",
"loro-preload",
"miniz_oxide",
@ -873,12 +862,6 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "str_indices"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f026164926842ec52deb1938fae44f83dfdb82d0a5b0270c5bd5935ab74d6dd"
[[package]]
name = "string_cache"
version = "0.8.7"

View file

@ -1,5 +1,5 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use loro_internal::fuzz::{test_multi_sites_refactored, Action};
use loro_internal::fuzz::{test_multi_sites, Action};
fuzz_target!(|actions: Vec<Action>| { test_multi_sites_refactored(5, &mut actions.clone()) });
fuzz_target!(|actions: Vec<Action>| { test_multi_sites(5, &mut actions.clone()) });

View file

@ -81,7 +81,6 @@ impl StyleRangeMap {
}
pub fn annotate(&mut self, range: Range<usize>, style: Arc<StyleOp>) {
debug_log::debug_log!("Annotate {:?}", &range);
let range = self.tree.range::<LengthFinder>(range);
if range.is_none() {
unreachable!();
@ -89,7 +88,6 @@ impl StyleRangeMap {
self.has_style = true;
let range = range.unwrap();
debug_log::debug_log!("Range={:?}", &range);
self.tree
.update(range.start.cursor..range.end.cursor, &mut |x| {
if let Some(set) = x.styles.get_mut(&style.get_style_key()) {
@ -163,8 +161,8 @@ impl StyleRangeMap {
false
});
self.tree.insert_by_path(right, Elem { len, styles });
return &self.tree.get_elem(right.leaf).unwrap().styles;
let (target, _) = self.tree.insert_by_path(right, Elem { len, styles });
return &self.tree.get_elem(target.leaf).unwrap().styles;
}
/// Return the style sets beside `index` and get the intersection of them.

View file

@ -76,7 +76,7 @@ impl Tracker {
return;
}
debug_log::group!("before insert {} pos={}", op_id, pos);
// debug_log::group!("before insert {} pos={}", op_id, pos);
// debug_log::debug_dbg!(&self);
let result = self.rope.insert(
pos,
@ -101,13 +101,13 @@ impl Tracker {
self.current_vv.extend_to_include_end_id(end_id);
self.applied_vv.extend_to_include_end_id(end_id);
// debug_log::debug_dbg!(&self);
debug_log::group_end!();
// debug_log::group_end!();
}
fn update_insert_by_split(&mut self, split: &[LeafIndex]) {
for &new_leaf_idx in split {
let leaf = self.rope.tree().get_elem(new_leaf_idx).unwrap();
debug_log::debug_dbg!(&leaf.id_span(), new_leaf_idx);
// debug_log::debug_dbg!(&leaf.id_span(), new_leaf_idx);
self.id_to_cursor
.update_insert(leaf.id_span(), new_leaf_idx)
}
@ -229,7 +229,6 @@ impl Tracker {
self._checkout(to, true);
// debug_log::debug_dbg!(from, to, &self);
// self.id_to_cursor.diagnose();
debug_log::debug_dbg!(&self);
self.rope.get_diff()
}
}

View file

@ -242,7 +242,7 @@ impl CrdtRope {
let start = start.cursor;
let elem = self.tree.get_elem_mut(start.leaf).unwrap();
if elem.rle_len() >= start.offset + len {
debug_log::debug_log!("len={} offset={} l={} ", elem.rle_len(), start.offset, len,);
// debug_log::debug_log!("len={} offset={} l={} ", elem.rle_len(), start.offset, len,);
let (_, splitted) = self.tree.update_leaf(start.leaf, |elem| {
let (a, b) = elem.update_with_split(start.offset..start.offset + len, |elem| {
assert!(elem.is_activated());
@ -254,7 +254,7 @@ impl CrdtRope {
(true, a, b)
});
debug_log::debug_dbg!(&splitted);
// debug_log::debug_dbg!(&splitted);
return splitted;
}

View file

@ -331,7 +331,6 @@ impl Cursor {
}
_ => unreachable!(),
}
debug_log::debug_dbg!(&self);
}
fn get_insert(&self, pos: usize) -> Option<LeafIndex> {

View file

@ -18,9 +18,9 @@ impl<V: Serialize, M: Serialize> Serialize for Delta<V, M> {
#[derive(Debug, EnumAsInner, Clone, PartialEq, Eq, Serialize)]
pub enum DeltaItem<Value, Meta> {
Retain { len: usize, meta: Meta },
Insert { value: Value, meta: Meta },
Delete { len: usize, meta: Meta },
Retain { retain: usize, attributes: Meta },
Insert { insert: Value, attributes: Meta },
Delete { delete: usize, attributes: Meta },
}
#[derive(PartialEq, Eq)]
@ -87,9 +87,18 @@ impl<V: DeltaValue, M: Debug> DeltaValue for DeltaItem<V, M> {
fn length(&self) -> usize {
match self {
DeltaItem::Retain { len, meta: _ } => *len,
DeltaItem::Insert { value, meta: _ } => value.length(),
DeltaItem::Delete { len, meta: _ } => *len,
DeltaItem::Retain {
retain: len,
attributes: _,
} => *len,
DeltaItem::Insert {
insert: value,
attributes: _,
} => value.length(),
DeltaItem::Delete {
delete: len,
attributes: _,
} => *len,
}
}
}
@ -97,25 +106,42 @@ impl<V: DeltaValue, M: Debug> DeltaValue for DeltaItem<V, M> {
impl<Value: DeltaValue, M: Meta> DeltaItem<Value, M> {
pub fn meta(&self) -> &M {
match self {
DeltaItem::Insert { meta, .. } => meta,
DeltaItem::Retain { meta, .. } => meta,
DeltaItem::Delete { len: _, meta } => meta,
DeltaItem::Insert {
attributes: meta, ..
} => meta,
DeltaItem::Retain {
attributes: meta, ..
} => meta,
DeltaItem::Delete {
delete: _,
attributes: meta,
} => meta,
}
}
pub fn meta_mut(&mut self) -> &mut M {
match self {
DeltaItem::Insert { meta, .. } => meta,
DeltaItem::Retain { meta, .. } => meta,
DeltaItem::Delete { len: _, meta } => meta,
DeltaItem::Insert {
attributes: meta, ..
} => meta,
DeltaItem::Retain {
attributes: meta, ..
} => meta,
DeltaItem::Delete {
delete: _,
attributes: meta,
} => meta,
}
}
pub fn set_meta(&mut self, meta: M) {
match self {
DeltaItem::Insert { meta: m, .. } => *m = meta,
DeltaItem::Retain { meta: m, .. } => *m = meta,
DeltaItem::Delete { len: _, meta: m } => *m = meta,
DeltaItem::Insert { attributes: m, .. } => *m = meta,
DeltaItem::Retain { attributes: m, .. } => *m = meta,
DeltaItem::Delete {
delete: _,
attributes: m,
} => *m = meta,
}
}
@ -138,26 +164,35 @@ impl<Value: DeltaValue, M: Meta> DeltaItem<Value, M> {
// and return the taken one.
pub(crate) fn take(&mut self, length: usize) -> Self {
match self {
DeltaItem::Insert { value, meta } => {
DeltaItem::Insert {
insert: value,
attributes: meta,
} => {
let v = value.take(length);
Self::Insert {
value: v,
meta: meta.clone(),
insert: v,
attributes: meta.clone(),
}
}
DeltaItem::Retain { len, meta } => {
DeltaItem::Retain {
retain: len,
attributes: meta,
} => {
*len -= length;
Self::Retain {
len: length,
meta: meta.clone(),
retain: length,
attributes: meta.clone(),
}
}
DeltaItem::Delete { len, meta: _ } => {
DeltaItem::Delete {
delete: len,
attributes: _,
} => {
*len -= length;
Self::Delete {
len: length,
delete: length,
// meta may store utf16 length, this take will invalidate it
meta: M::empty(),
attributes: M::empty(),
}
}
}
@ -165,25 +200,34 @@ impl<Value: DeltaValue, M: Meta> DeltaItem<Value, M> {
pub(crate) fn take_with_meta_ref(&mut self, length: usize, other_meta: &Self) -> Self {
match self {
DeltaItem::Insert { value, meta } => {
DeltaItem::Insert {
insert: value,
attributes: meta,
} => {
let v = value.take(length);
Self::Insert {
value: v,
meta: meta.take(other_meta.meta()),
insert: v,
attributes: meta.take(other_meta.meta()),
}
}
DeltaItem::Retain { len, meta } => {
DeltaItem::Retain {
retain: len,
attributes: meta,
} => {
*len -= length;
Self::Retain {
len: length,
meta: meta.take(other_meta.meta()),
retain: length,
attributes: meta.take(other_meta.meta()),
}
}
DeltaItem::Delete { len, meta } => {
DeltaItem::Delete {
delete: len,
attributes: meta,
} => {
*len -= length;
Self::Delete {
len: length,
meta: meta.take(other_meta.meta()),
delete: length,
attributes: meta.take(other_meta.meta()),
}
}
}
@ -191,7 +235,7 @@ impl<Value: DeltaValue, M: Meta> DeltaItem<Value, M> {
fn insert_inner(self) -> Value {
match self {
DeltaItem::Insert { value, .. } => value,
DeltaItem::Insert { insert: value, .. } => value,
_ => unreachable!(),
}
}
@ -229,8 +273,8 @@ impl<V: DeltaValue, M: Meta> DeltaIterator<V, M> {
let next_op = self.peek_mut();
if next_op.is_none() {
return DeltaItem::Retain {
len: usize::MAX,
meta: M::empty(),
retain: usize::MAX,
attributes: M::empty(),
};
}
let op = next_op.unwrap();
@ -247,8 +291,8 @@ impl<V: DeltaValue, M: Meta> DeltaIterator<V, M> {
let next_op = self.peek_mut();
if next_op.is_none() {
return DeltaItem::Retain {
len: other.length(),
meta: other.meta().clone(),
retain: other.length(),
attributes: other.meta().clone(),
};
}
let op = next_op.unwrap();
@ -344,20 +388,26 @@ impl<Value: DeltaValue, M: Meta> Delta<Value, M> {
return self;
}
self.push(DeltaItem::Retain { len, meta });
self.push(DeltaItem::Retain {
retain: len,
attributes: meta,
});
self
}
pub fn insert_with_meta<V: Into<Value>>(mut self, value: V, meta: M) -> Self {
self.push(DeltaItem::Insert {
value: value.into(),
meta,
insert: value.into(),
attributes: meta,
});
self
}
pub fn delete_with_meta(mut self, len: usize, meta: M) -> Self {
self.push(DeltaItem::Delete { len, meta });
self.push(DeltaItem::Delete {
delete: len,
attributes: meta,
});
self
}
@ -366,8 +416,8 @@ impl<Value: DeltaValue, M: Meta> Delta<Value, M> {
return self;
}
self.push(DeltaItem::Delete {
len,
meta: M::empty(),
delete: len,
attributes: M::empty(),
});
self
}
@ -378,16 +428,16 @@ impl<Value: DeltaValue, M: Meta> Delta<Value, M> {
}
self.push(DeltaItem::Retain {
len,
meta: M::empty(),
retain: len,
attributes: M::empty(),
});
self
}
pub fn insert<V: Into<Value>>(mut self, value: V) -> Self {
self.push(DeltaItem::Insert {
value: value.into(),
meta: M::empty(),
insert: value.into(),
attributes: M::empty(),
});
self
}
@ -427,8 +477,13 @@ impl<Value: DeltaValue, M: Meta> Delta<Value, M> {
}
Err(inner) => {
self.vec.insert(index - 1, last_op);
self.vec
.insert(index, DeltaItem::Insert { value: inner, meta });
self.vec.insert(
index,
DeltaItem::Insert {
insert: inner,
attributes: meta,
},
);
return false;
}
}
@ -476,6 +531,7 @@ impl<Value: DeltaValue, M: Meta> Delta<Value, M> {
/// Reference: [Quill Delta](https://github.com/quilljs/delta)
// TODO: PERF use &mut self and &other
pub fn compose(self, other: Delta<Value, M>) -> Delta<Value, M> {
// debug_log::debug_dbg!(&self, &other);
let mut this_iter = self.into_op_iter();
let mut other_iter = other.into_op_iter();
let mut ops = vec![];
@ -526,12 +582,12 @@ impl<Value: DeltaValue, M: Meta> Delta<Value, M> {
if concat_rest {
let vec = this_iter.rest();
if vec.is_empty() {
debug_log::debug_dbg!(&delta);
return delta.chop();
break;
}
let rest = Delta { vec };
debug_log::debug_dbg!(&delta, &rest);
return delta.concat(rest).chop();
delta = delta.concat(rest);
break;
}
} else if other_op.is_delete() && this_op.is_retain() {
// 3. this: retain, other: delete
@ -544,7 +600,8 @@ impl<Value: DeltaValue, M: Meta> Delta<Value, M> {
}
}
}
debug_log::debug_dbg!(&delta);
// debug_log::debug_dbg!(&delta);
delta.chop()
}
@ -681,8 +738,8 @@ mod test {
fn delta_push() {
let mut a: Delta<String, ()> = Delta::new().insert("a".to_string());
a.push(DeltaItem::Insert {
value: "b".to_string(),
meta: (),
insert: "b".to_string(),
attributes: (),
});
assert_eq!(a, Delta::new().insert("ab".to_string()));
}

View file

@ -298,7 +298,6 @@ impl DiffCalculator {
}
}
debug_log::debug_dbg!(&ans);
ans.into_iter().map(|x| x.1).collect_vec()
}
}
@ -526,7 +525,11 @@ impl DiffCalculatorTrait for ListDiffCalculator {
let ans = self.tracker.diff(from, to);
// PERF: We may simplify list to avoid these getting
for v in ans.iter() {
if let crate::delta::DeltaItem::Insert { value, meta: _ } = &v {
if let crate::delta::DeltaItem::Insert {
insert: value,
attributes: _,
} = &v
{
for range in &value.0 {
for i in range.0.clone() {
let v = oplog.arena.get_value(i as usize);

View file

@ -21,7 +21,7 @@ pub(crate) use encode_updates::encode_oplog_updates;
pub(crate) const COMPRESS_RLE_THRESHOLD: usize = 20 * 1024;
// TODO: Test this threshold
#[cfg(not(test))]
pub(crate) const UPDATE_ENCODE_THRESHOLD: usize = 512;
pub(crate) const UPDATE_ENCODE_THRESHOLD: usize = 32;
#[cfg(test)]
pub(crate) const UPDATE_ENCODE_THRESHOLD: usize = 16;
pub(crate) const MAGIC_BYTES: [u8; 4] = [0x6c, 0x6f, 0x72, 0x6f];
@ -121,7 +121,6 @@ pub(crate) fn decode_oplog(oplog: &mut OpLog, input: &[u8]) -> Result<(), LoroEr
let mode: EncodeMode = input[0].try_into()?;
let decoded = &input[1..];
debug_log::debug_dbg!(&mode);
match mode {
EncodeMode::Updates => decode_oplog_updates(oplog, decoded),
EncodeMode::Snapshot => unimplemented!(),

View file

@ -1,11 +1,22 @@
pub mod recursive_refactored;
pub mod tree;
use crate::{array_mut_ref, container::richtext::TextStyleInfoFlag, loro::LoroDoc};
use crate::{
array_mut_ref,
container::richtext::TextStyleInfoFlag,
delta::{Delta, DeltaItem, StyleMeta},
loro::LoroDoc,
state::ContainerState,
utils::string_slice::StringSlice,
};
use debug_log::debug_log;
use enum_as_inner::EnumAsInner;
use loro_common::LoroValue;
use std::{fmt::Debug, time::Instant};
use loro_common::{ContainerID, LoroValue};
use std::{
fmt::Debug,
sync::{Arc, Mutex},
time::Instant,
};
use tabled::{TableIteratorExt, Tabled};
const STYLES: [TextStyleInfoFlag; 8] = [
@ -323,7 +334,7 @@ pub fn change_pos_to_char_boundary(pos: &mut usize, len: usize) {
*pos %= len + 1;
}
fn check_synced_refactored(sites: &mut [LoroDoc]) {
fn check_synced(sites: &mut [LoroDoc], texts: &[Arc<Mutex<Delta<StringSlice, StyleMeta>>>]) {
for i in 0..sites.len() - 1 {
for j in i + 1..sites.len() {
debug_log::group!("checking {} with {}", i, j);
@ -346,6 +357,25 @@ fn check_synced_refactored(sites: &mut [LoroDoc]) {
}
check_eq(a, b);
debug_log::group_end!();
// for (x, (site, text)) in sites.iter().zip(texts.iter()).enumerate() {
// if x != i && x != j {
// continue;
// }
// debug_log::group!("Check {}", x);
// let diff = site.get_text("text").with_state_mut(|s| s.to_diff());
// let mut diff = diff.into_text().unwrap();
// compact(&mut diff);
// let mut text = text.lock().unwrap();
// compact(&mut text);
// assert_eq!(
// &diff, &*text,
// "site:{}\nEXPECTED {:#?}\nACTUAL {:#?}",
// x, diff, text
// );
// debug_log::group_end!();
// }
}
}
}
@ -479,11 +509,31 @@ where
}
}
pub fn test_multi_sites_refactored(site_num: u8, actions: &mut [Action]) {
pub fn test_multi_sites(site_num: u8, actions: &mut [Action]) {
let mut sites = Vec::new();
let mut texts = Vec::new();
for i in 0..site_num {
let loro = LoroDoc::new();
let text: Arc<Mutex<Delta<StringSlice, StyleMeta>>> = Arc::new(Mutex::new(Delta::new()));
let text_clone = text.clone();
loro.set_peer_id(i as u64).unwrap();
loro.subscribe(
&ContainerID::new_root("text", loro_common::ContainerType::Text),
Arc::new(move |event| {
if let crate::event::Diff::Text(t) = &event.container.diff {
let mut text = text_clone.lock().unwrap();
debug_log::debug_log!(
"RECEIVE site:{} event:{:#?}\nCURRENT: {:#?}",
i,
t,
&text
);
*text = text.clone().compose(t.clone());
debug_log::debug_log!("new:{:#?}", &text);
}
}),
);
texts.push(text);
sites.push(loro);
}
@ -495,12 +545,87 @@ pub fn test_multi_sites_refactored(site_num: u8, actions: &mut [Action]) {
debug_log::group!("ApplyAction {:?}", &action);
sites.apply_action(action);
debug_log::group_end!();
// for (i, (site, text)) in sites.iter().zip(texts.iter()).enumerate() {
// debug_log::group!("Check {}", i);
// let diff = site.get_text("text").with_state_mut(|s| s.to_diff());
// let mut diff = diff.into_text().unwrap();
// compact(&mut diff);
// let mut text = text.lock().unwrap();
// compact(&mut text);
// assert_eq!(
// &diff, &*text,
// "site:{}\nEXPECTED{:#?}\nACTUAL{:#?}",
// i, diff, text
// );
// debug_log::group_end!();
// }
}
debug_log::group!("CheckSynced");
// println!("{}", actions.table());
check_synced_refactored(&mut sites);
check_synced(&mut sites, &texts);
debug_log::group_end!();
debug_log::group!("CheckTextEvent");
for (i, (site, text)) in sites.iter().zip(texts.iter()).enumerate() {
debug_log::group!("Check {}", i);
let diff = site.get_text("text").with_state_mut(|s| s.to_diff());
let mut diff = diff.into_text().unwrap();
compact(&mut diff);
let mut text = text.lock().unwrap();
compact(&mut text);
assert_eq!(
&diff, &*text,
"site:{}\nEXPECTED{:#?}\nACTUAL{:#?}",
i, diff, text
);
debug_log::group_end!();
}
debug_log::group_end!();
}
pub fn compact(delta: &mut Delta<StringSlice, StyleMeta>) {
let mut ops: Vec<DeltaItem<StringSlice, StyleMeta>> = vec![];
for op in delta.vec.drain(..) {
match (ops.last_mut(), op) {
(
Some(DeltaItem::Retain {
retain: last_retain,
attributes: last_attr,
}),
DeltaItem::Retain { retain, attributes },
) if &attributes == last_attr => {
*last_retain += retain;
}
(
Some(DeltaItem::Insert {
insert: last_insert,
attributes: last_attr,
}),
DeltaItem::Insert { insert, attributes },
) if last_attr == &attributes => {
last_insert.extend(insert.as_str());
}
(
Some(DeltaItem::Delete {
delete: last_delete,
attributes: _,
}),
DeltaItem::Delete {
delete,
attributes: _,
},
) => {
*last_delete += delete;
}
(_, a) => {
ops.push(a);
}
}
}
delta.vec = ops;
}
#[cfg(test)]
@ -510,7 +635,7 @@ mod test {
#[test]
fn fuzz_r1() {
test_multi_sites_refactored(
test_multi_sites(
8,
&mut [
Ins {
@ -540,7 +665,7 @@ mod test {
#[test]
fn fuzz_r() {
test_multi_sites_refactored(
test_multi_sites(
8,
&mut [
Ins {
@ -795,7 +920,7 @@ mod test {
#[test]
fn new_encode() {
test_multi_sites_refactored(
test_multi_sites(
8,
&mut [
Ins {
@ -836,7 +961,7 @@ mod test {
#[test]
fn snapshot() {
test_multi_sites_refactored(
test_multi_sites(
8,
&mut [
Ins {
@ -860,7 +985,7 @@ mod test {
#[test]
fn snapshot_2() {
test_multi_sites_refactored(
test_multi_sites(
8,
&mut [
Ins {
@ -1185,7 +1310,7 @@ mod test {
#[test]
fn checkout() {
test_multi_sites_refactored(
test_multi_sites(
4,
&mut [
Ins {
@ -1210,7 +1335,7 @@ mod test {
#[test]
fn text_fuzz_2() {
test_multi_sites_refactored(
test_multi_sites(
8,
&mut [
Ins {
@ -1274,7 +1399,7 @@ mod test {
#[test]
fn text_fuzz_3() {
test_multi_sites_refactored(
test_multi_sites(
2,
&mut [
Ins {
@ -1398,7 +1523,7 @@ mod test {
#[test]
fn text_fuzz_4() {
test_multi_sites_refactored(
test_multi_sites(
2,
&mut [
Ins {
@ -1821,7 +1946,7 @@ mod test {
#[test]
fn richtext_fuzz_0() {
test_multi_sites_refactored(
test_multi_sites(
5,
&mut [
Ins {
@ -1847,7 +1972,7 @@ mod test {
#[test]
fn richtext_fuzz_1() {
test_multi_sites_refactored(
test_multi_sites(
5,
&mut [
Ins {
@ -1879,7 +2004,7 @@ mod test {
#[test]
fn richtext_fuzz_2() {
test_multi_sites_refactored(
test_multi_sites(
5,
&mut [
Del {
@ -1916,7 +2041,7 @@ mod test {
#[test]
fn richtext_fuzz_3() {
test_multi_sites_refactored(
test_multi_sites(
5,
&mut [Del {
pos: 36310271995488768,
@ -1928,7 +2053,7 @@ mod test {
#[test]
fn fuzz_4() {
test_multi_sites_refactored(
test_multi_sites(
5,
&mut [
Ins {
@ -1952,7 +2077,7 @@ mod test {
#[test]
fn fuzz_5() {
test_multi_sites_refactored(
test_multi_sites(
5,
&mut [
Ins {
@ -1986,7 +2111,7 @@ mod test {
#[test]
fn fuzz_6() {
test_multi_sites_refactored(
test_multi_sites(
5,
&mut [
Ins {
@ -2064,7 +2189,7 @@ mod test {
#[test]
fn fuzz_7() {
test_multi_sites_refactored(
test_multi_sites(
5,
&mut [
Ins {
@ -2093,10 +2218,39 @@ mod test {
)
}
#[test]
fn fuzz_8() {
test_multi_sites(
5,
&mut [
Ins {
content: 0,
pos: 16384000,
site: 0,
},
Mark {
pos: 4503599627370752,
len: 14829735428355981312,
site: 0,
style_key: 0,
},
Ins {
content: 52685,
pos: 3474262130214096333,
site: 128,
},
Mark {
pos: 3607102274975328360,
len: 7812629349709198644,
site: 108,
style_key: 108,
},
],
)
}
#[test]
fn mini_r() {
minify_error(5, vec![], test_multi_sites_refactored, |_, ans| {
ans.to_vec()
})
minify_error(5, vec![], test_multi_sites, |_, ans| ans.to_vec())
}
}

View file

@ -110,10 +110,16 @@ impl Actor {
let mut index = 0;
for item in delta.iter() {
match item {
DeltaItem::Retain { len, meta: _ } => {
DeltaItem::Retain {
retain: len,
attributes: _,
} => {
index += len;
}
DeltaItem::Insert { value, meta: _ } => {
DeltaItem::Insert {
insert: value,
attributes: _,
} => {
let utf8_index = if cfg!(feature = "wasm") {
utf16_to_utf8_index(&text, index).unwrap()
} else {
@ -122,7 +128,7 @@ impl Actor {
text.insert_str(utf8_index, value.as_str());
index += value.len_unicode();
}
DeltaItem::Delete { len, .. } => {
DeltaItem::Delete { delete: len, .. } => {
text.drain(index..index + *len);
}
}
@ -171,16 +177,22 @@ impl Actor {
let mut index = 0;
for item in delta.iter() {
match item {
DeltaItem::Retain { len, meta: _ } => {
DeltaItem::Retain {
retain: len,
attributes: _,
} => {
index += len;
}
DeltaItem::Insert { value, meta: _ } => {
DeltaItem::Insert {
insert: value,
attributes: _,
} => {
for v in value {
list.insert(index, v.clone());
index += 1;
}
}
DeltaItem::Delete { len, .. } => {
DeltaItem::Delete { delete: len, .. } => {
list.drain(index..index + *len);
}
}

View file

@ -10,16 +10,35 @@ use crate::{
op::ListSlice,
state::RichtextState,
txn::EventHint,
utils::utf16::count_utf16_chars,
};
use enum_as_inner::EnumAsInner;
use fxhash::FxHashMap;
use loro_common::{
ContainerID, ContainerType, LoroError, LoroResult, LoroTreeError, LoroValue, TreeID,
};
use serde::{Deserialize, Serialize};
use std::{
borrow::Cow,
sync::{Mutex, Weak},
};
#[derive(Debug, Clone, EnumAsInner, Deserialize, Serialize)]
#[serde(untagged)]
pub enum TextDelta {
Retain {
retain: usize,
attributes: Option<FxHashMap<String, LoroValue>>,
},
Insert {
insert: String,
attributes: Option<FxHashMap<String, LoroValue>>,
},
Delete {
delete: usize,
},
}
#[derive(Clone)]
pub struct TextHandler {
txn: Weak<Mutex<Option<Transaction>>>,
@ -215,7 +234,7 @@ impl TextHandler {
})
}
pub fn with_state(&self, f: impl FnOnce(&RichtextState)) {
pub fn with_state<R>(&self, f: impl FnOnce(&RichtextState) -> R) -> R {
self.state
.upgrade()
.unwrap()
@ -223,7 +242,19 @@ impl TextHandler {
.unwrap()
.with_state(self.container_idx, |state| {
let state = state.as_richtext_state().unwrap();
f(state);
f(state)
})
}
pub fn with_state_mut<R>(&self, f: impl FnOnce(&mut RichtextState) -> R) -> R {
self.state
.upgrade()
.unwrap()
.lock()
.unwrap()
.with_state_mut(self.container_idx, |state| {
let state = state.as_richtext_state_mut().unwrap();
f(state)
})
}
@ -326,9 +357,10 @@ impl TextHandler {
});
debug_assert_eq!(ranges.iter().map(|x| x.len()).sum::<usize>(), len);
let mut offset = 0;
let mut end = (pos + len) as isize;
for range in ranges.iter().rev() {
let len = (range.end - range.start) as isize;
let start = end - len;
txn.apply_local_op(
self.container_idx,
crate::op::RawOpContent::List(ListOp::Delete(DeleteSpan {
@ -336,12 +368,12 @@ impl TextHandler {
signed_len: len,
})),
EventHint::DeleteText(DeleteSpan {
pos: pos as isize + offset,
pos: start,
signed_len: len,
}),
&self.state,
)?;
offset += len;
end = start;
}
debug_log::group_end!();
@ -435,6 +467,63 @@ impl TextHandler {
Ok(())
}
pub fn apply_delta_(&self, delta: &[TextDelta]) -> LoroResult<()> {
with_txn(&self.txn, |txn| self.apply_delta(txn, delta))
}
pub fn apply_delta(&self, txn: &mut Transaction, delta: &[TextDelta]) -> LoroResult<()> {
let mut index = 0;
let mut marks = Vec::new();
for d in delta {
match d {
TextDelta::Insert { insert, attributes } => {
let end = index + event_len(insert.as_str());
self.insert(txn, index, insert.as_str())?;
match attributes {
Some(attr) if !attr.is_empty() => {
for (key, value) in attr {
marks.push((index, end, key.as_str(), value.clone()));
}
}
_ => {}
}
index = end;
}
TextDelta::Delete { delete } => {
self.delete(txn, index, *delete)?;
}
TextDelta::Retain { attributes, retain } => {
let end = index + *retain;
match attributes {
Some(attr) if !attr.is_empty() => {
for (key, value) in attr {
marks.push((index, end, key.as_str(), value.clone()));
}
}
_ => {}
}
index = end;
}
}
}
for (start, end, key, value) in marks {
// FIXME: allow users to set a config table to store the flag, so that we can use it directly
self.mark(txn, start, end, key, value, TextStyleInfoFlag::BOLD)?;
}
Ok(())
}
}
fn event_len(s: &str) -> usize {
if cfg!(feature = "wasm") {
count_utf16_chars(s.as_bytes())
} else {
s.chars().count()
}
}
impl ListHandler {
@ -1069,8 +1158,11 @@ mod test {
use crate::container::richtext::TextStyleInfoFlag;
use crate::loro::LoroDoc;
use crate::version::Frontiers;
use crate::ToJson;
use crate::{fx_map, ToJson};
use loro_common::ID;
use serde_json::json;
use super::TextDelta;
#[test]
fn test() {
@ -1293,4 +1385,38 @@ mod test {
loro2.import(&loro.export_from(&loro2.oplog_vv())).unwrap();
assert_eq!(loro.get_deep_value(), loro2.get_deep_value());
}
#[test]
fn richtext_apply_delta() {
let loro = LoroDoc::new_auto_commit();
let text = loro.get_text("text");
text.apply_delta_(&[TextDelta::Insert {
insert: "Hello World!".into(),
attributes: None,
}])
.unwrap();
dbg!(text.get_richtext_value());
text.apply_delta_(&[
TextDelta::Retain {
retain: 6,
attributes: Some(fx_map!("italic".into() => loro_common::LoroValue::Bool(true))),
},
TextDelta::Insert {
insert: "New ".into(),
attributes: Some(fx_map!("bold".into() => loro_common::LoroValue::Bool(true))),
},
])
.unwrap();
dbg!(text.get_richtext_value());
loro.commit_then_renew();
assert_eq!(
text.get_richtext_value().to_json_value(),
json!([
{"insert": "Hello ", "attributes": {"italic": true}},
{"insert": "New ", "attributes": {"bold": true}},
{"insert": "World!"}
])
)
}
}

View file

@ -715,7 +715,7 @@ impl OpLog {
self.changes.values().map(|x| x.len()).sum()
}
pub fn diagnose_size(&self) {
pub fn diagnose_size(&self) -> SizeInfo {
let mut total_changes = 0;
let mut total_ops = 0;
let mut total_atom_ops = 0;
@ -732,9 +732,23 @@ impl OpLog {
println!("total ops: {}", total_ops);
println!("total atom ops: {}", total_atom_ops);
println!("total dag node: {}", total_dag_node);
SizeInfo {
total_changes,
total_ops,
total_atom_ops,
total_dag_node,
}
}
}
#[derive(Debug)]
pub struct SizeInfo {
pub total_changes: usize,
pub total_ops: usize,
pub total_atom_ops: usize,
pub total_dag_node: usize,
}
impl Default for OpLog {
fn default() -> Self {
Self::new()

View file

@ -297,11 +297,11 @@ impl ContainerState for ListState {
let mut index = 0;
for span in delta.iter() {
match span {
crate::delta::DeltaItem::Retain { len, .. } => {
crate::delta::DeltaItem::Retain { retain: len, .. } => {
index += len;
ans = ans.retain(*len);
}
crate::delta::DeltaItem::Insert { value, .. } => {
crate::delta::DeltaItem::Insert { insert: value, .. } => {
let mut arr = Vec::new();
for slices in value.0.iter() {
for i in slices.0.start..slices.0.end {
@ -319,7 +319,7 @@ impl ContainerState for ListState {
self.insert_batch(index, arr);
index += len;
}
crate::delta::DeltaItem::Delete { len, .. } => {
crate::delta::DeltaItem::Delete { delete: len, .. } => {
self.delete_range(index..index + len);
ans = ans.delete(*len);
}
@ -335,10 +335,10 @@ impl ContainerState for ListState {
let mut index = 0;
for span in delta.iter() {
match span {
crate::delta::DeltaItem::Retain { len, .. } => {
crate::delta::DeltaItem::Retain { retain: len, .. } => {
index += len;
}
crate::delta::DeltaItem::Insert { value, .. } => {
crate::delta::DeltaItem::Insert { insert: value, .. } => {
let mut arr = Vec::new();
for slices in value.0.iter() {
for i in slices.0.start..slices.0.end {
@ -356,7 +356,7 @@ impl ContainerState for ListState {
self.insert_batch(index, arr);
index += len;
}
crate::delta::DeltaItem::Delete { len, .. } => {
crate::delta::DeltaItem::Delete { delete: len, .. } => {
self.delete_range(index..index + len);
}
}

View file

@ -137,6 +137,8 @@ impl ContainerState for RichtextState {
unreachable!()
};
debug_log::group!("apply_diff_and_convert");
debug_log::debug_dbg!(&richtext);
// PERF: compose delta
let mut ans: Delta<StringSlice, StyleMeta> = Delta::new();
let mut style_delta: Delta<StringSlice, StyleMeta> = Delta::new();
@ -150,10 +152,10 @@ impl ContainerState for RichtextState {
let mut event_index = 0;
for span in richtext.vec.iter() {
match span {
crate::delta::DeltaItem::Retain { len, .. } => {
crate::delta::DeltaItem::Retain { retain: len, .. } => {
entity_index += len;
}
crate::delta::DeltaItem::Insert { value, .. } => {
crate::delta::DeltaItem::Insert { insert: value, .. } => {
match value {
RichtextStateChunk::Text { unicode_len, text } => {
let (pos, styles) = self.state.get_mut().insert_elem_at_entity_index(
@ -166,17 +168,7 @@ impl ContainerState for RichtextState {
let insert_styles = styles.clone().into();
if pos > event_index {
let mut new_len = 0;
for (len, styles) in self
.state
.get_mut()
.iter_styles_in_event_index_range(event_index..pos)
{
new_len += len;
ans = ans.retain_with_meta(len, styles.clone().into());
}
assert_eq!(new_len, pos - event_index);
ans = ans.retain(pos - event_index);
}
event_index = pos
+ (if cfg!(feature = "wasm") {
@ -188,7 +180,7 @@ impl ContainerState for RichtextState {
.insert_with_meta(StringSlice::from(text.clone()), insert_styles);
}
RichtextStateChunk::Style { anchor_type, style } => {
let (event_index, _) =
let (new_event_index, _) =
self.state.get_mut().insert_elem_at_entity_index(
entity_index,
RichtextStateChunk::Style {
@ -197,12 +189,18 @@ impl ContainerState for RichtextState {
},
);
if new_event_index > event_index {
ans = ans.retain(new_event_index - event_index);
// inserting style anchor will not affect event_index's positions
event_index = new_event_index;
}
if *anchor_type == AnchorType::Start {
style_starts.insert(
style.clone(),
Pos {
entity_index,
event_index,
event_index: new_event_index,
},
);
} else {
@ -230,28 +228,24 @@ impl ContainerState for RichtextState {
);
let delta: Delta<StringSlice, _> = Delta::new()
.retain(start_event_index)
.retain_with_meta(event_index - start_event_index, meta);
dbg!(&delta);
.retain_with_meta(new_event_index - start_event_index, meta);
debug_log::debug_dbg!(&delta);
style_delta = style_delta.compose(delta);
}
}
}
entity_index += value.rle_len();
}
crate::delta::DeltaItem::Delete { len, meta: _ } => {
crate::delta::DeltaItem::Delete {
delete: len,
attributes: _,
} => {
let (start, end) =
self.state
.get_mut()
.drain_by_entity_index(entity_index, *len, |_| {});
if start > event_index {
for (len, styles) in self
.state
.get_mut()
.iter_styles_in_event_index_range(event_index..start)
{
ans = ans.retain_with_meta(len, styles.clone().into());
}
ans = ans.retain(start - event_index);
event_index = start;
}
@ -261,7 +255,11 @@ impl ContainerState for RichtextState {
}
debug_assert!(style_starts.is_empty(), "Styles should always be paired");
Diff::Text(ans.compose(style_delta))
debug_log::debug_dbg!(&ans, &style_delta);
let ans = ans.compose(style_delta);
debug_log::debug_dbg!(&ans);
debug_log::group_end!();
Diff::Text(ans)
}
fn apply_diff(&mut self, diff: InternalDiff, _arena: &SharedArena) {
@ -273,10 +271,16 @@ impl ContainerState for RichtextState {
let mut entity_index = 0;
for span in richtext.vec.iter() {
match span {
crate::delta::DeltaItem::Retain { len, meta: _ } => {
crate::delta::DeltaItem::Retain {
retain: len,
attributes: _,
} => {
entity_index += len;
}
crate::delta::DeltaItem::Insert { value, meta: _ } => {
crate::delta::DeltaItem::Insert {
insert: value,
attributes: _,
} => {
match value {
RichtextStateChunk::Text { unicode_len, text } => {
self.state.get_mut().insert_elem_at_entity_index(
@ -311,7 +315,10 @@ impl ContainerState for RichtextState {
}
entity_index += value.rle_len();
}
crate::delta::DeltaItem::Delete { len, meta: _ } => {
crate::delta::DeltaItem::Delete {
delete: len,
attributes: _,
} => {
self.state
.get_mut()
.drain_by_entity_index(entity_index, *len, |_| {});
@ -395,8 +402,8 @@ impl ContainerState for RichtextState {
let mut delta = crate::delta::Delta::new();
for span in self.state.get_mut().iter() {
delta.vec.push(DeltaItem::Insert {
value: span.text,
meta: span.attributes,
insert: span.text,
attributes: span.attributes,
})
}

View file

@ -96,6 +96,19 @@ impl StringSlice {
pub fn is_empty(&self) -> bool {
self.bytes().is_empty()
}
pub fn extend(&mut self, s: &str) {
match &mut self.bytes {
Variant::BytesSlice(_) => {
*self = Self {
bytes: Variant::Owned(format!("{}{}", self.as_str(), s)),
}
}
Variant::Owned(v) => {
v.push_str(s);
}
}
}
}
impl std::fmt::Display for StringSlice {

View file

@ -49,7 +49,10 @@ impl ToJson for LoroValue {
impl ToJson for DeltaItem<StringSlice, StyleMeta> {
fn to_json_value(&self) -> serde_json::Value {
match self {
DeltaItem::Retain { len, meta } => {
DeltaItem::Retain {
retain: len,
attributes: meta,
} => {
let mut map = serde_json::Map::new();
map.insert("retain".into(), serde_json::to_value(len).unwrap());
if !meta.is_empty() {
@ -57,7 +60,10 @@ impl ToJson for DeltaItem<StringSlice, StyleMeta> {
}
serde_json::Value::Object(map)
}
DeltaItem::Insert { value, meta } => {
DeltaItem::Insert {
insert: value,
attributes: meta,
} => {
let mut map = serde_json::Map::new();
map.insert("insert".into(), serde_json::to_value(value).unwrap());
if !meta.is_empty() {
@ -65,7 +71,10 @@ impl ToJson for DeltaItem<StringSlice, StyleMeta> {
}
serde_json::Value::Object(map)
}
DeltaItem::Delete { len, meta: _ } => {
DeltaItem::Delete {
delete: len,
attributes: _,
} => {
let mut map = serde_json::Map::new();
map.insert("delete".into(), serde_json::to_value(len).unwrap());
serde_json::Value::Object(map)
@ -83,8 +92,8 @@ impl ToJson for DeltaItem<StringSlice, StyleMeta> {
StyleMeta::default()
};
DeltaItem::Retain {
len: len as usize,
meta,
retain: len as usize,
attributes: meta,
}
} else if map.contains_key("insert") {
let value = map["insert"].as_str().unwrap().to_string().into();
@ -93,12 +102,15 @@ impl ToJson for DeltaItem<StringSlice, StyleMeta> {
} else {
StyleMeta::default()
};
DeltaItem::Insert { value, meta }
DeltaItem::Insert {
insert: value,
attributes: meta,
}
} else if map.contains_key("delete") {
let len = map["delete"].as_u64().unwrap();
DeltaItem::Delete {
len: len as usize,
meta: Default::default(),
delete: len as usize,
attributes: Default::default(),
}
} else {
panic!("Invalid delta item: {}", s);
@ -148,14 +160,14 @@ impl ApplyDiff for LoroValue {
let mut index = 0;
for delta_item in delta.iter() {
match delta_item {
DeltaItem::Retain { len, .. } => {
DeltaItem::Retain { retain: len, .. } => {
index += len;
}
DeltaItem::Insert { value, .. } => {
DeltaItem::Insert { insert: value, .. } => {
s.insert_str(index, value.as_str());
index += value.len_bytes();
}
DeltaItem::Delete { len, .. } => {
DeltaItem::Delete { delete: len, .. } => {
s.drain(index..index + len);
}
}
@ -170,17 +182,17 @@ impl ApplyDiff for LoroValue {
let mut index = 0;
for delta_item in delta.iter() {
match delta_item {
DeltaItem::Retain { len, .. } => {
DeltaItem::Retain { retain: len, .. } => {
index += len;
}
DeltaItem::Insert { value, .. } => {
DeltaItem::Insert { insert: value, .. } => {
value.iter().for_each(|v| {
let value = unresolved_to_collection(v);
seq.insert(index, value);
index += 1;
});
}
DeltaItem::Delete { len, .. } => {
DeltaItem::Delete { delete: len, .. } => {
seq.drain(index..index + len);
}
}
@ -578,7 +590,10 @@ pub mod wasm {
fn from(value: DeltaItem<StringSlice, StyleMeta>) -> Self {
let obj = Object::new();
match value {
DeltaItem::Retain { len, meta } => {
DeltaItem::Retain {
retain: len,
attributes: meta,
} => {
js_sys::Reflect::set(
&obj,
&JsValue::from_str("retain"),
@ -594,7 +609,10 @@ pub mod wasm {
.unwrap();
}
}
DeltaItem::Insert { value, meta } => {
DeltaItem::Insert {
insert: value,
attributes: meta,
} => {
js_sys::Reflect::set(
&obj,
&JsValue::from_str("insert"),
@ -610,7 +628,10 @@ pub mod wasm {
.unwrap();
}
}
DeltaItem::Delete { len, meta: _ } => {
DeltaItem::Delete {
delete: len,
attributes: _,
} => {
js_sys::Reflect::set(
&obj,
&JsValue::from_str("delete"),
@ -649,7 +670,7 @@ pub mod wasm {
fn from(value: DeltaItem<Vec<LoroValue>, ()>) -> Self {
let obj = Object::new();
match value {
DeltaItem::Retain { len, .. } => {
DeltaItem::Retain { retain: len, .. } => {
js_sys::Reflect::set(
&obj,
&JsValue::from_str("retain"),
@ -657,7 +678,7 @@ pub mod wasm {
)
.unwrap();
}
DeltaItem::Insert { value, .. } => {
DeltaItem::Insert { insert: value, .. } => {
let arr = Array::new_with_length(value.len() as u32);
for (i, v) in value.into_iter().enumerate() {
arr.set(i as u32, convert(v));
@ -670,7 +691,7 @@ pub mod wasm {
)
.unwrap();
}
DeltaItem::Delete { len, .. } => {
DeltaItem::Delete { delete: len, .. } => {
js_sys::Reflect::set(
&obj,
&JsValue::from_str("delete"),

View file

@ -5,7 +5,7 @@ use loro_internal::{
ContainerID,
},
event::{Diff, Index},
handler::{ListHandler, MapHandler, TextHandler, TreeHandler},
handler::{ListHandler, MapHandler, TextDelta, TextHandler, TreeHandler},
id::{Counter, TreeID, ID},
obs::SubID,
version::Frontiers,
@ -50,6 +50,8 @@ impl Deref for Loro {
extern "C" {
#[wasm_bindgen(typescript_type = "ContainerID")]
pub type JsContainerID;
#[wasm_bindgen(typescript_type = "ContainerID | string")]
pub type JsIntoContainerID;
#[wasm_bindgen(typescript_type = "Transaction | Loro")]
pub type JsTransaction;
#[wasm_bindgen(typescript_type = "string | undefined")]
@ -66,6 +68,8 @@ extern "C" {
pub type JsTreeID;
#[wasm_bindgen(typescript_type = "Delta<string>[]")]
pub type JsStringDelta;
#[wasm_bindgen(typescript_type = "Map<bigint, number>")]
pub type JsVersionVectorMap;
}
mod observer {
@ -130,6 +134,36 @@ fn frontiers_to_ids(frontiers: &Frontiers) -> Vec<JsID> {
ans
}
fn js_value_to_container_id(
cid: &JsIntoContainerID,
kind: ContainerType,
) -> Result<ContainerID, JsValue> {
if !cid.is_string() {
return Err(JsValue::from_str("ContainerID must be a string"));
}
let s = cid.as_string().unwrap();
let cid = ContainerID::try_from(s.as_str())
.unwrap_or_else(|_| ContainerID::new_root(s.as_str(), kind));
Ok(cid)
}
fn js_value_to_version(version: &JsValue) -> Result<VersionVector, JsValue> {
let version: Option<Vec<u8>> = if version.is_null() || version.is_undefined() {
None
} else {
let arr: Uint8Array = Uint8Array::new(version);
Some(arr.to_vec())
};
let vv = match version {
Some(x) => VersionVector::decode(&x)?,
None => Default::default(),
};
Ok(vv)
}
#[wasm_bindgen]
impl Loro {
#[wasm_bindgen(constructor)]
@ -145,20 +179,57 @@ impl Loro {
Ok(Loro(doc))
}
/// Attach the document state to the latest known version.
///
/// > The document becomes detached during a `checkout` operation.
/// > Being `detached` implies that the `DocState` is not synchronized with the latest version of the `OpLog`.
/// > In a detached state, the document is not editable, and any `import` operations will be
/// > recorded in the `OpLog` without being applied to the `DocState`.
///
/// This method has the same effect as invoking `checkout_to_latest`.
pub fn attach(&mut self) {
self.0.attach();
}
pub fn checkout(&mut self, frontiers: Vec<JsID>) -> JsResult<()> {
self.0.checkout(&ids_to_frontiers(frontiers)?)?;
Ok(())
/// `detached` indicates that the `DocState` is not synchronized with the latest version of `OpLog`.
///
/// > The document becomes detached during a `checkout` operation.
/// > Being `detached` implies that the `DocState` is not synchronized with the latest version of the `OpLog`.
/// > In a detached state, the document is not editable, and any `import` operations will be
/// > recorded in the `OpLog` without being applied to the `DocState`.
///
/// When `detached`, the document is not editable.
pub fn is_detached(&self) -> bool {
self.0.is_detached()
}
/// Checkout the `DocState` to the lastest version of `OpLog`.
///
/// > The document becomes detached during a `checkout` operation.
/// > Being `detached` implies that the `DocState` is not synchronized with the latest version of the `OpLog`.
/// > In a detached state, the document is not editable, and any `import` operations will be
/// > recorded in the `OpLog` without being applied to the `DocState`.
///
/// This has the same effect as `attach`.
pub fn checkout_to_latest(&mut self) -> JsResult<()> {
self.0.checkout_to_latest();
Ok(())
}
/// Checkout the `DocState` to a specific version.
///
/// > The document becomes detached during a `checkout` operation.
/// > Being `detached` implies that the `DocState` is not synchronized with the latest version of the `OpLog`.
/// > In a detached state, the document is not editable, and any `import` operations will be
/// > recorded in the `OpLog` without being applied to the `DocState`.
///
/// You should call `attach` to attach the `DocState` to the lastest version of `OpLog`.
pub fn checkout(&mut self, frontiers: Vec<JsID>) -> JsResult<()> {
self.0.checkout(&ids_to_frontiers(frontiers)?)?;
Ok(())
}
/// Peer ID of the current writer.
#[wasm_bindgen(js_name = "peerId", method, getter)]
pub fn peer_id(&self) -> u64 {
self.0.peer_id()
@ -170,10 +241,14 @@ impl Loro {
format!("{:X}", self.0.peer_id())
}
#[wasm_bindgen(js_name = "getText")]
pub fn get_text(&self, name: &str) -> JsResult<LoroText> {
let text = self.0.get_text(name);
Ok(LoroText(text))
/// Set the peer ID of the current writer.
///
/// Note: use it with caution. You need to make sure there is not chance that two peers
/// have the same peer ID.
#[wasm_bindgen(js_name = "setPeerId", method)]
pub fn set_peer_id(&self, id: u64) -> JsResult<()> {
self.0.set_peer_id(id)?;
Ok(())
}
/// Commit the cumulative auto commit transaction.
@ -181,21 +256,35 @@ impl Loro {
self.0.commit_with(origin.map(|x| x.into()), None, true);
}
#[wasm_bindgen(js_name = "getText")]
pub fn get_text(&self, cid: &JsIntoContainerID) -> JsResult<LoroText> {
let text = self
.0
.get_text(js_value_to_container_id(cid, ContainerType::Text)?);
Ok(LoroText(text))
}
#[wasm_bindgen(js_name = "getMap")]
pub fn get_map(&self, name: &str) -> JsResult<LoroMap> {
let map = self.0.get_map(name);
pub fn get_map(&self, cid: &JsIntoContainerID) -> JsResult<LoroMap> {
let map = self
.0
.get_map(js_value_to_container_id(cid, ContainerType::Map)?);
Ok(LoroMap(map))
}
#[wasm_bindgen(js_name = "getList")]
pub fn get_list(&self, name: &str) -> JsResult<LoroList> {
let list = self.0.get_list(name);
pub fn get_list(&self, cid: &JsIntoContainerID) -> JsResult<LoroList> {
let list = self
.0
.get_list(js_value_to_container_id(cid, ContainerType::List)?);
Ok(LoroList(list))
}
#[wasm_bindgen(js_name = "getTree")]
pub fn get_tree(&self, name: &str) -> JsResult<LoroTree> {
let tree = self.0.get_tree(name);
pub fn get_tree(&self, cid: &JsIntoContainerID) -> JsResult<LoroTree> {
let tree = self
.0
.get_tree(js_value_to_container_id(cid, ContainerType::Tree)?);
Ok(LoroTree(tree))
}
@ -223,19 +312,54 @@ impl Loro {
})
}
/// Get the encoded version vector of the current document.
///
/// If you checkout to a specific version, the version vector will change.
#[inline(always)]
pub fn version(&self) -> Vec<u8> {
self.0.oplog_vv().encode()
self.0.state_vv().encode()
}
/// Get the encoded version vector of the lastest verison in OpLog.
///
/// If you checkout to a specific version, the version vector will not change.
#[inline(always)]
pub fn oplog_version(&self) -> Vec<u8> {
self.0.state_vv().encode()
}
/// Get the frontiers of the current document.
///
/// If you checkout to a specific version, this value will change.
#[inline]
pub fn frontiers(&self) -> Vec<JsID> {
frontiers_to_ids(&self.0.state_frontiers())
}
/// Get the frontiers of the lastest version in OpLog.
///
/// If you checkout to a specific version, this value will not change.
#[inline(always)]
pub fn oplog_frontiers(&self) -> Vec<JsID> {
frontiers_to_ids(&self.0.oplog_frontiers())
}
/// - -1: self's version is less than frontiers or is parallel to target
/// - 0: self's version equals to frontiers
/// - 1: self's version is greater than frontiers
/// Compare the version of the OpLog with the specified frontiers.
///
/// This method is useful to compare the version by only a small amount of data.
///
/// This method returns an integer indicating the relationship between the version of the OpLog (referred to as 'self')
/// and the provided 'frontiers' parameter:
///
/// - -1: The version of 'self' is either less than 'frontiers' or is non-comparable (parallel) to 'frontiers',
/// indicating that it is not definitively less than 'frontiers'.
/// - 0: The version of 'self' is equal to 'frontiers'.
/// - 1: The version of 'self' is greater than 'frontiers'.
///
/// # Internal
///
/// Frontiers cannot be compared without the history of the OpLog.
///
#[inline]
#[wasm_bindgen(js_name = "cmpFrontiers")]
pub fn cmp_frontiers(&self, frontiers: Vec<JsID>) -> JsResult<i32> {
@ -254,18 +378,8 @@ impl Loro {
#[wasm_bindgen(skip_typescript, js_name = "exportFrom")]
pub fn export_from(&self, version: &JsValue) -> JsResult<Vec<u8>> {
let version: Option<Vec<u8>> = if version.is_null() || version.is_undefined() {
None
} else {
let arr: Uint8Array = Uint8Array::new(version);
Some(arr.to_vec())
};
let vv = match version {
Some(x) => VersionVector::decode(&x)?,
None => Default::default(),
};
// `version` may be null or undefined
let vv = js_value_to_version(version)?;
Ok(self.0.export_from(&vv))
}
@ -274,6 +388,9 @@ impl Loro {
Ok(())
}
/// Import a batch of updates.
///
/// It's more efficient than importing updates one by one.
#[wasm_bindgen(js_name = "importUpdateBatch")]
pub fn import_update_batch(&mut self, data: Array) -> JsResult<()> {
let data = data
@ -309,6 +426,12 @@ impl Loro {
pub fn unsubscribe(&self, subscription: u32) {
self.0.unsubscribe(SubID::from_u32(subscription))
}
#[wasm_bindgen(js_name = "debugHistory")]
pub fn debug_history(&self) {
let oplog = self.0.oplog().lock().unwrap();
console_log!("{:#?}", oplog.diagnose_size());
}
}
#[allow(unused)]
@ -546,6 +669,14 @@ impl LoroText {
loro.0.unsubscribe(SubID::from_u32(subscription));
Ok(())
}
#[wasm_bindgen(js_name = "applyDelta")]
pub fn apply_delta(&self, delta: JsValue) -> JsResult<()> {
let delta: Vec<TextDelta> = serde_wasm_bindgen::from_value(delta)?;
console_log!("apply_delta {:?}", delta);
self.0.apply_delta_(&delta)?;
Ok(())
}
}
#[wasm_bindgen]
@ -799,6 +930,33 @@ impl LoroTree {
}
}
/// Convert a encoded version vector to a readable js Map.
///
/// # Example
///
/// ```js
/// const loro = new Loro();
/// loro.setPeerId('100');
/// loro.getText("t").insert(0, 'a');
/// loro.commit();
/// const version = loro.getVersion();
/// const readableVersion = convertVersionToReadableObj(version);
/// console.log(readableVersion); // Map(1) { 100n => 1 }
/// ```
#[wasm_bindgen(js_name = "convertVersionToReadableMap")]
pub fn convert_version_to_readable_map(version: &[u8]) -> Result<JsVersionVectorMap, JsValue> {
let version_vector = VersionVector::decode(version)?;
let map = js_sys::Map::new();
for (k, v) in version_vector.iter() {
let k = js_sys::BigInt::from(*k);
let v = JsValue::from(*v);
map.set(&k.to_owned(), &v);
}
let map: JsValue = map.into();
Ok(JsVersionVectorMap::from(map))
}
#[wasm_bindgen(typescript_custom_section)]
const TYPES: &'static str = r#"
export type ContainerType = "Text" | "Map" | "List"| "Tree";
@ -814,18 +972,19 @@ interface Loro {
export type Delta<T> =
| {
insert: T;
attributes?: { [key in string]: {} },
attributes?: { [key in string]: {} };
retain?: undefined;
delete?: undefined;
}
| {
delete: number;
attributes?: undefined;
retain?: undefined;
insert?: undefined;
}
| {
retain: number;
attributes?: { [key in string]: {} },
attributes?: { [key in string]: {} };
delete?: undefined;
insert?: undefined;
};

24
examples/loro-quill/.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View file

@ -0,0 +1,3 @@
{
"type": "module"
}

View file

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}

View file

@ -0,0 +1,18 @@
# Vue 3 + TypeScript + Vite
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
## Type Support For `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
1. Disable the built-in TypeScript Extension
1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.

View file

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/Loro.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Loro crdt-richtext Quill Demo</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View file

@ -0,0 +1,26 @@
{
"name": "loro-quill",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --force",
"build": "vue-tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"loro-crdt": "workspace:*",
"is-equal": "^1.6.4",
"quill": "^1.3.7",
"vue": "^3.2.47"
},
"devDependencies": {
"@types/quill": "^1.3.7",
"@vitejs/plugin-vue": "^4.1.0",
"typescript": "^5.0.2",
"vite": "^4.3.2",
"vite-plugin-top-level-await": "^1.3.0",
"vite-plugin-wasm": "^3.2.2",
"vue-tsc": "^1.4.2"
}
}

View file

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Created with Vectornator (http://vectornator.io/) -->
<svg height="100%" stroke-miterlimit="10" style="fill-rule:nonzero;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;" version="1.1" viewBox="0 0 1080 1016.1" width="100%" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:vectornator="http://vectornator.io" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<linearGradient gradientTransform="matrix(0.913158 0 0 0.913158 49.811 45.1459)" gradientUnits="userSpaceOnUse" id="LinearGradient" x1="150.613" x2="415.258" y1="270.181" y2="397.943">
<stop offset="0" stop-color="#b8f6ff"/>
<stop offset="0.15" stop-color="#aaf0fa"/>
<stop offset="0.4331" stop-color="#85dfed"/>
<stop offset="0.6323" stop-color="#67d2e3"/>
<stop offset="1" stop-color="#67d2e3"/>
</linearGradient>
</defs>
<g id="Untitled" vectornator:layerName="Untitled">
<g opacity="1">
<g opacity="1">
<clipPath id="ClipPath">
<path d="M1020.6 850.501C982.978 784.845 950.378 729.142 924.079 684.763C948.826 628.512 962.615 566.235 962.615 500.762C962.615 248.547 758.158 44 505.853 44C253.547 44 49 248.547 49 500.853C49 746.401 242.681 946.657 485.581 957.249C492.247 957.523 499.004 957.706 505.762 957.706C584.567 957.706 658.716 937.799 723.367 902.642L771.673 937.982C802.081 960.171 837.968 971.951 875.499 971.951C931.567 971.951 984.713 944.922 1017.77 899.72L1035.12 875.978L1020.6 850.501Z"/>
</clipPath>
<g clip-path="url(#ClipPath)">
<path d="M962.615 500.853C962.615 620.385 916.683 729.234 841.53 810.687C758.067 901.09 638.535 957.706 505.853 957.706C499.096 957.706 492.338 957.523 485.672 957.249C242.681 946.657 49 746.401 49 500.853C49 248.547 253.547 44 505.853 44C758.158 44 962.615 248.547 962.615 500.853Z" fill="#000000" fill-rule="nonzero" opacity="1" stroke="none"/>
</g>
</g>
<g opacity="1">
<clipPath id="ClipPath_2">
<path d="M1021.41 851.647C983.789 785.991 951.189 730.288 924.89 685.909C949.637 629.658 963.426 567.381 963.426 501.908C963.426 249.693 758.969 45.1459 506.664 45.1459C254.358 45.1459 49.811 249.693 49.811 501.999C49.811 747.547 243.492 947.803 486.392 958.395C493.058 958.669 499.815 958.852 506.573 958.852C585.378 958.852 659.527 938.945 724.178 903.788L772.484 939.128C802.892 961.317 838.78 973.097 876.31 973.097C932.378 973.097 985.524 946.068 1018.58 900.866L1035.93 877.124L1021.41 851.647Z"/>
</clipPath>
<g clip-path="url(#ClipPath_2)">
<path d="M839.967 506.199L839.967 814.025C757.782 903.149 639.985 958.943 509.129 958.943C502.463 958.943 495.432 958.76 488.857 958.486L489.223 210.336L544.103 210.336C707.559 210.336 839.967 342.835 839.967 506.199Z" fill="#ffffff" fill-rule="nonzero" opacity="1" stroke="none"/>
</g>
</g>
<g opacity="1">
<clipPath id="ClipPath_3">
<path d="M1021.41 851.647C983.789 785.991 951.189 730.288 924.89 685.909C949.637 629.658 963.426 567.381 963.426 501.908C963.426 249.693 758.969 45.1459 506.664 45.1459C254.358 45.1459 49.811 249.693 49.811 501.999C49.811 747.547 243.492 947.803 486.392 958.395C493.058 958.669 499.815 958.852 506.573 958.852C585.378 958.852 659.527 938.945 724.178 903.788L772.484 939.128C802.892 961.317 838.78 973.097 876.31 973.097C932.378 973.097 985.524 946.068 1018.58 900.866L1035.93 877.124L1021.41 851.647Z"/>
</clipPath>
<g clip-path="url(#ClipPath_3)">
<path d="M568.393 378.083C568.393 335.266 603.103 300.556 645.921 300.556C688.738 300.556 723.448 335.266 723.448 378.083C723.448 420.9 688.738 455.61 645.921 455.61C603.103 455.61 568.393 420.9 568.393 378.083Z" fill="#000000" fill-rule="nonzero" opacity="1" stroke="none"/>
</g>
</g>
<g opacity="1">
<clipPath id="ClipPath_4">
<path d="M1021.41 851.647C983.789 785.991 951.189 730.288 924.89 685.909C949.637 629.658 963.426 567.381 963.426 501.908C963.426 249.693 758.969 45.1459 506.664 45.1459C254.358 45.1459 49.811 249.693 49.811 501.999C49.811 747.547 243.492 947.803 486.392 958.395C493.058 958.669 499.815 958.852 506.573 958.852C585.378 958.852 659.527 938.945 724.178 903.788L772.484 939.128C802.892 961.317 838.78 973.097 876.31 973.097C932.378 973.097 985.524 946.068 1018.58 900.866L1035.93 877.124L1021.41 851.647Z"/>
</clipPath>
<g clip-path="url(#ClipPath_4)">
<path d="M740.067 575.782C764.996 575.782 790.2 583.452 811.933 599.25C811.933 599.25 872.018 695.588 969.178 865.253L969.178 865.253C945.345 897.944 908.271 915.203 870.74 915.203C845.811 915.203 820.608 907.532 798.875 891.735L668.019 796.036C613.686 756.313 601.815 680.065 641.537 625.732L641.537 625.732C665.462 593.132 702.445 575.782 740.067 575.782M736.049 517.979C679.981 517.979 626.836 545.009 593.779 590.21C566.019 628.197 554.696 674.677 561.91 721.248C569.124 767.728 594.053 808.637 632.041 836.397L772.576 939.128C802.984 961.317 838.871 973.097 876.402 973.097C932.469 973.097 985.615 946.068 1018.67 900.866L1036.02 877.124L1021.41 851.647C917.768 670.75 852.294 565.555 851.564 564.459L846.907 557.062L839.875 551.949C809.558 529.667 773.58 517.979 736.049 517.979L736.049 517.979Z" fill="#000000" fill-rule="nonzero" opacity="1" stroke="none"/>
</g>
</g>
<g opacity="1">
<clipPath id="ClipPath_5">
<path d="M1021.41 851.647C983.789 785.991 951.189 730.288 924.89 685.909C949.637 629.658 963.426 567.381 963.426 501.908C963.426 249.693 758.969 45.1459 506.664 45.1459C254.358 45.1459 49.811 249.693 49.811 501.999C49.811 747.547 243.492 947.803 486.392 958.395C493.058 958.669 499.815 958.852 506.573 958.852C585.378 958.852 659.527 938.945 724.178 903.788L772.484 939.128C802.892 961.317 838.78 973.097 876.31 973.097C932.378 973.097 985.524 946.068 1018.58 900.866L1035.93 877.124L1021.41 851.647Z"/>
</clipPath>
<g clip-path="url(#ClipPath_5)">
<path d="M740.067 575.782C764.996 575.782 790.2 583.452 811.933 599.25C811.933 599.25 872.018 695.588 969.178 865.253L969.178 865.253C945.345 897.944 908.271 915.203 870.74 915.203C845.811 915.203 820.608 907.532 798.875 891.735L668.019 796.036C613.686 756.313 601.815 680.065 641.537 625.732L641.537 625.732C665.462 593.132 702.445 575.782 740.067 575.782" fill="#ffffff" fill-rule="nonzero" opacity="1" stroke="none"/>
</g>
</g>
<g opacity="1">
<clipPath id="ClipPath_6">
<path d="M1021.41 851.647C983.789 785.991 951.189 730.288 924.89 685.909C949.637 629.658 963.426 567.381 963.426 501.908C963.426 249.693 758.969 45.1459 506.664 45.1459C254.358 45.1459 49.811 249.693 49.811 501.999C49.811 747.547 243.492 947.803 486.392 958.395C493.058 958.669 499.815 958.852 506.573 958.852C585.378 958.852 659.527 938.945 724.178 903.788L772.484 939.128C802.892 961.317 838.78 973.097 876.31 973.097C932.378 973.097 985.524 946.068 1018.58 900.866L1035.93 877.124L1021.41 851.647Z"/>
</clipPath>
<g clip-path="url(#ClipPath_6)">
<path d="M249.701 534.325C249.701 634.041 330.607 714.947 430.324 714.947L430.324 534.325L249.701 534.325Z" fill="#ffffff" fill-rule="nonzero" opacity="1" stroke="none"/>
</g>
</g>
<g opacity="1">
<clipPath id="ClipPath_7">
<path d="M1021.41 851.647C983.789 785.991 951.189 730.288 924.89 685.909C949.637 629.658 963.426 567.381 963.426 501.908C963.426 249.693 758.969 45.1459 506.664 45.1459C254.358 45.1459 49.811 249.693 49.811 501.999C49.811 747.547 243.492 947.803 486.392 958.395C493.058 958.669 499.815 958.852 506.573 958.852C585.378 958.852 659.527 938.945 724.178 903.788L772.484 939.128C802.892 961.317 838.78 973.097 876.31 973.097C932.378 973.097 985.524 946.068 1018.58 900.866L1035.93 877.124L1021.41 851.647Z"/>
</clipPath>
<g clip-path="url(#ClipPath_7)">
<path d="M430.324 210.245C282.118 210.245 162.038 330.325 162.038 478.531L430.324 478.531L430.324 210.245Z" fill="url(#LinearGradient)" fill-rule="nonzero" opacity="1" stroke="none"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.6 KiB

View file

@ -0,0 +1,202 @@
<script setup lang="ts">
import { onMounted, onUnmounted, reactive, ref, watch } from "vue";
import Quill from "quill";
import "quill/dist/quill.core.css";
import "quill/dist/quill.bubble.css";
import "quill/dist/quill.snow.css";
import { QuillBinding } from "./binding";
import { Loro, setPanicHook, convertVersionToReadableMap } from "loro-crdt";
setPanicHook();
const editor1 = ref<null | HTMLDivElement>(null);
const editor2 = ref<null | HTMLDivElement>(null);
const editor3 = ref<null | HTMLDivElement>(null);
const editor4 = ref<null | HTMLDivElement>(null);
const binds: QuillBinding[] = [];
const texts: Loro[] = [];
const editors = [editor1, editor2, editor3, editor4];
const editorVersions = reactive(["", "", "", ""]);
const online = reactive([true, true, true, true]);
onMounted(() => {
let index = 0;
for (const editor of editors) {
const text = new Loro();
text.setPeerId(BigInt(index));
texts.push(text);
const quill = new Quill(editor.value!, {
modules: {
toolbar: [
[
{
header: [1, 2, 3, 4, false],
},
],
["bold", "italic", "underline", "link"],
],
},
//theme: 'bubble',
theme: "snow",
formats: ["bold", "underline", "header", "italic", "link"],
placeholder: "Type something in here!",
});
binds.push(new QuillBinding(text, quill));
const this_index = index;
const sync = () => {
if (!online[this_index]) {
return;
}
for (let i = 0; i < texts.length; i++) {
if (i === this_index || !online[i]) {
continue;
}
texts[i].import(text.exportFrom(texts[i].version()));
text.import(texts[i].exportFrom(text.version()));
}
};
text.subscribe((e) => {
if (e.local) {
Promise.resolve().then(sync);
}
Promise.resolve().then(() => {
const version = text.version();
const map = convertVersionToReadableMap(version);
const versionObj = {};
for (const [key, value] of map) {
versionObj[key.toString()] = value;
}
const versionStr = JSON.stringify(versionObj, null, 2);
console.log(map, versionStr);
editorVersions[this_index] = versionStr;
});
});
watch(
() => online[this_index],
(isOnline) => {
if (isOnline) {
sync();
}
}
);
index += 1;
}
});
onUnmounted(() => {
binds.forEach((x) => x.destroy());
});
</script>
<template>
<h2>
<a href="https://github.com/loro-dev/crdt-richtext">
<img src="./assets/Loro.svg" alt="Loro Logo" class="logo" />
Loro crdt-richtext
</a>
</h2>
<div class="parent">
<div class="editor">
<button
@click="
() => {
online[0] = !online[0];
}
"
>
Editor 0 online: {{ online[0] }}
</button>
<div class="version">version: {{ editorVersions[0] }}</div>
<div ref="editor1" />
</div>
<div class="editor">
<button
@click="
() => {
online[1] = !online[1];
}
"
>
Editor 1 online: {{ online[1] }}
</button>
<div class="version">version: {{ editorVersions[1] }}</div>
<div ref="editor2" />
</div>
<div class="editor">
<button
@click="
() => {
online[2] = !online[2];
}
"
>
Editor 2 online: {{ online[2] }}
</button>
<div class="version">version: {{ editorVersions[2] }}</div>
<div ref="editor3" />
</div>
<div class="editor">
<button
@click="
() => {
online[3] = !online[3];
}
"
>
Editor 3 online: {{ online[3] }}
</button>
<div class="version">version: {{ editorVersions[3] }}</div>
<div ref="editor4" />
</div>
</div>
</template>
<style scoped>
a {
color: black;
font-weight: 900;
}
h2 {
font-weight: 900;
}
.logo {
width: 2em;
margin-right: 0.5em;
vertical-align: -0.5em;
}
.editor {
width: 400px;
display: flex;
flex-direction: column;
min-height: 200px;
}
button {
color: #565656;
padding: 0.3em 0.6em;
margin-bottom: 0.4em;
background-color: #eee;
}
/**matrix 2x2 */
.parent {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 2em 1em;
}
.version {
color: grey;
font-size: 0.8em;
}
</style>

View file

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Created with Vectornator (http://vectornator.io/) -->
<svg height="100%" stroke-miterlimit="10" style="fill-rule:nonzero;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;" version="1.1" viewBox="0 0 1080 1016.1" width="100%" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:vectornator="http://vectornator.io" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<linearGradient gradientTransform="matrix(0.913158 0 0 0.913158 49.811 45.1459)" gradientUnits="userSpaceOnUse" id="LinearGradient" x1="150.613" x2="415.258" y1="270.181" y2="397.943">
<stop offset="0" stop-color="#b8f6ff"/>
<stop offset="0.15" stop-color="#aaf0fa"/>
<stop offset="0.4331" stop-color="#85dfed"/>
<stop offset="0.6323" stop-color="#67d2e3"/>
<stop offset="1" stop-color="#67d2e3"/>
</linearGradient>
</defs>
<g id="Untitled" vectornator:layerName="Untitled">
<g opacity="1">
<g opacity="1">
<clipPath id="ClipPath">
<path d="M1020.6 850.501C982.978 784.845 950.378 729.142 924.079 684.763C948.826 628.512 962.615 566.235 962.615 500.762C962.615 248.547 758.158 44 505.853 44C253.547 44 49 248.547 49 500.853C49 746.401 242.681 946.657 485.581 957.249C492.247 957.523 499.004 957.706 505.762 957.706C584.567 957.706 658.716 937.799 723.367 902.642L771.673 937.982C802.081 960.171 837.968 971.951 875.499 971.951C931.567 971.951 984.713 944.922 1017.77 899.72L1035.12 875.978L1020.6 850.501Z"/>
</clipPath>
<g clip-path="url(#ClipPath)">
<path d="M962.615 500.853C962.615 620.385 916.683 729.234 841.53 810.687C758.067 901.09 638.535 957.706 505.853 957.706C499.096 957.706 492.338 957.523 485.672 957.249C242.681 946.657 49 746.401 49 500.853C49 248.547 253.547 44 505.853 44C758.158 44 962.615 248.547 962.615 500.853Z" fill="#000000" fill-rule="nonzero" opacity="1" stroke="none"/>
</g>
</g>
<g opacity="1">
<clipPath id="ClipPath_2">
<path d="M1021.41 851.647C983.789 785.991 951.189 730.288 924.89 685.909C949.637 629.658 963.426 567.381 963.426 501.908C963.426 249.693 758.969 45.1459 506.664 45.1459C254.358 45.1459 49.811 249.693 49.811 501.999C49.811 747.547 243.492 947.803 486.392 958.395C493.058 958.669 499.815 958.852 506.573 958.852C585.378 958.852 659.527 938.945 724.178 903.788L772.484 939.128C802.892 961.317 838.78 973.097 876.31 973.097C932.378 973.097 985.524 946.068 1018.58 900.866L1035.93 877.124L1021.41 851.647Z"/>
</clipPath>
<g clip-path="url(#ClipPath_2)">
<path d="M839.967 506.199L839.967 814.025C757.782 903.149 639.985 958.943 509.129 958.943C502.463 958.943 495.432 958.76 488.857 958.486L489.223 210.336L544.103 210.336C707.559 210.336 839.967 342.835 839.967 506.199Z" fill="#ffffff" fill-rule="nonzero" opacity="1" stroke="none"/>
</g>
</g>
<g opacity="1">
<clipPath id="ClipPath_3">
<path d="M1021.41 851.647C983.789 785.991 951.189 730.288 924.89 685.909C949.637 629.658 963.426 567.381 963.426 501.908C963.426 249.693 758.969 45.1459 506.664 45.1459C254.358 45.1459 49.811 249.693 49.811 501.999C49.811 747.547 243.492 947.803 486.392 958.395C493.058 958.669 499.815 958.852 506.573 958.852C585.378 958.852 659.527 938.945 724.178 903.788L772.484 939.128C802.892 961.317 838.78 973.097 876.31 973.097C932.378 973.097 985.524 946.068 1018.58 900.866L1035.93 877.124L1021.41 851.647Z"/>
</clipPath>
<g clip-path="url(#ClipPath_3)">
<path d="M568.393 378.083C568.393 335.266 603.103 300.556 645.921 300.556C688.738 300.556 723.448 335.266 723.448 378.083C723.448 420.9 688.738 455.61 645.921 455.61C603.103 455.61 568.393 420.9 568.393 378.083Z" fill="#000000" fill-rule="nonzero" opacity="1" stroke="none"/>
</g>
</g>
<g opacity="1">
<clipPath id="ClipPath_4">
<path d="M1021.41 851.647C983.789 785.991 951.189 730.288 924.89 685.909C949.637 629.658 963.426 567.381 963.426 501.908C963.426 249.693 758.969 45.1459 506.664 45.1459C254.358 45.1459 49.811 249.693 49.811 501.999C49.811 747.547 243.492 947.803 486.392 958.395C493.058 958.669 499.815 958.852 506.573 958.852C585.378 958.852 659.527 938.945 724.178 903.788L772.484 939.128C802.892 961.317 838.78 973.097 876.31 973.097C932.378 973.097 985.524 946.068 1018.58 900.866L1035.93 877.124L1021.41 851.647Z"/>
</clipPath>
<g clip-path="url(#ClipPath_4)">
<path d="M740.067 575.782C764.996 575.782 790.2 583.452 811.933 599.25C811.933 599.25 872.018 695.588 969.178 865.253L969.178 865.253C945.345 897.944 908.271 915.203 870.74 915.203C845.811 915.203 820.608 907.532 798.875 891.735L668.019 796.036C613.686 756.313 601.815 680.065 641.537 625.732L641.537 625.732C665.462 593.132 702.445 575.782 740.067 575.782M736.049 517.979C679.981 517.979 626.836 545.009 593.779 590.21C566.019 628.197 554.696 674.677 561.91 721.248C569.124 767.728 594.053 808.637 632.041 836.397L772.576 939.128C802.984 961.317 838.871 973.097 876.402 973.097C932.469 973.097 985.615 946.068 1018.67 900.866L1036.02 877.124L1021.41 851.647C917.768 670.75 852.294 565.555 851.564 564.459L846.907 557.062L839.875 551.949C809.558 529.667 773.58 517.979 736.049 517.979L736.049 517.979Z" fill="#000000" fill-rule="nonzero" opacity="1" stroke="none"/>
</g>
</g>
<g opacity="1">
<clipPath id="ClipPath_5">
<path d="M1021.41 851.647C983.789 785.991 951.189 730.288 924.89 685.909C949.637 629.658 963.426 567.381 963.426 501.908C963.426 249.693 758.969 45.1459 506.664 45.1459C254.358 45.1459 49.811 249.693 49.811 501.999C49.811 747.547 243.492 947.803 486.392 958.395C493.058 958.669 499.815 958.852 506.573 958.852C585.378 958.852 659.527 938.945 724.178 903.788L772.484 939.128C802.892 961.317 838.78 973.097 876.31 973.097C932.378 973.097 985.524 946.068 1018.58 900.866L1035.93 877.124L1021.41 851.647Z"/>
</clipPath>
<g clip-path="url(#ClipPath_5)">
<path d="M740.067 575.782C764.996 575.782 790.2 583.452 811.933 599.25C811.933 599.25 872.018 695.588 969.178 865.253L969.178 865.253C945.345 897.944 908.271 915.203 870.74 915.203C845.811 915.203 820.608 907.532 798.875 891.735L668.019 796.036C613.686 756.313 601.815 680.065 641.537 625.732L641.537 625.732C665.462 593.132 702.445 575.782 740.067 575.782" fill="#ffffff" fill-rule="nonzero" opacity="1" stroke="none"/>
</g>
</g>
<g opacity="1">
<clipPath id="ClipPath_6">
<path d="M1021.41 851.647C983.789 785.991 951.189 730.288 924.89 685.909C949.637 629.658 963.426 567.381 963.426 501.908C963.426 249.693 758.969 45.1459 506.664 45.1459C254.358 45.1459 49.811 249.693 49.811 501.999C49.811 747.547 243.492 947.803 486.392 958.395C493.058 958.669 499.815 958.852 506.573 958.852C585.378 958.852 659.527 938.945 724.178 903.788L772.484 939.128C802.892 961.317 838.78 973.097 876.31 973.097C932.378 973.097 985.524 946.068 1018.58 900.866L1035.93 877.124L1021.41 851.647Z"/>
</clipPath>
<g clip-path="url(#ClipPath_6)">
<path d="M249.701 534.325C249.701 634.041 330.607 714.947 430.324 714.947L430.324 534.325L249.701 534.325Z" fill="#ffffff" fill-rule="nonzero" opacity="1" stroke="none"/>
</g>
</g>
<g opacity="1">
<clipPath id="ClipPath_7">
<path d="M1021.41 851.647C983.789 785.991 951.189 730.288 924.89 685.909C949.637 629.658 963.426 567.381 963.426 501.908C963.426 249.693 758.969 45.1459 506.664 45.1459C254.358 45.1459 49.811 249.693 49.811 501.999C49.811 747.547 243.492 947.803 486.392 958.395C493.058 958.669 499.815 958.852 506.573 958.852C585.378 958.852 659.527 938.945 724.178 903.788L772.484 939.128C802.892 961.317 838.78 973.097 876.31 973.097C932.378 973.097 985.524 946.068 1018.58 900.866L1035.93 877.124L1021.41 851.647Z"/>
</clipPath>
<g clip-path="url(#ClipPath_7)">
<path d="M430.324 210.245C282.118 210.245 162.038 330.325 162.038 478.531L430.324 478.531L430.324 210.245Z" fill="url(#LinearGradient)" fill-rule="nonzero" opacity="1" stroke="none"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.6 KiB

View file

@ -0,0 +1,160 @@
/**
* The skeleton of this binding is learned from https://github.com/yjs/y-quill
*/
import { Delta, Loro, LoroText } from "loro-crdt";
import Quill, { DeltaStatic, Sources } from "quill";
// @ts-ignore
import isEqual from "is-equal";
const Delta = Quill.import("delta");
export class QuillBinding {
private richtext: LoroText;
constructor(public doc: Loro, public quill: Quill) {
this.quill = quill;
this.richtext = doc.getText("text");
this.richtext.subscribe(doc, (event) => {
// Promise.resolve().then(() => {
// let delta: DeltaType = new Delta(
// richtext.getAnnSpans(),
// );
// quill.setContents(
// delta,
// "this" as any,
// );
// });
Promise.resolve().then(() => {
if (!event.local && event.diff.type == "text") {
console.log(
doc.peerId,
"CRDT_EVENT",
event,
);
const eventDelta = event.diff.diff;
const delta: Delta<string>[] = [];
let index = 0;
for (let i = 0; i < eventDelta.length; i++) {
const d = eventDelta[i];
const length = d.delete || d.retain || d.insert!.length;
// skip the last newline that quill automatically appends
if (
d.insert && d.insert === "\n" &&
index === quill.getLength() - 1 &&
i === eventDelta.length - 1 && d.attributes != null &&
Object.keys(d.attributes).length > 0
) {
delta.push({
retain: 1,
attributes: d.attributes,
});
index += length;
continue;
}
delta.push(d);
index += length;
}
quill.updateContents(new Delta(delta), "this" as any);
const a = this.richtext.toDelta();
const b = this.quill.getContents().ops;
console.log(this.doc.peerId, "COMPARE AFTER CRDT_EVENT");
if (!assertEqual(a, b as any)) {
quill.setContents(new Delta(a), "this" as any);
}
}
});
});
quill.setContents(
new Delta(
this.richtext.toDelta().map((x) => ({
insert: x.insert,
attributions: x.attributes,
})),
),
"this" as any,
);
quill.on("editor-change", this.quillObserver);
}
quillObserver: (
name: "text-change",
delta: DeltaStatic,
oldContents: DeltaStatic,
source: Sources,
) => any = (_eventType, delta, _state, origin) => {
if (delta && delta.ops) {
// update content
const ops = delta.ops;
if (origin !== "this" as any) {
this.richtext.applyDelta(ops);
const a = this.richtext.toDelta();
const b = this.quill.getContents().ops;
console.log(this.doc.peerId, "COMPARE AFTER QUILL_EVENT");
assertEqual(a, b as any);
console.log(
this.doc.peerId,
"CHECK_MATCH",
{ delta },
a,
b,
);
console.log("SIZE", this.doc.exportFrom().length);
}
}
};
destroy() {
// TODO: unobserve
this.quill.off("editor-change", this.quillObserver);
}
}
function assertEqual(a: Delta<string>[], b: Delta<string>[]): boolean {
a = normQuillDelta(a);
b = normQuillDelta(b);
const equal = isEqual(a, b);
console.assert(equal, a, b);
return equal;
}
/**
* Removes the pending '\n's if it has no attributes.
*
* Normalize attributes field
*/
export const normQuillDelta = (delta: Delta<string>[]) => {
for (const d of delta) {
for (const key of Object.keys(d.attributes || {})) {
if (d.attributes![key] == null) {
delete d.attributes![key];
}
}
}
for (const d of delta) {
if (Object.keys(d.attributes || {}).length === 0) {
delete d.attributes;
}
}
if (delta.length > 0) {
const d = delta[delta.length - 1];
const insert = d.insert;
if (
d.attributes === undefined && insert !== undefined &&
insert.slice(-1) === "\n"
) {
delta = delta.slice();
let ins = insert.slice(0, -1);
while (ins.slice(-1) === "\n") {
ins = ins.slice(0, -1);
}
delta[delta.length - 1] = { insert: ins };
if (ins.length === 0) {
delta.pop();
}
return delta;
}
}
return delta;
};

View file

@ -0,0 +1,38 @@
<script setup lang="ts">
import { ref } from 'vue'
defineProps<{ msg: string }>()
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
>create-vue</a
>, the official Vue + Vite starter
</p>
<p>
Install
<a href="https://github.com/vuejs/language-tools" target="_blank">Volar</a>
in your IDE for a better DX
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>

View file

@ -0,0 +1,5 @@
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
createApp(App).mount('#app')

View file

@ -0,0 +1,78 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #ddd;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}

1
examples/loro-quill/src/vite-env.d.ts vendored Normal file
View file

@ -0,0 +1 @@
/// <reference types="vite/client" />

View file

@ -0,0 +1,9 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import wasm from "vite-plugin-wasm";
import topLevelAwait from "vite-plugin-top-level-await";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue(), wasm(), topLevelAwait()],
});

View file

@ -20,7 +20,7 @@
"@rollup/plugin-node-resolve": "^15.0.1",
"@typescript-eslint/parser": "^6.2.0",
"@vitest/ui": "^0.34.6",
"esbuild": "^0.17.12",
"esbuild": "^0.18.20",
"eslint": "^8.46.0",
"prettier": "^3.0.0",
"rollup": "^3.20.1",
@ -29,6 +29,6 @@
"typescript": "^5.0.2",
"vite": "^4.2.1",
"vite-plugin-wasm": "^3.2.2",
"vitest": "^0.29.7"
"vitest": "^0.30.1"
}
}

View file

@ -1,20 +1,9 @@
export {
LoroList,
LoroMap,
LoroText,
PrelimList,
PrelimMap,
PrelimText,
Delta,
setPanicHook,
} from "loro-wasm";
export * from "loro-wasm";
import { Delta, PrelimMap } from "loro-wasm";
import { PrelimText } from "loro-wasm";
import { PrelimList } from "loro-wasm";
import { ContainerID, Loro, LoroList, LoroMap, LoroText } from "loro-wasm";
export type { ContainerID, ContainerType } from "loro-wasm";
Loro.prototype.getTypedMap = function (...args) {
return this.getMap(...args);
};

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,4 @@
packages:
- "loro-js"
- "crates/loro-wasm"
- "examples/loro-quill"