feat: add old parent and old index in tree diff (#452)

* feat: add old parent in tree diff

* chore: enable ci

* feat: add old_index to tree diff

* fix: new fractional index config

* fix: cargo fix

* fix: add FractionalIndexNotEnabled error

* fix: move config to tree state

* fix: error string

---------

Co-authored-by: Zixuan Chen <remch183@outlook.com>
This commit is contained in:
Leon Zhao 2024-09-09 16:16:02 +08:00 committed by GitHub
parent dd6bb10fff
commit 07671ea9fd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 549 additions and 371 deletions

View file

@ -4,7 +4,7 @@ on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
branches: ["main", "dev"]
types: [opened, synchronize, reopened, ready_for_review]
env:

View file

@ -780,6 +780,7 @@ name = "loro_fractional_index"
version = "0.16.2"
dependencies = [
"imbl",
"once_cell",
"rand",
"serde",
"smallvec",

View file

@ -24,8 +24,6 @@ use super::{
container::MapActor,
};
const DEFAULT_WITH_FRACTIONAL_INDEX: bool = false;
#[derive(Debug)]
pub struct Undo {
pub undo: UndoManager,
@ -47,7 +45,6 @@ impl Actor {
pub fn new(id: PeerID) -> Self {
let loro = LoroDoc::new();
loro.set_peer_id(id).unwrap();
loro.set_with_fractional_index(DEFAULT_WITH_FRACTIONAL_INDEX);
let undo = UndoManager::new(&loro);
let tracker = Arc::new(Mutex::new(ContainerTracker::Map(MapTracker::empty(
ContainerID::new_root("sys:root", ContainerType::Map),
@ -122,6 +119,9 @@ impl Actor {
}
if let Some(idx) = idx {
if let Container::Tree(tree) = &idx {
tree.set_enable_fractional_index(0);
}
self.add_new_container(idx);
}
}

View file

@ -10,7 +10,6 @@ use loro::{
event::Diff, Container, ContainerID, ContainerType, LoroDoc, LoroError, LoroTree, LoroValue,
TreeExternalDiff, TreeID,
};
use tracing::{debug, trace};
use crate::{
actions::{Actionable, FromGenericAction, GenericAction},
@ -117,6 +116,7 @@ impl TreeActor {
);
let root = loro.get_tree("tree");
root.set_enable_fractional_index(0);
Self {
loro,
containers: vec![root],
@ -184,7 +184,7 @@ impl Actionable for TreeAction {
}
*parent = (nodes[parent_idx].peer, nodes[parent_idx].counter);
*index %= tree
.children_num(Some(TreeID::new(parent.0, parent.1)))
.children_num(TreeID::new(parent.0, parent.1))
.unwrap_or(0)
+ 1;
}
@ -426,7 +426,7 @@ impl ApplyDiff for TreeTracker {
index,
position,
} => {
self.create_node(target, parent, position.to_string(), index);
self.create_node(target, &parent.tree_id(), position.to_string(), index);
}
TreeExternalDiff::Delete { .. } => {
let node = self.find_node_by_id(target).unwrap();
@ -442,9 +442,10 @@ impl ApplyDiff for TreeTracker {
parent,
index,
position,
..
} => {
let Some(node) = self.find_node_by_id(target) else {
self.create_node(target, parent, position.to_string(), index);
self.create_node(target, &parent.tree_id(), position.to_string(), index);
continue;
};
@ -456,10 +457,10 @@ impl ApplyDiff for TreeTracker {
let index = self.tree.iter().position(|n| n.id == target).unwrap();
self.tree.remove(index)
};
node.parent = *parent;
node.parent = parent.tree_id();
node.position = position.to_string();
if let Some(parent) = parent {
let parent = self.find_node_by_id_mut(*parent).unwrap();
if let Some(parent) = parent.tree_id() {
let parent = self.find_node_by_id_mut(parent).unwrap();
parent.children.insert(*index, node);
} else {
if self.find_node_by_id_mut(target).is_some() {

View file

@ -88,6 +88,8 @@ pub enum LoroTreeError {
TreeNodeNotExist(TreeID),
#[error("The index({index}) should be <= the length of children ({len})")]
IndexOutOfBound { len: usize, index: usize },
#[error("Fractional index is not enabled, you should enable it first by `LoroTree::set_enable_fractional_index`")]
FractionalIndexNotEnabled,
}
#[cfg(feature = "wasm")]

View file

@ -1,4 +1,4 @@
use std::{fmt::Display, io::Write, str::Bytes, sync::Arc};
use std::{fmt::Display, io::Write, sync::Arc};
use arbitrary::Arbitrary;
use enum_as_inner::EnumAsInner;

View file

@ -3,7 +3,7 @@ use criterion::{criterion_group, criterion_main, Criterion};
mod tree {
use super::*;
use criterion::{AxisScale, BenchmarkId, PlotConfiguration};
use loro_internal::LoroDoc;
use loro_internal::{LoroDoc, TreeParentId};
use rand::{rngs::StdRng, Rng};
pub fn tree_move(c: &mut Criterion) {
@ -22,7 +22,7 @@ mod tree {
let loro = LoroDoc::new_auto_commit();
let tree = loro.get_tree("tree");
for idx in 0..*i {
tree.create_at(None, idx as usize).unwrap();
tree.create_at(TreeParentId::Root, idx as usize).unwrap();
}
})
},
@ -39,15 +39,16 @@ mod tree {
let mut ids = vec![];
for _ in 0..SIZE {
let pos = rng.gen::<usize>() % (ids.len() + 1);
ids.push(tree.create_at(None, pos).unwrap());
ids.push(tree.create_at(TreeParentId::Root, pos).unwrap());
}
b.iter(|| {
for _ in 0..*i {
tree.create_at(None, 0).unwrap();
tree.create_at(TreeParentId::Root, 0).unwrap();
let i = rng.gen::<usize>() % SIZE;
let j = rng.gen::<usize>() % SIZE;
tree.mov(ids[i], ids[j]).unwrap_or_default();
tree.mov(ids[i], TreeParentId::Node(ids[j]))
.unwrap_or_default();
}
})
},
@ -62,14 +63,14 @@ mod tree {
let mut versions = vec![];
let size = 1000;
for _ in 0..size {
ids.push(tree.create(None).unwrap())
ids.push(tree.create(TreeParentId::Root).unwrap())
}
let mut rng: StdRng = rand::SeedableRng::seed_from_u64(0);
let mut n = 1000;
while n > 0 {
let i = rng.gen::<usize>() % size;
let j = rng.gen::<usize>() % size;
if tree.mov(ids[i], ids[j]).is_ok() {
if tree.mov(ids[i], TreeParentId::Node(ids[j])).is_ok() {
versions.push(loro.oplog_frontiers());
n -= 1;
};
@ -90,11 +91,13 @@ mod tree {
let tree = loro.get_tree("tree");
let mut ids = vec![];
let mut versions = vec![];
let id1 = tree.create(None).unwrap();
let id1 = tree.create(TreeParentId::Root).unwrap();
ids.push(id1);
versions.push(loro.oplog_frontiers());
for _ in 1..depth {
let id = tree.create(*ids.last().unwrap()).unwrap();
let id = tree
.create(TreeParentId::Node(*ids.last().unwrap()))
.unwrap();
ids.push(id);
versions.push(loro.oplog_frontiers());
}
@ -118,7 +121,7 @@ mod tree {
let mut ids = vec![];
let size = 1000;
for _ in 0..size {
ids.push(tree_a.create(None).unwrap())
ids.push(tree_a.create(TreeParentId::Root).unwrap())
}
doc_b.import(&doc_a.export_snapshot()).unwrap();
let mut rng: StdRng = rand::SeedableRng::seed_from_u64(0);
@ -128,10 +131,14 @@ mod tree {
let i = rng.gen::<usize>() % size;
let j = rng.gen::<usize>() % size;
if t % 2 == 0 {
tree_a.mov(ids[i], ids[j]).unwrap_or_default();
tree_a
.mov(ids[i], TreeParentId::Node(ids[j]))
.unwrap_or_default();
doc_b.import(&doc_a.export_from(&doc_b.oplog_vv())).unwrap();
} else {
tree_b.mov(ids[i], ids[j]).unwrap_or_default();
tree_b
.mov(ids[i], TreeParentId::Node(ids[j]))
.unwrap_or_default();
doc_a.import(&doc_b.export_from(&doc_a.oplog_vv())).unwrap();
}
}

View file

@ -1,6 +1,6 @@
use std::time::Instant;
use loro_internal::LoroDoc;
use loro_internal::{LoroDoc, TreeParentId};
use rand::{rngs::StdRng, Rng};
#[allow(unused)]
@ -10,11 +10,13 @@ fn checkout() {
let tree = loro.get_tree("tree");
let mut ids = vec![];
let mut versions = vec![];
let id1 = tree.create_at(None, 0).unwrap();
let id1 = tree.create_at(TreeParentId::Root, 0).unwrap();
ids.push(id1);
versions.push(loro.oplog_frontiers());
for _ in 1..depth {
let id = tree.create_at(*ids.last().unwrap(), 0).unwrap();
let id = tree
.create_at(TreeParentId::Node(*ids.last().unwrap()), 0)
.unwrap();
ids.push(id);
versions.push(loro.oplog_frontiers());
}
@ -34,7 +36,7 @@ fn mov() {
let mut ids = vec![];
let size = 10000;
for _ in 0..size {
ids.push(tree.create_at(None, 0).unwrap())
ids.push(tree.create_at(TreeParentId::Root, 0).unwrap())
}
let mut rng: StdRng = rand::SeedableRng::seed_from_u64(0);
let n = 100000;
@ -42,8 +44,8 @@ fn mov() {
for _ in 0..n {
let i = rng.gen::<usize>() % size;
let j = rng.gen::<usize>() % size;
let children_num = tree.children_num(Some(ids[j])).unwrap_or(0);
tree.move_to(ids[i], ids[j], children_num)
let children_num = tree.children_num(&TreeParentId::Node(ids[j])).unwrap_or(0);
tree.move_to(ids[i], TreeParentId::Node(ids[j]), children_num)
.unwrap_or_default();
}
println!("encode snapshot size {:?}", loro.export_snapshot().len());
@ -59,7 +61,7 @@ fn create() {
let loro = LoroDoc::default();
let tree = loro.get_tree("tree");
for _ in 0..size {
tree.create_at(None, 0).unwrap();
tree.create_at(TreeParentId::Root, 0).unwrap();
}
println!("encode snapshot size {:?}\n", loro.export_snapshot().len());
println!(

View file

@ -5,11 +5,6 @@ pub struct Configure {
pub(crate) text_style_config: Arc<RwLock<StyleConfigMap>>,
record_timestamp: Arc<AtomicBool>,
pub(crate) merge_interval: Arc<AtomicI64>,
/// Whether the tree has fractional index. `false` by default. If false, the fractional index is always [`FractionalIndex::default`] and
/// `tree_position_jitter` is not used.
pub(crate) tree_with_fractional_index: Arc<AtomicBool>,
/// do not use `jitter` by default
pub(crate) tree_position_jitter: Arc<AtomicU8>,
}
impl Default for Configure {
@ -18,8 +13,6 @@ impl Default for Configure {
text_style_config: Arc::new(RwLock::new(StyleConfigMap::default_rich_text_config())),
record_timestamp: Arc::new(AtomicBool::new(false)),
merge_interval: Arc::new(AtomicI64::new(1000 * 1000)),
tree_position_jitter: Arc::new(AtomicU8::new(0)),
tree_with_fractional_index: Arc::new(AtomicBool::new(false)),
}
}
}
@ -38,14 +31,6 @@ impl Configure {
self.merge_interval
.load(std::sync::atomic::Ordering::Relaxed),
)),
tree_position_jitter: Arc::new(AtomicU8::new(
self.tree_position_jitter
.load(std::sync::atomic::Ordering::Relaxed),
)),
tree_with_fractional_index: Arc::new(AtomicBool::new(
self.tree_with_fractional_index
.load(std::sync::atomic::Ordering::Relaxed),
)),
}
}
@ -63,16 +48,6 @@ impl Configure {
.store(record, std::sync::atomic::Ordering::Relaxed);
}
pub fn set_with_fractional_index(&self, with_fractional_index: bool) {
self.tree_with_fractional_index
.store(with_fractional_index, std::sync::atomic::Ordering::Relaxed);
}
pub fn set_fractional_index_jitter(&self, jitter: u8) {
self.tree_position_jitter
.store(jitter, std::sync::atomic::Ordering::Relaxed);
}
pub fn merge_interval(&self) -> i64 {
self.merge_interval
.load(std::sync::atomic::Ordering::Relaxed)
@ -90,7 +65,7 @@ pub struct DefaultRandom;
#[cfg(test)]
use std::sync::atomic::AtomicU64;
use std::sync::{
atomic::{AtomicBool, AtomicI64, AtomicU8},
atomic::{AtomicBool, AtomicI64},
Arc, RwLock,
};
#[cfg(test)]

View file

@ -21,16 +21,21 @@ pub struct TreeDiffItem {
#[derive(Debug, Clone, PartialEq)]
pub enum TreeExternalDiff {
Create {
parent: Option<TreeID>,
parent: TreeParentId,
index: usize,
position: FractionalIndex,
},
Move {
parent: Option<TreeID>,
parent: TreeParentId,
index: usize,
position: FractionalIndex,
old_parent: TreeParentId,
old_index: usize,
},
Delete {
old_parent: TreeParentId,
old_index: usize,
},
Delete,
}
impl TreeDiff {

View file

@ -50,7 +50,7 @@ impl DiffCalculatorTrait for TreeDiffCalculator {
fn apply_change(
&mut self,
_oplog: &OpLog,
oplog: &OpLog,
op: crate::op::RichOp,
_vv: Option<&crate::VersionVector>,
) {

View file

@ -11,7 +11,7 @@ use crate::{
diff::{myers_diff, DiffHandler, OperateProxy},
event::{Diff, TextDiffItem},
op::ListSlice,
state::{ContainerState, IndexType, State},
state::{ContainerState, IndexType, State, TreeParentId},
txn::EventHint,
utils::{string_slice::StringSlice, utf16::count_utf16_len},
};
@ -1171,7 +1171,7 @@ impl Handler {
index: _,
position,
} => {
if let Some(p) = parent.as_mut() {
if let TreeParentId::Node(p) = &mut parent {
remap_tree_id(p, container_remap)
}
remap_tree_id(&mut target, container_remap);
@ -1206,14 +1206,16 @@ impl Handler {
mut parent,
index: _,
position,
old_parent: _,
old_index: _,
} => {
if let Some(p) = parent.as_mut() {
if let TreeParentId::Node(p) = &mut parent {
remap_tree_id(p, container_remap)
}
remap_tree_id(&mut target, container_remap);
x.move_at_with_target_for_apply_diff(parent, position, target)?;
}
TreeExternalDiff::Delete => {
TreeExternalDiff::Delete { .. } => {
remap_tree_id(&mut target, container_remap);
if x.contains(target) {
x.delete(target)?;
@ -3939,6 +3941,7 @@ mod test {
use super::{HandlerTrait, TextDelta};
use crate::loro::LoroDoc;
use crate::state::TreeParentId;
use crate::version::Frontiers;
use crate::{fx_map, ToJson};
use loro_common::ID;
@ -4104,7 +4107,7 @@ mod test {
loro.set_peer_id(1).unwrap();
let tree = loro.get_tree("root");
let id = loro
.with_txn(|txn| tree.create_with_txn(txn, None, 0))
.with_txn(|txn| tree.create_with_txn(txn, TreeParentId::Root, 0))
.unwrap();
loro.with_txn(|txn| {
let meta = tree.get_meta(id)?;
@ -4134,11 +4137,11 @@ mod test {
let tree = loro.get_tree("root");
let text = loro.get_text("text");
loro.with_txn(|txn| {
let id = tree.create_with_txn(txn, None, 0)?;
let id = tree.create_with_txn(txn, TreeParentId::Root, 0)?;
let meta = tree.get_meta(id)?;
meta.insert_with_txn(txn, "a", 1.into())?;
text.insert_with_txn(txn, 0, "abc")?;
let _id2 = tree.create_with_txn(txn, None, 0)?;
let _id2 = tree.create_with_txn(txn, TreeParentId::Root, 0)?;
meta.insert_with_txn(txn, "b", 2.into())?;
Ok(id)
})

View file

@ -93,8 +93,8 @@ impl TreeInner {
self.children_links.get(&parent).map(|x| x.len())
}
fn is_parent(&self, target: TreeID, parent: Option<TreeID>) -> bool {
self.parent_links.get(&target) == Some(&parent)
fn is_parent(&self, target: &TreeID, parent: &Option<TreeID>) -> bool {
self.parent_links.get(target) == Some(parent)
}
fn get_index_by_tree_id(&self, target: &TreeID) -> Option<usize> {
@ -164,7 +164,9 @@ impl HandlerTrait for TreeHandler {
let mut q = children
.map(|c| {
VecDeque::from_iter(
c.iter().enumerate().zip(std::iter::repeat(None::<TreeID>)),
c.iter()
.enumerate()
.zip(std::iter::repeat(TreeParentId::Root)),
)
})
.unwrap_or_default();
@ -179,7 +181,7 @@ impl HandlerTrait for TreeHandler {
if let Some(children) = t.value.children_links.get(&Some(*target)) {
for (idx, child) in children.iter().enumerate() {
q.push_back(((idx, child), Some(real_id)));
q.push_back(((idx, child), TreeParentId::Node(real_id)));
}
}
}
@ -290,27 +292,25 @@ impl TreeHandler {
crate::op::RawOpContent::Tree(Arc::new(TreeOp::Delete { target })),
EventHint::Tree(smallvec![TreeDiffItem {
target,
action: TreeExternalDiff::Delete,
action: TreeExternalDiff::Delete {
old_parent: self.get_node_parent(&target).unwrap(),
old_index: self.get_index_by_tree_id(&target).unwrap(),
},
}]),
&inner.state,
)
}
pub fn create<T: Into<Option<TreeID>>>(&self, parent: T) -> LoroResult<TreeID> {
let parent = parent.into();
let index: usize = self.children_num(parent).unwrap_or(0);
pub fn create(&self, parent: TreeParentId) -> LoroResult<TreeID> {
let index: usize = self.children_num(&parent).unwrap_or(0);
self.create_at(parent, index)
}
pub fn create_at<T: Into<Option<TreeID>>>(
&self,
parent: T,
index: usize,
) -> LoroResult<TreeID> {
pub fn create_at(&self, parent: TreeParentId, index: usize) -> LoroResult<TreeID> {
match &self.inner {
MaybeDetached::Detached(t) => {
let t = &mut t.try_lock().unwrap().value;
Ok(t.create(parent.into(), index))
Ok(t.create(parent.tree_id(), index))
}
MaybeDetached::Attached(a) => {
a.with_txn(|txn| self.create_with_txn(txn, parent, index))
@ -321,23 +321,33 @@ impl TreeHandler {
/// For undo/redo, Specify the TreeID of the created node
pub(crate) fn create_at_with_target_for_apply_diff(
&self,
parent: Option<TreeID>,
parent: TreeParentId,
position: FractionalIndex,
target: TreeID,
) -> LoroResult<bool> {
let MaybeDetached::Attached(a) = &self.inner else {
unreachable!();
};
if let Some(p) = self.get_node_parent(&target) {
if p == parent {
return Ok(false);
// If parent is deleted, we need to create the node, so this op from move_apply_diff
} else if !p.is_some_and(|p| !self.contains(p)) {
return self.move_at_with_target_for_apply_diff(parent, position, target);
}
match p {
TreeParentId::Node(p) => {
if self.contains(p) {
return self.move_at_with_target_for_apply_diff(parent, position, target);
}
}
TreeParentId::Root => {
return self.move_at_with_target_for_apply_diff(parent, position, target);
}
TreeParentId::Deleted | TreeParentId::Unexist => {}
}
}
let with_event = !parent.is_some_and(|p| !self.contains(p));
let with_event = !parent.tree_id().is_some_and(|p| !self.contains(p));
if !with_event {
return Ok(false);
}
@ -349,7 +359,7 @@ impl TreeHandler {
let index = self
.get_index_by_fractional_index(
parent,
&parent,
&NodePosition {
position: position.clone(),
idlp: self.next_idlp(),
@ -365,7 +375,7 @@ impl TreeHandler {
inner.container_idx,
crate::op::RawOpContent::Tree(Arc::new(TreeOp::Create {
target,
parent,
parent: parent.tree_id(),
position: position.clone(),
})),
EventHint::Tree(smallvec![TreeDiffItem {
@ -379,11 +389,13 @@ impl TreeHandler {
&inner.state,
)?;
Ok(self.children(Some(target)).unwrap_or_default())
Ok(self
.children(&TreeParentId::Node(target))
.unwrap_or_default())
})?;
for child in children {
let position = self.get_position_by_tree_id(&child).unwrap();
self.create_at_with_target_for_apply_diff(Some(target), position, child)?;
self.create_at_with_target_for_apply_diff(TreeParentId::Node(target), position, child)?;
}
Ok(true)
}
@ -391,7 +403,7 @@ impl TreeHandler {
/// For undo/redo, Specify the TreeID of the created node
pub(crate) fn move_at_with_target_for_apply_diff(
&self,
parent: Option<TreeID>,
parent: TreeParentId,
position: FractionalIndex,
target: TreeID,
) -> LoroResult<bool> {
@ -412,14 +424,14 @@ impl TreeHandler {
let index = self
.get_index_by_fractional_index(
parent,
&parent,
&NodePosition {
position: position.clone(),
idlp: self.next_idlp(),
},
)
.unwrap_or(0);
let with_event = !parent.is_some_and(|p| !self.contains(p));
let with_event = !parent.tree_id().is_some_and(|p| !self.contains(p));
if !with_event {
return Ok(false);
@ -436,7 +448,7 @@ impl TreeHandler {
inner.container_idx,
crate::op::RawOpContent::Tree(Arc::new(TreeOp::Move {
target,
parent,
parent: parent.tree_id(),
position: position.clone(),
})),
EventHint::Tree(smallvec![TreeDiffItem {
@ -445,6 +457,9 @@ impl TreeHandler {
parent,
index,
position: position.clone(),
// the old parent should be exist, so we can unwrap
old_parent: self.get_node_parent(&target).unwrap(),
old_index: self.get_index_by_tree_id(&target).unwrap(),
},
}]),
&inner.state,
@ -453,17 +468,16 @@ impl TreeHandler {
Ok(true)
}
pub(crate) fn create_with_txn<T: Into<Option<TreeID>>>(
pub(crate) fn create_with_txn(
&self,
txn: &mut Transaction,
parent: T,
parent: TreeParentId,
index: usize,
) -> LoroResult<TreeID> {
let inner = self.inner.try_attached_state()?;
let parent: Option<TreeID> = parent.into();
let target = TreeID::from_id(txn.next_id());
match self.generate_position_at(&target, parent, index) {
match self.generate_position_at(&target, &parent, index) {
FractionalIndexGenResult::Ok(position) => {
self.create_with_position(inner, txn, target, parent, index, position)
}
@ -473,26 +487,25 @@ impl TreeHandler {
self.create_with_position(inner, txn, id, parent, index, position)?;
continue;
}
self.mov_with_position(inner, txn, id, parent, index + i, position)?;
self.mov_with_position(inner, txn, id, parent, index + i, position, index + i)?;
}
Ok(target)
}
}
}
pub fn mov<T: Into<Option<TreeID>>>(&self, target: TreeID, parent: T) -> LoroResult<()> {
let parent = parent.into();
pub fn mov(&self, target: TreeID, parent: TreeParentId) -> LoroResult<()> {
match &self.inner {
MaybeDetached::Detached(_) => {
let mut index: usize = self.children_num(parent).unwrap_or(0);
if self.is_parent(target, parent) {
let mut index: usize = self.children_num(&parent).unwrap_or(0);
if self.is_parent(&target, &parent) {
index -= 1;
}
self.move_to(target, parent, index)
}
MaybeDetached::Attached(a) => {
let mut index = self.children_num(parent).unwrap_or(0);
if self.is_parent(target, parent) {
let mut index = self.children_num(&parent).unwrap_or(0);
if self.is_parent(&target, &parent) {
index -= 1;
}
a.with_txn(|txn| self.mov_with_txn(txn, target, parent, index))
@ -501,11 +514,11 @@ impl TreeHandler {
}
pub fn mov_after(&self, target: TreeID, other: TreeID) -> LoroResult<()> {
let parent: Option<TreeID> = self
let parent = self
.get_node_parent(&other)
.ok_or(LoroTreeError::TreeNodeNotExist(other))?;
let mut index = self.get_index_by_tree_id(&other).unwrap() + 1;
if self.is_parent(target, parent) && self.get_index_by_tree_id(&target).unwrap() < index {
if self.is_parent(&target, &parent) && self.get_index_by_tree_id(&target).unwrap() < index {
index -= 1;
}
self.move_to(target, parent, index)
@ -516,7 +529,7 @@ impl TreeHandler {
.get_node_parent(&other)
.ok_or(LoroTreeError::TreeNodeNotExist(other))?;
let mut index = self.get_index_by_tree_id(&other).unwrap();
if self.is_parent(target, parent)
if self.is_parent(&target, &parent)
&& index > 1
&& self.get_index_by_tree_id(&target).unwrap() < index
{
@ -525,16 +538,11 @@ impl TreeHandler {
self.move_to(target, parent, index)
}
pub fn move_to<T: Into<Option<TreeID>>>(
&self,
target: TreeID,
parent: T,
index: usize,
) -> LoroResult<()> {
pub fn move_to(&self, target: TreeID, parent: TreeParentId, index: usize) -> LoroResult<()> {
match &self.inner {
MaybeDetached::Detached(t) => {
let mut t = t.try_lock().unwrap();
t.value.mov(target, parent.into(), index)
t.value.mov(target, parent.tree_id(), index)
}
MaybeDetached::Attached(a) => {
a.with_txn(|txn| self.mov_with_txn(txn, target, parent, index))
@ -542,19 +550,18 @@ impl TreeHandler {
}
}
pub(crate) fn mov_with_txn<T: Into<Option<TreeID>>>(
pub(crate) fn mov_with_txn(
&self,
txn: &mut Transaction,
target: TreeID,
parent: T,
parent: TreeParentId,
index: usize,
) -> LoroResult<()> {
let parent = parent.into();
let inner = self.inner.try_attached_state()?;
let mut children_len = self.children_num(parent).unwrap_or(0);
let mut children_len = self.children_num(&parent).unwrap_or(0);
let mut already_in_parent = false;
// check the input is valid
if self.is_parent(target, parent) {
if self.is_parent(&target, &parent) {
// If the position after moving is same as the current position , do nothing
if let Some(current_index) = self.get_index_by_tree_id(&target) {
if current_index == index {
@ -573,17 +580,18 @@ impl TreeHandler {
}
.into());
}
let old_index = self.get_index_by_tree_id(&target).unwrap();
if already_in_parent {
self.delete_position(parent, target);
self.delete_position(&parent, &target);
}
match self.generate_position_at(&target, parent, index) {
match self.generate_position_at(&target, &parent, index) {
FractionalIndexGenResult::Ok(position) => {
self.mov_with_position(inner, txn, target, parent, index, position)
self.mov_with_position(inner, txn, target, parent, index, position, old_index)
}
FractionalIndexGenResult::Rearrange(ids) => {
for (i, (id, position)) in ids.into_iter().enumerate() {
self.mov_with_position(inner, txn, id, parent, index + i, position)?;
self.mov_with_position(inner, txn, id, parent, index + i, position, old_index)?;
}
Ok(())
}
@ -596,7 +604,7 @@ impl TreeHandler {
inner: &BasicHandler,
txn: &mut Transaction,
tree_id: TreeID,
parent: Option<TreeID>,
parent: TreeParentId,
index: usize,
position: FractionalIndex,
) -> LoroResult<TreeID> {
@ -604,7 +612,7 @@ impl TreeHandler {
inner.container_idx,
crate::op::RawOpContent::Tree(Arc::new(TreeOp::Create {
target: tree_id,
parent,
parent: parent.tree_id(),
position: position.clone(),
})),
EventHint::Tree(smallvec![TreeDiffItem {
@ -626,15 +634,16 @@ impl TreeHandler {
inner: &BasicHandler,
txn: &mut Transaction,
target: TreeID,
parent: Option<TreeID>,
parent: TreeParentId,
index: usize,
position: FractionalIndex,
old_index: usize,
) -> LoroResult<()> {
txn.apply_local_op(
inner.container_idx,
crate::op::RawOpContent::Tree(Arc::new(TreeOp::Move {
target,
parent,
parent: parent.tree_id(),
position: position.clone(),
})),
EventHint::Tree(smallvec![TreeDiffItem {
@ -643,6 +652,8 @@ impl TreeHandler {
parent,
index,
position,
old_parent: self.get_node_parent(&target).unwrap(),
old_index,
},
}]),
&inner.state,
@ -671,46 +682,42 @@ impl TreeHandler {
}
/// Get the parent of the node, if the node is deleted or does not exist, return None
pub fn get_node_parent(&self, target: &TreeID) -> Option<Option<TreeID>> {
pub fn get_node_parent(&self, target: &TreeID) -> Option<TreeParentId> {
match &self.inner {
MaybeDetached::Detached(t) => {
let t = t.try_lock().unwrap();
t.value.get_parent(target)
t.value.get_parent(target).map(TreeParentId::from)
}
MaybeDetached::Attached(a) => a.with_state(|state| {
let a = state.as_tree_state().unwrap();
match a.parent(target) {
TreeParentId::Root => Some(None),
TreeParentId::Node(parent) => Some(Some(parent)),
TreeParentId::Deleted | TreeParentId::Unexist => None,
}
a.parent(target)
}),
}
}
// TODO: iterator
pub fn children(&self, parent: Option<TreeID>) -> Option<Vec<TreeID>> {
pub fn children(&self, parent: &TreeParentId) -> Option<Vec<TreeID>> {
match &self.inner {
MaybeDetached::Detached(t) => {
let t = t.try_lock().unwrap();
t.value.get_children(parent)
t.value.get_children(parent.tree_id())
}
MaybeDetached::Attached(a) => a.with_state(|state| {
let a = state.as_tree_state().unwrap();
a.children(&TreeParentId::from(parent))
a.children(parent)
}),
}
}
pub fn children_num(&self, parent: Option<TreeID>) -> Option<usize> {
pub fn children_num(&self, parent: &TreeParentId) -> Option<usize> {
match &self.inner {
MaybeDetached::Detached(t) => {
let t = t.try_lock().unwrap();
t.value.children_num(parent)
t.value.children_num(parent.tree_id())
}
MaybeDetached::Attached(a) => a.with_state(|state| {
let a = state.as_tree_state().unwrap();
a.children_num(&TreeParentId::from(parent))
a.children_num(parent)
}),
}
}
@ -741,15 +748,15 @@ impl TreeHandler {
}
}
pub fn is_parent(&self, target: TreeID, parent: Option<TreeID>) -> bool {
pub fn is_parent(&self, target: &TreeID, parent: &TreeParentId) -> bool {
match &self.inner {
MaybeDetached::Detached(t) => {
let t = t.try_lock().unwrap();
t.value.is_parent(target, parent)
t.value.is_parent(target, &parent.tree_id())
}
MaybeDetached::Attached(a) => a.with_state(|state| {
let a = state.as_tree_state().unwrap();
a.is_parent(&TreeParentId::from(parent), &target)
a.is_parent(parent, target)
}),
}
}
@ -768,7 +775,7 @@ impl TreeHandler {
}
pub fn roots(&self) -> Vec<TreeID> {
self.children(None).unwrap_or_default()
self.children(&TreeParentId::Root).unwrap_or_default()
}
#[allow(non_snake_case)]
@ -787,7 +794,7 @@ impl TreeHandler {
fn generate_position_at(
&self,
target: &TreeID,
parent: Option<TreeID>,
parent: &TreeParentId,
index: usize,
) -> FractionalIndexGenResult {
let MaybeDetached::Attached(a) = &self.inner else {
@ -795,7 +802,7 @@ impl TreeHandler {
};
a.with_state(|state| {
let a = state.as_tree_state_mut().unwrap();
a.generate_position_at(target, &TreeParentId::from(parent), index)
a.generate_position_at(target, parent, index)
})
}
@ -825,20 +832,20 @@ impl TreeHandler {
}
}
fn delete_position(&self, parent: Option<TreeID>, target: TreeID) {
fn delete_position(&self, parent: &TreeParentId, target: &TreeID) {
let MaybeDetached::Attached(a) = &self.inner else {
unreachable!()
};
a.with_state(|state| {
let a = state.as_tree_state_mut().unwrap();
a.delete_position(&TreeParentId::from(parent), target)
a.delete_position(parent, &target)
})
}
// use for apply diff
pub(crate) fn get_index_by_fractional_index(
&self,
parent: Option<TreeID>,
parent: &TreeParentId,
node_position: &NodePosition,
) -> Option<usize> {
match &self.inner {
@ -847,7 +854,7 @@ impl TreeHandler {
}
MaybeDetached::Attached(a) => a.with_state(|state| {
let a = state.as_tree_state().unwrap();
a.get_index_by_position(&TreeParentId::from(parent), node_position)
a.get_index_by_position(parent, node_position)
}),
}
}
@ -860,4 +867,51 @@ impl TreeHandler {
MaybeDetached::Attached(a) => a.with_txn(|txn| Ok(txn.next_idlp())).unwrap(),
}
}
pub fn is_fractional_index_enabled(&self) -> bool {
match &self.inner {
MaybeDetached::Detached(_) => {
unreachable!()
}
MaybeDetached::Attached(a) => a.with_state(|state| {
let a = state.as_tree_state().unwrap();
a.is_fractional_index_enabled()
}),
}
}
/// Set whether to generate fractional index for Tree Position. The LoroDoc is set to disable fractional index by default.
///
/// The jitter is used to avoid conflicts when multiple users are creating the node at the same position.
/// value 0 is default, which means no jitter, any value larger than 0 will enable jitter.
///
/// Generally speaking, jitter will affect the growth rate of document size.
/// [Read more about it](https://www.loro.dev/blog/movable-tree#implementation-and-encoding-size)
pub fn set_enable_fractional_index(&self, jitter: u8) {
match &self.inner {
MaybeDetached::Detached(_) => {
unreachable!()
}
MaybeDetached::Attached(a) => a.with_state(|state| {
let a = state.as_tree_state_mut().unwrap();
a.enable_generate_fractional_index(jitter);
}),
}
}
/// Disable the fractional index generation for Tree Position when
/// you don't need the Tree's siblings to be sorted. The fractional index will be always default.
///
/// The LoroDoc is set to disable fractional index by default.
pub fn set_disable_fractional_index(&self) {
match &self.inner {
MaybeDetached::Detached(_) => {
unreachable!()
}
MaybeDetached::Attached(a) => a.with_state(|state| {
let a = state.as_tree_state_mut().unwrap();
a.disable_generate_fractional_index();
}),
}
}
}

View file

@ -21,6 +21,7 @@ pub use loro::LoroDoc;
pub use loro_common;
pub use oplog::OpLog;
pub use state::DocState;
pub use state::TreeParentId;
pub use undo::UndoManager;
pub mod awareness;
pub mod cursor;

View file

@ -31,7 +31,7 @@ use crate::{
decode_snapshot, export_fast_snapshot, export_snapshot, json_schema::json::JsonSchema,
parse_header_and_body, EncodeMode, ParsedHeaderAndBody,
},
event::{str_to_path, EventTriggerKind, Index, InternalDocDiff, Path},
event::{str_to_path, EventTriggerKind, Index, InternalDocDiff},
handler::{Handler, MovableListHandler, TextHandler, TreeHandler, ValueOrHandler},
id::PeerID,
obs::{Observer, SubID, Subscriber},
@ -172,22 +172,6 @@ impl LoroDoc {
self.config.set_merge_interval(interval);
}
/// Set whether to use fractional index for Tree Position.
#[inline]
pub fn set_with_fractional_index(&self, with_fractional_index: bool) {
self.config.set_with_fractional_index(with_fractional_index);
}
/// Set the jitter of the tree position(Fractional Index).
///
/// The jitter is used to avoid conflicts when multiple users are creating the node at the same position.
/// value 0 is default, which means no jitter, any value larger than 0 will enable jitter.
/// Generally speaking, jitter will affect the growth rate of document size.
#[inline]
pub fn set_fractional_index_jitter(&self, jitter: u8) {
self.config.set_fractional_index_jitter(jitter);
}
#[inline]
pub fn config_text_style(&self, text_style: StyleConfigMap) {
*self.config.text_style_config.try_write().unwrap() = text_style;

View file

@ -7,7 +7,7 @@ use std::cell::RefCell;
use std::cmp::Ordering;
use std::rc::Rc;
use std::sync::Mutex;
use tracing::{debug, instrument, trace, trace_span};
use tracing::{debug, trace, trace_span};
use self::change_store::iter::MergedChangeIter;
use self::pending_changes::PendingChanges;

View file

@ -1320,8 +1320,8 @@ impl ChangesBlockBytes {
#[cfg(test)]
mod test {
use crate::{
oplog::convert_change_to_remote, ListHandler, LoroDoc, MovableListHandler, TextHandler,
TreeHandler,
oplog::convert_change_to_remote, state::TreeParentId, ListHandler, LoroDoc,
MovableListHandler, TextHandler, TreeHandler,
};
use super::*;
@ -1399,10 +1399,10 @@ mod test {
let tree = map
.insert_container("tree", TreeHandler::new_detached())
.unwrap();
let node_id = tree.create(None)?;
let node_id = tree.create(TreeParentId::Root)?;
tree.get_meta(node_id)?.insert("key", "value")?;
let node_b = tree.create(None)?;
tree.move_to(node_b, None, 0).unwrap();
let node_b = tree.create(TreeParentId::Root)?;
tree.move_to(node_b, TreeParentId::Root, 0).unwrap();
let movable_list = map
.insert_container("movable", MovableListHandler::new_detached())

View file

@ -1,9 +1,8 @@
use std::{
borrow::Cow,
collections::BTreeMap,
io::Write,
sync::{
atomic::{AtomicBool, AtomicU64, AtomicU8, Ordering},
atomic::{AtomicU64, Ordering},
Arc, Mutex, RwLock, Weak,
},
};
@ -15,7 +14,7 @@ use fxhash::{FxHashMap, FxHashSet};
use itertools::Itertools;
use loro_common::{ContainerID, LoroError, LoroResult};
use loro_delta::DeltaItem;
use tracing::{info_span, instrument, trace};
use tracing::{info_span, instrument};
use crate::{
configure::{Configure, DefaultRandom, SecureRandomGenerator},
@ -49,9 +48,8 @@ pub(crate) use self::movable_list_state::{IndexType, MovableListState};
pub(crate) use list_state::ListState;
pub(crate) use map_state::MapState;
pub(crate) use richtext_state::RichtextState;
pub(crate) use tree_state::{
get_meta_value, FractionalIndexGenResult, NodePosition, TreeParentId, TreeState,
};
pub use tree_state::TreeParentId;
pub(crate) use tree_state::{get_meta_value, FractionalIndexGenResult, NodePosition, TreeState};
use self::{container_store::ContainerWrapper, unknown_state::UnknownState};
@ -310,18 +308,8 @@ impl State {
Self::RichtextState(Box::new(RichtextState::new(idx, config)))
}
pub fn new_tree(
idx: ContainerIdx,
peer: PeerID,
jitter: Arc<AtomicU8>,
with_fractional_index: Arc<AtomicBool>,
) -> Self {
Self::TreeState(Box::new(TreeState::new(
idx,
peer,
jitter,
with_fractional_index,
)))
pub fn new_tree(idx: ContainerIdx, peer: PeerID) -> Self {
Self::TreeState(Box::new(TreeState::new(idx, peer)))
}
pub fn new_unknown(idx: ContainerIdx) -> Self {
@ -1482,12 +1470,7 @@ fn create_state_(idx: ContainerIdx, config: &Configure, peer: u64) -> State {
idx,
config.text_style_config.clone(),
))),
ContainerType::Tree => State::TreeState(Box::new(TreeState::new(
idx,
peer,
config.tree_position_jitter.clone(),
config.tree_with_fractional_index.clone(),
))),
ContainerType::Tree => State::TreeState(Box::new(TreeState::new(idx, peer))),
ContainerType::MovableList => State::MovableListState(Box::new(MovableListState::new(idx))),
#[cfg(feature = "counter")]
ContainerType::Counter => {

View file

@ -1,16 +1,12 @@
#[cfg(feature = "counter")]
use super::counter_state::CounterState;
use super::{ContainerCreationContext, MovableListState, State, TreeState};
use super::{ContainerCreationContext, State};
use crate::{
arena::SharedArena,
configure::Configure,
container::idx::ContainerIdx,
state::{FastStateSnapshot, RichtextState},
};
use bytes::Bytes;
use fxhash::FxHashMap;
use inner_store::InnerStore;
use loro_common::{ContainerID, ContainerType, LoroResult, LoroValue};
use loro_common::{LoroResult, LoroValue};
use std::sync::{atomic::AtomicU64, Arc};
pub(crate) use container_wrapper::ContainerWrapper;
@ -443,7 +439,7 @@ mod encode {
#[cfg(test)]
mod test {
use super::*;
use crate::{ListHandler, LoroDoc, MapHandler, MovableListHandler};
use crate::{state::TreeParentId, ListHandler, LoroDoc, MapHandler, MovableListHandler};
fn decode_container_store(bytes: Bytes) -> ContainerStore {
let mut new_store = ContainerStore::new(
@ -468,8 +464,8 @@ mod test {
list.push("item1").unwrap();
let tree = doc.get_tree("tree");
let root = tree.create(None).unwrap();
tree.create_at(Some(root), 0).unwrap();
let root = tree.create(TreeParentId::Root).unwrap();
tree.create_at(TreeParentId::Node(root), 0).unwrap();
let movable_list = doc.get_movable_list("movable_list");
movable_list.insert(0, "movable_item").unwrap();

View file

@ -18,8 +18,7 @@ use crate::{
handler::ValueOrHandler,
op::{ListSlice, Op, RawOp},
state::movable_list_state::inner::PushElemInfo,
txn::Transaction,
ApplyDiff, DocState, ListDiff,
txn::Transaction, DocState, ListDiff,
};
use self::{

View file

@ -13,7 +13,6 @@ use serde::Serialize;
use std::collections::VecDeque;
use std::fmt::Debug;
use std::ops::{Deref, DerefMut};
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
use std::sync::{Arc, Mutex, Weak};
use super::{ContainerState, DiffApplyContext};
@ -33,6 +32,16 @@ use crate::{
op::RawOp,
};
#[derive(Clone, Debug, Default, EnumAsInner)]
pub enum TreeFractionalIndexConfigInner {
GenerateFractionalIndex {
jitter: u8,
rng: Box<rand::rngs::StdRng>,
},
#[default]
AlwaysDefault,
}
/// The state of movable tree.
///
/// using flat representation
@ -41,10 +50,8 @@ pub struct TreeState {
idx: ContainerIdx,
trees: FxHashMap<TreeID, TreeStateNode>,
children: TreeChildrenCache,
/// Whether the tree has fractional index. If false, the fractional index is always [`FractionalIndex::default`]
with_fractional_index: Arc<AtomicBool>,
rng: Option<rand::rngs::StdRng>,
jitter: Arc<AtomicU8>,
fractional_index_config: TreeFractionalIndexConfigInner,
peer_id: PeerID,
}
#[derive(Debug, Clone, PartialEq, Eq)]
@ -88,8 +95,28 @@ impl From<Option<TreeID>> for TreeParentId {
}
}
impl From<TreeID> for TreeParentId {
fn from(id: TreeID) -> Self {
if TreeID::is_deleted_root(&id) {
TreeParentId::Deleted
} else {
TreeParentId::Node(id)
}
}
}
impl From<&TreeID> for TreeParentId {
fn from(id: &TreeID) -> Self {
if TreeID::is_deleted_root(id) {
TreeParentId::Deleted
} else {
TreeParentId::Node(*id)
}
}
}
impl TreeParentId {
fn id(&self) -> Option<TreeID> {
pub fn tree_id(&self) -> Option<TreeID> {
match self {
TreeParentId::Node(id) => Some(*id),
TreeParentId::Root => None,
@ -599,22 +626,13 @@ impl NodePosition {
}
impl TreeState {
pub fn new(
idx: ContainerIdx,
peer_id: PeerID,
jitter_config: Arc<AtomicU8>,
with_fractional_index: Arc<AtomicBool>,
) -> Self {
let jitter = jitter_config.load(Ordering::Relaxed);
let use_jitter = jitter != 1;
pub fn new(idx: ContainerIdx, peer_id: PeerID) -> Self {
Self {
idx,
trees: FxHashMap::default(),
children: Default::default(),
with_fractional_index,
rng: use_jitter.then_some(rand::rngs::StdRng::seed_from_u64(peer_id)),
jitter: jitter_config,
fractional_index_config: TreeFractionalIndexConfigInner::default(),
peer_id,
}
}
@ -638,7 +656,7 @@ impl TreeState {
}
if let Some(old_parent) = self.trees.get(&target).map(|x| x.parent) {
// remove old position
self.delete_position(&old_parent, target);
self.delete_position(&old_parent, &target);
}
let entry = self.children.entry(parent).or_default();
@ -686,11 +704,8 @@ impl TreeState {
}
/// Get the parent of the node, if the node is deleted or does not exist, return None
pub fn parent(&self, target: &TreeID) -> TreeParentId {
self.trees
.get(target)
.map(|x| x.parent)
.unwrap_or(TreeParentId::Unexist)
pub fn parent(&self, target: &TreeID) -> Option<TreeParentId> {
self.trees.get(target).map(|x| x.parent)
}
/// If the node exists and is not deleted, return false.
@ -720,7 +735,11 @@ impl TreeState {
let children = self.children.get(&root);
let mut q = children
.map(|x| {
VecDeque::from_iter(x.iter().enumerate().zip(std::iter::repeat(None::<TreeID>)))
VecDeque::from_iter(
x.iter()
.enumerate()
.zip(std::iter::repeat(TreeParentId::Root)),
)
})
.unwrap_or_default();
@ -737,7 +756,7 @@ impl TreeState {
.iter()
.enumerate()
.map(|(index, (position, this_target))| {
((index, (position, this_target)), Some(target))
((index, (position, this_target)), TreeParentId::Node(target))
}),
);
}
@ -758,7 +777,7 @@ impl TreeState {
for (index, (position, target)) in children.iter().enumerate() {
ans.push(TreeNode {
id: *target,
parent: root.id(),
parent: root,
position: position.position.clone(),
index,
});
@ -815,9 +834,9 @@ impl TreeState {
}
/// Delete the position cache of the node
pub(crate) fn delete_position(&mut self, parent: &TreeParentId, target: TreeID) {
pub(crate) fn delete_position(&mut self, parent: &TreeParentId, target: &TreeID) {
if let Some(x) = self.children.get_mut(parent) {
x.delete_child(&target);
x.delete_child(target);
}
}
@ -827,21 +846,49 @@ impl TreeState {
parent: &TreeParentId,
index: usize,
) -> FractionalIndexGenResult {
if !self.with_fractional_index.load(Ordering::Relaxed) {
return FractionalIndexGenResult::Ok(FractionalIndex::default());
match &mut self.fractional_index_config {
TreeFractionalIndexConfigInner::GenerateFractionalIndex { jitter, rng } => {
if *jitter == 0 {
self.children
.entry(*parent)
.or_default()
.generate_fi_at(index, target)
} else {
self.children
.entry(*parent)
.or_default()
.generate_fi_at_jitter(index, target, rng.as_mut(), *jitter)
}
}
TreeFractionalIndexConfigInner::AlwaysDefault => {
FractionalIndexGenResult::Ok(FractionalIndex::default())
}
}
}
if let Some(rng) = self.rng.as_mut() {
self.children
.entry(*parent)
.or_default()
.generate_fi_at_jitter(index, target, rng, self.jitter.load(Ordering::Relaxed))
} else {
self.children
.entry(*parent)
.or_default()
.generate_fi_at(index, target)
pub(crate) fn is_fractional_index_enabled(&self) -> bool {
!matches!(
self.fractional_index_config,
TreeFractionalIndexConfigInner::AlwaysDefault
)
}
pub(crate) fn enable_generate_fractional_index(&mut self, jitter: u8) {
if let TreeFractionalIndexConfigInner::GenerateFractionalIndex {
jitter: old_jitter, ..
} = &mut self.fractional_index_config
{
*old_jitter = jitter;
return;
}
self.fractional_index_config = TreeFractionalIndexConfigInner::GenerateFractionalIndex {
jitter,
rng: Box::new(rand::rngs::StdRng::seed_from_u64(self.peer_id)),
};
}
pub(crate) fn disable_generate_fractional_index(&mut self) {
self.fractional_index_config = TreeFractionalIndexConfigInner::AlwaysDefault;
}
pub(crate) fn get_position(&self, target: &TreeID) -> Option<FractionalIndex> {
@ -849,7 +896,7 @@ impl TreeState {
}
pub(crate) fn get_index_by_tree_id(&self, target: &TreeID) -> Option<usize> {
let parent = self.parent(target);
let parent = self.parent(target)?;
(!parent.is_deleted())
.then(|| {
self.children
@ -922,13 +969,15 @@ impl ContainerState for TreeState {
ans.push(TreeDiffItem {
target,
action: TreeExternalDiff::Create {
parent: parent.into_node().ok(),
parent: *parent,
index,
position: position.clone(),
},
});
}
TreeInternalDiff::Move { parent, position } => {
let old_parent = self.trees.get(&target).unwrap().parent;
let old_index = self.get_index_by_tree_id(&target).unwrap();
if need_check {
let was_alive = !self.is_node_deleted(&target);
if self
@ -940,7 +989,10 @@ impl ContainerState for TreeState {
// delete event
ans.push(TreeDiffItem {
target,
action: TreeExternalDiff::Delete,
action: TreeExternalDiff::Delete {
old_parent,
old_index,
},
});
}
// Otherwise, it's a normal move inside deleted nodes, no event is needed
@ -949,9 +1001,11 @@ impl ContainerState for TreeState {
ans.push(TreeDiffItem {
target,
action: TreeExternalDiff::Move {
parent: parent.into_node().ok(),
parent: *parent,
index: self.get_index_by_tree_id(&target).unwrap(),
position: position.clone(),
old_parent,
old_index,
},
});
} else {
@ -959,7 +1013,7 @@ impl ContainerState for TreeState {
ans.push(TreeDiffItem {
target,
action: TreeExternalDiff::Create {
parent: parent.into_node().ok(),
parent: *parent,
index: self.get_index_by_tree_id(&target).unwrap(),
position: position.clone(),
},
@ -974,9 +1028,11 @@ impl ContainerState for TreeState {
ans.push(TreeDiffItem {
target,
action: TreeExternalDiff::Move {
parent: parent.into_node().ok(),
parent: *parent,
index,
position: position.clone(),
old_parent,
old_index,
},
});
};
@ -986,15 +1042,17 @@ impl ContainerState for TreeState {
if need_check && self.is_node_deleted(&target) {
send_event = false;
}
self.mov(target, *parent, last_move_op, position.clone(), false)
.unwrap();
if send_event {
ans.push(TreeDiffItem {
target,
action: TreeExternalDiff::Delete,
action: TreeExternalDiff::Delete {
old_parent: self.trees.get(&target).unwrap().parent,
old_index: self.get_index_by_tree_id(&target).unwrap(),
},
});
}
self.mov(target, *parent, last_move_op, position.clone(), false)
.unwrap();
}
TreeInternalDiff::MoveInDelete { parent, position } => {
self.mov(target, *parent, last_move_op, position.clone(), false)
@ -1005,7 +1063,10 @@ impl ContainerState for TreeState {
if !self.is_node_deleted(&target) {
ans.push(TreeDiffItem {
target,
action: TreeExternalDiff::Delete,
action: TreeExternalDiff::Delete {
old_parent: self.trees.get(&target).unwrap().parent,
old_index: self.get_index_by_tree_id(&target).unwrap(),
},
});
}
// delete it from state
@ -1041,7 +1102,9 @@ impl ContainerState for TreeState {
// create associated metadata container
match &diff.action {
TreeInternalDiff::Create { parent, position }
| TreeInternalDiff::Move { parent, position } => {
| TreeInternalDiff::Move {
parent, position, ..
} => {
if need_check {
self.mov(target, *parent, last_move_op, Some(position.clone()), true)
.unwrap_or_default();
@ -1130,7 +1193,7 @@ impl ContainerState for TreeState {
let diff = TreeDiffItem {
target: *node,
action: TreeExternalDiff::Create {
parent: node_parent.into_node().ok(),
parent: node_parent,
index,
position: position.position.clone(),
},
@ -1241,7 +1304,7 @@ pub(crate) fn get_meta_value(nodes: &mut Vec<LoroValue>, state: &mut DocState) {
pub(crate) struct TreeNode {
pub(crate) id: TreeID,
pub(crate) parent: Option<TreeID>,
pub(crate) parent: TreeParentId,
pub(crate) position: FractionalIndex,
pub(crate) index: usize,
}
@ -1252,6 +1315,7 @@ impl TreeNode {
t.insert("id".to_string(), self.id.to_string().into());
let p = self
.parent
.tree_id()
.map(|p| p.to_string().into())
.unwrap_or(LoroValue::Null);
t.insert("parent".to_string(), p);
@ -1431,16 +1495,12 @@ mod snapshot {
let n = state.trees.get(&node.id).unwrap();
let last_set_id = n.last_move_op;
nodes.push(EncodedTreeNode {
parent_idx_plus_two: node
.parent
.map(|p| {
if p.is_deleted_root() {
1
} else {
id_to_idx.get(&p).unwrap() + 2
}
})
.unwrap_or(0),
parent_idx_plus_two: match node.parent {
TreeParentId::Deleted => 1,
TreeParentId::Root => 0,
TreeParentId::Node(id) => id_to_idx.get(&id).unwrap() + 2,
TreeParentId::Unexist => unreachable!(),
},
last_set_peer_idx: peers.register(&last_set_id.peer),
last_set_counter: last_set_id.counter,
last_set_lamport: last_set_id.lamport,
@ -1496,12 +1556,7 @@ mod snapshot {
peers.push(PeerID::from_le_bytes(buf));
}
let mut tree = TreeState::new(
idx,
ctx.peer,
ctx.configure.tree_position_jitter.clone(),
ctx.configure.tree_with_fractional_index.clone(),
);
let mut tree = TreeState::new(idx, ctx.peer);
let encoded: EncodedTree = serde_columnar::from_bytes(bytes)?;
let fractional_indexes = PositionArena::decode(&encoded.fractional_indexes).unwrap();
let fractional_indexes = fractional_indexes.parse_to_positions();
@ -1556,10 +1611,10 @@ mod snapshot {
doc.set_peer_id(0).unwrap();
doc.start_auto_commit();
let tree = doc.get_tree("tree");
let a = tree.create(None).unwrap();
let b = tree.create(None).unwrap();
let _c = tree.create(None).unwrap();
tree.mov(b, a).unwrap();
let a = tree.create(TreeParentId::Root).unwrap();
let b = tree.create(TreeParentId::Root).unwrap();
let _c = tree.create(TreeParentId::Root).unwrap();
tree.mov(b, TreeParentId::Node(a)).unwrap();
let (bytes, value) = {
let mut doc_state = doc.app_state().lock().unwrap();
let tree_state = doc_state.get_tree("tree").unwrap();

View file

@ -1,12 +1,6 @@
use std::{
collections::BTreeMap,
ops::Bound,
sync::{Arc, Mutex},
};
use std::sync::{Arc, Mutex};
use bytes::Bytes;
use fxhash::FxHashMap;
use loro_common::ContainerID;
use loro_kv_store::MemKvStore;
use crate::kv_store::KvStore;

View file

@ -558,8 +558,12 @@ pub mod wasm {
position,
} => {
js_sys::Reflect::set(&obj, &"action".into(), &"create".into()).unwrap();
js_sys::Reflect::set(&obj, &"parent".into(), &JsValue::from(*parent))
.unwrap();
js_sys::Reflect::set(
&obj,
&"parent".into(),
&JsValue::from(parent.tree_id()),
)
.unwrap();
js_sys::Reflect::set(&obj, &"index".into(), &(*index).into()).unwrap();
js_sys::Reflect::set(
&obj,
@ -568,18 +572,34 @@ pub mod wasm {
)
.unwrap();
}
TreeExternalDiff::Delete { .. } => {
TreeExternalDiff::Delete {
old_parent,
old_index,
} => {
js_sys::Reflect::set(&obj, &"action".into(), &"delete".into()).unwrap();
js_sys::Reflect::set(
&obj,
&"old_parent".into(),
&JsValue::from(old_parent.tree_id()),
)
.unwrap();
js_sys::Reflect::set(&obj, &"old_index".into(), &(*old_index).into())
.unwrap();
}
TreeExternalDiff::Move {
parent,
index,
position,
..
old_parent,
old_index,
} => {
js_sys::Reflect::set(&obj, &"action".into(), &"move".into()).unwrap();
js_sys::Reflect::set(&obj, &"parent".into(), &JsValue::from(*parent))
.unwrap();
js_sys::Reflect::set(
&obj,
&"parent".into(),
&JsValue::from(parent.tree_id()),
)
.unwrap();
js_sys::Reflect::set(&obj, &"index".into(), &(*index).into()).unwrap();
js_sys::Reflect::set(
&obj,
@ -587,6 +607,14 @@ pub mod wasm {
&position.to_string().into(),
)
.unwrap();
js_sys::Reflect::set(
&obj,
&"old_parent".into(),
&JsValue::from(old_parent.tree_id()),
)
.unwrap();
js_sys::Reflect::set(&obj, &"old_index".into(), &(*old_index).into())
.unwrap();
}
}
array.push(&obj);

View file

@ -8,6 +8,7 @@ use loro_internal::{
handler::{Handler, TextDelta, ValueOrHandler},
version::Frontiers,
ApplyDiff, HandlerTrait, ListHandler, LoroDoc, MapHandler, TextHandler, ToJson, TreeHandler,
TreeParentId,
};
use serde_json::json;
@ -724,11 +725,11 @@ fn tree_checkout() {
doc_a.subscribe_root(Arc::new(|_e| {}));
doc_a.set_peer_id(1).unwrap();
let tree = doc_a.get_tree("root");
let id1 = tree.create(None).unwrap();
let id2 = tree.create(id1).unwrap();
let id1 = tree.create(TreeParentId::Root).unwrap();
let id2 = tree.create(TreeParentId::Node(id1)).unwrap();
let v1_state = tree.get_deep_value();
let v1 = doc_a.oplog_frontiers();
let _id3 = tree.create(id2).unwrap();
let _id3 = tree.create(TreeParentId::Node(id2)).unwrap();
let v2_state = tree.get_deep_value();
let v2 = doc_a.oplog_frontiers();
tree.delete(id2).unwrap();
@ -757,7 +758,7 @@ fn tree_checkout() {
);
doc_a.attach();
tree.create(None).unwrap();
tree.create(TreeParentId::Root).unwrap();
}
#[test]
@ -853,8 +854,8 @@ fn missing_event_when_checkout() {
let doc2 = LoroDoc::new_auto_commit();
let tree = doc2.get_tree("tree");
let node = tree.create_at(None, 0).unwrap();
let _ = tree.create_at(None, 0).unwrap();
let node = tree.create_at(TreeParentId::Root, 0).unwrap();
let _ = tree.create_at(TreeParentId::Root, 0).unwrap();
let meta = tree.get_meta(node).unwrap();
meta.insert("a", 0).unwrap();
doc.import(&doc2.export_from(&doc.oplog_vv())).unwrap();
@ -930,7 +931,7 @@ fn insert_attach_container() -> LoroResult<()> {
#[test]
fn tree_attach() {
let tree = TreeHandler::new_detached();
let id = tree.create(None).unwrap();
let id = tree.create(TreeParentId::Root).unwrap();
tree.get_meta(id).unwrap().insert("key", "value").unwrap();
let doc = LoroDoc::new_auto_commit();
doc.get_list("list").insert_container(0, tree).unwrap();

View file

@ -0,0 +1,25 @@
use loro_internal::{LoroDoc, TreeParentId};
#[test]
fn tree_index() {
let doc = LoroDoc::new_auto_commit();
doc.set_peer_id(0).unwrap();
let tree = doc.get_tree("tree");
let root = tree.create(TreeParentId::Root).unwrap();
let child = tree.create(root.into()).unwrap();
let child2 = tree.create_at(root.into(), 0).unwrap();
// sort with OpID
assert_eq!(tree.get_index_by_tree_id(&child).unwrap(), 0);
assert_eq!(tree.get_index_by_tree_id(&child2).unwrap(), 1);
let doc = LoroDoc::new_auto_commit();
doc.set_peer_id(0).unwrap();
let tree = doc.get_tree("tree");
tree.set_enable_fractional_index(0);
let root = tree.create(TreeParentId::Root).unwrap();
let child = tree.create(root.into()).unwrap();
let child2 = tree.create_at(root.into(), 0).unwrap();
// sort with fractional index
assert_eq!(tree.get_index_by_tree_id(&child).unwrap(), 1);
assert_eq!(tree.get_index_by_tree_id(&child2).unwrap(), 0);
}

View file

@ -24,7 +24,8 @@ use loro_internal::{
undo::{UndoItemMeta, UndoOrRedo},
version::Frontiers,
ContainerType, DiffEvent, FxHashMap, HandlerTrait, LoroDoc as LoroDocInner, LoroValue,
MovableListHandler, UndoManager as InnerUndoManager, VersionVector as InternalVersionVector,
MovableListHandler, TreeParentId, UndoManager as InnerUndoManager,
VersionVector as InternalVersionVector,
};
use rle::HasLength;
use serde::{Deserialize, Serialize};
@ -340,16 +341,6 @@ impl LoroDoc {
self.0.set_change_merge_interval(interval as i64);
}
/// Set the jitter of the tree position(Fractional Index).
///
/// The jitter is used to avoid conflicts when multiple users are creating the node at the same position.
/// value 0 is default, which means no jitter, any value larger than 0 will enable jitter.
/// Generally speaking, jitter will affect the growth rate of document size.
#[wasm_bindgen(js_name = "setFractionalIndexJitter")]
pub fn set_fractional_index_jitter(&self, jitter: u8) {
self.0.set_fractional_index_jitter(jitter);
}
/// Set the rich text format configuration of the document.
///
/// You need to config it if you use rich text `mark` method.
@ -3043,9 +3034,9 @@ impl LoroTreeNode {
#[wasm_bindgen(js_name = "createNode", skip_typescript)]
pub fn create_node(&self, index: Option<usize>) -> JsResult<LoroTreeNode> {
let id = if let Some(index) = index {
self.tree.create_at(Some(self.id), index)?
self.tree.create_at(TreeParentId::Node(self.id), index)?
} else {
self.tree.create(Some(self.id))?
self.tree.create(TreeParentId::Node(self.id))?
};
let node = LoroTreeNode::from_tree(id, self.tree.clone(), self.doc.clone());
Ok(node)
@ -3075,9 +3066,10 @@ impl LoroTreeNode {
pub fn mov(&self, parent: &JsTreeNodeOrUndefined, index: Option<usize>) -> JsResult<()> {
let parent: Option<LoroTreeNode> = parse_js_tree_node(parent)?;
if let Some(index) = index {
self.tree.move_to(self.id, parent.map(|x| x.id), index)?
self.tree
.move_to(self.id, parent.map(|x| x.id).into(), index)?
} else {
self.tree.mov(self.id, parent.map(|x| x.id))?;
self.tree.mov(self.id, parent.map(|x| x.id).into())?;
}
Ok(())
@ -3169,9 +3161,15 @@ impl LoroTreeNode {
/// - The object returned is a new js object each time because it need to cross
/// the WASM boundary.
#[wasm_bindgen]
pub fn parent(&self) -> Option<LoroTreeNode> {
let parent = self.tree.get_node_parent(&self.id).flatten();
parent.map(|p| LoroTreeNode::from_tree(p, self.tree.clone(), self.doc.clone()))
pub fn parent(&self) -> JsResult<Option<LoroTreeNode>> {
let parent = self
.tree
.get_node_parent(&self.id)
.ok_or(JsValue::from_str(&format!("TreeID({}) not found", self.id)))?;
let ans = parent
.tree_id()
.map(|p| LoroTreeNode::from_tree(p, self.tree.clone(), self.doc.clone()));
Ok(ans)
}
/// Get the children of this node.
@ -3180,7 +3178,7 @@ impl LoroTreeNode {
/// the WASM boundary.
#[wasm_bindgen(skip_typescript)]
pub fn children(&self) -> JsValue {
let Some(children) = self.tree.children(Some(self.id)) else {
let Some(children) = self.tree.children(&TreeParentId::Node(self.id)) else {
return JsValue::undefined();
};
let children = children.into_iter().map(|c| {
@ -3236,9 +3234,9 @@ impl LoroTree {
) -> JsResult<LoroTreeNode> {
let parent: Option<TreeID> = parse_js_parent(parent)?;
let id = if let Some(index) = index {
self.handler.create_at(parent, index)?
self.handler.create_at(parent.into(), index)?
} else {
self.handler.create(parent)?
self.handler.create(parent.into())?
};
let node = LoroTreeNode::from_tree(id, self.handler.clone(), self.doc.clone());
Ok(node)
@ -3272,9 +3270,9 @@ impl LoroTree {
let parent = parse_js_parent(parent)?;
if let Some(index) = index {
self.handler.move_to(target, parent, index)?
self.handler.move_to(target, parent.into(), index)?
} else {
self.handler.mov(target, parent)?
self.handler.mov(target, parent.into())?
};
Ok(())
@ -3533,6 +3531,25 @@ impl LoroTree {
JsValue::UNDEFINED.into()
}
}
/// Set whether to generate fractional index for Tree Position.
///
/// The jitter is used to avoid conflicts when multiple users are creating the node at the same position.
/// value 0 is default, which means no jitter, any value larger than 0 will enable jitter.
///
/// Generally speaking, jitter will affect the growth rate of document size.
/// [Read more about it](https://www.loro.dev/blog/movable-tree#implementation-and-encoding-size)
#[wasm_bindgen(js_name = "setEnableFractionalIndex")]
pub fn set_enable_fractional_index(&self, jitter: u8) {
self.handler.set_enable_fractional_index(jitter);
}
/// Disable the fractional index generation for Tree Position when
/// you don't need the Tree's siblings to be sorted. The fractional index will be always default.
#[wasm_bindgen(js_name = "setDisableFractionalIndex")]
pub fn set_disable_fractional_index(&self) {
self.handler.set_disable_fractional_index();
}
}
impl Default for LoroTree {

View file

@ -2,7 +2,7 @@ use std::{cmp::Ordering, sync::Arc};
use loro_internal::{
change::{Change, Lamport, Timestamp},
id::{Counter, ID},
id::ID,
version::Frontiers,
};

View file

@ -12,11 +12,12 @@ use loro_internal::cursor::Side;
use loro_internal::encoding::ImportBlobMetadata;
use loro_internal::handler::HandlerTrait;
use loro_internal::handler::ValueOrHandler;
use loro_internal::json::JsonChange;
use loro_internal::loro_common::LoroTreeError;
use loro_internal::undo::{OnPop, OnPush};
use loro_internal::DocState;
use loro_internal::LoroDoc as InnerLoroDoc;
use loro_internal::OpLog;
use loro_internal::TreeParentId;
use loro_internal::{
handler::Handler as InnerHandler, ListHandler as InnerListHandler,
MapHandler as InnerMapHandler, MovableListHandler as InnerMovableListHandler,
@ -155,22 +156,6 @@ impl LoroDoc {
self.doc.set_change_merge_interval(interval);
}
/// Set whether to use fractional index for Tree Position.
#[inline]
pub fn set_with_fractional_index(&self, with_fractional_index: bool) {
self.doc.set_with_fractional_index(with_fractional_index);
}
/// Set the jitter of the tree position(Fractional Index).
///
/// The jitter is used to avoid conflicts when multiple users are creating the node at the same position.
/// value 0 is default, which means no jitter, any value larger than 0 will enable jitter.
/// Generally speaking, jitter will affect the growth rate of document size.
#[inline]
pub fn set_fractional_index_jitter(&self, jitter: u8) {
self.doc.set_fractional_index_jitter(jitter);
}
/// Set the rich text format configuration of the document.
///
/// You need to config it if you use rich text `mark` method.
@ -1422,10 +1407,8 @@ impl LoroTree {
/// // create a new child
/// let child = tree.create(root).unwrap();
/// ```
pub fn create<T: Into<Option<TreeID>>>(&self, parent: T) -> LoroResult<TreeID> {
let parent = parent.into();
let index = self.children_num(parent).unwrap_or(0);
self.handler.create_at(parent, index)
pub fn create<T: Into<TreeParentId>>(&self, parent: T) -> LoroResult<TreeID> {
self.handler.create(parent.into())
}
/// Get the root nodes of the forest.
@ -1445,17 +1428,18 @@ impl LoroTree {
///
/// let doc = LoroDoc::new();
/// let tree = doc.get_tree("tree");
/// // enable generate fractional index
/// tree.set_enable_fractional_index(0);
/// // create a root
/// let root = tree.create(None).unwrap();
/// // create a new child at index 0
/// let child = tree.create_at(root, 0).unwrap();
/// ```
pub fn create_at<T: Into<Option<TreeID>>>(
&self,
parent: T,
index: usize,
) -> LoroResult<TreeID> {
self.handler.create_at(parent, index)
pub fn create_at<T: Into<TreeParentId>>(&self, parent: T, index: usize) -> LoroResult<TreeID> {
if !self.handler.is_fractional_index_enabled() {
return Err(LoroTreeError::FractionalIndexNotEnabled.into());
}
self.handler.create_at(parent.into(), index)
}
/// Move the `target` node to be a child of the `parent` node.
@ -1474,10 +1458,8 @@ impl LoroTree {
/// // move `root2` to be a child of `root`.
/// tree.mov(root2, root).unwrap();
/// ```
pub fn mov<T: Into<Option<TreeID>>>(&self, target: TreeID, parent: T) -> LoroResult<()> {
let parent = parent.into();
let index = self.children_num(parent).unwrap_or(0);
self.handler.move_to(target, parent, index)
pub fn mov<T: Into<TreeParentId>>(&self, target: TreeID, parent: T) -> LoroResult<()> {
self.handler.mov(target, parent.into())
}
/// Move the `target` node to be a child of the `parent` node at the given index.
@ -1490,19 +1472,23 @@ impl LoroTree {
///
/// let doc = LoroDoc::new();
/// let tree = doc.get_tree("tree");
/// // enable generate fractional index
/// tree.set_enable_fractional_index(0);
/// let root = tree.create(None).unwrap();
/// let root2 = tree.create(None).unwrap();
/// // move `root2` to be a child of `root` at index 0.
/// tree.mov_to(root2, root, 0).unwrap();
/// ```
pub fn mov_to<T: Into<Option<TreeID>>>(
pub fn mov_to<T: Into<TreeParentId>>(
&self,
target: TreeID,
parent: T,
to: usize,
) -> LoroResult<()> {
let parent = parent.into();
self.handler.move_to(target, parent, to)
if !self.handler.is_fractional_index_enabled() {
return Err(LoroTreeError::FractionalIndexNotEnabled.into());
}
self.handler.move_to(target, parent.into(), to)
}
/// Move the `target` node to be a child after the `after` node with the same parent.
@ -1514,12 +1500,17 @@ impl LoroTree {
///
/// let doc = LoroDoc::new();
/// let tree = doc.get_tree("tree");
/// // enable generate fractional index
/// tree.set_enable_fractional_index(0);
/// let root = tree.create(None).unwrap();
/// let root2 = tree.create(None).unwrap();
/// // move `root` to be a child after `root2`.
/// tree.mov_after(root, root2).unwrap();
/// ```
pub fn mov_after(&self, target: TreeID, after: TreeID) -> LoroResult<()> {
if !self.handler.is_fractional_index_enabled() {
return Err(LoroTreeError::FractionalIndexNotEnabled.into());
}
self.handler.mov_after(target, after)
}
@ -1532,12 +1523,17 @@ impl LoroTree {
///
/// let doc = LoroDoc::new();
/// let tree = doc.get_tree("tree");
/// // enable generate fractional index
/// tree.set_enable_fractional_index(0);
/// let root = tree.create(None).unwrap();
/// let root2 = tree.create(None).unwrap();
/// // move `root` to be a child before `root2`.
/// tree.mov_before(root, root2).unwrap();
/// ```
pub fn mov_before(&self, target: TreeID, before: TreeID) -> LoroResult<()> {
if !self.handler.is_fractional_index_enabled() {
return Err(LoroTreeError::FractionalIndexNotEnabled.into());
}
self.handler.mov_before(target, before)
}
@ -1582,7 +1578,7 @@ impl LoroTree {
///
/// - If the target node does not exist, return `None`.
/// - If the target node is a root node, return `Some(None)`.
pub fn parent(&self, target: TreeID) -> Option<Option<TreeID>> {
pub fn parent(&self, target: TreeID) -> Option<TreeParentId> {
self.handler.get_node_parent(&target)
}
@ -1599,13 +1595,14 @@ impl LoroTree {
/// Return all children of the target node.
///
/// If the parent node does not exist, return `None`.
pub fn children(&self, parent: Option<TreeID>) -> Option<Vec<TreeID>> {
self.handler.children(parent)
pub fn children<T: Into<TreeParentId>>(&self, parent: T) -> Option<Vec<TreeID>> {
self.handler.children(&parent.into())
}
/// Return the number of children of the target node.
pub fn children_num(&self, parent: Option<TreeID>) -> Option<usize> {
self.handler.children_num(parent)
pub fn children_num<T: Into<TreeParentId>>(&self, parent: T) -> Option<usize> {
let parent: TreeParentId = parent.into();
self.handler.children_num(&parent)
}
/// Return container id of the tree.
@ -1639,6 +1636,30 @@ impl LoroTree {
pub fn __internal__next_tree_id(&self) -> TreeID {
self.handler.__internal__next_tree_id()
}
/// Whether the fractional index is enabled.
pub fn is_fractional_index_enabled(&self) -> bool {
self.handler.is_fractional_index_enabled()
}
/// Set whether to generate fractional index for Tree Position.
///
/// The jitter is used to avoid conflicts when multiple users are creating the node at the same position.
/// value 0 is default, which means no jitter, any value larger than 0 will enable jitter.
///
/// Generally speaking, jitter will affect the growth rate of document size.
/// [Read more about it](https://www.loro.dev/blog/movable-tree#implementation-and-encoding-size)
#[inline]
pub fn set_enable_fractional_index(&self, jitter: u8) {
self.handler.set_enable_fractional_index(jitter);
}
/// Disable the fractional index generation for Tree Position when
/// you don't need the Tree's siblings to be sorted. The fractional index will be always default.
#[inline]
pub fn set_disable_fractional_index(&self) {
self.handler.set_disable_fractional_index();
}
}
impl Default for LoroTree {

View file

@ -93,15 +93,17 @@ export type TreeDiffItem =
action: "create";
parent: TreeID | undefined;
index: number;
position: string;
fractional_index: string;
}
| { target: TreeID; action: "delete" }
| { target: TreeID; action: "delete"; old_parent: TreeID | undefined; old_index: number }
| {
target: TreeID;
action: "move";
parent: TreeID | undefined;
index: number;
position: string;
fractional_index: string;
old_parent: TreeID | undefined;
old_index: number;
};
export type TreeDiff = {

View file

@ -1,5 +1,5 @@
import { assert, describe, expect, it} from "vitest";
import { LoroDoc, LoroTree, LoroTreeNode } from "../src";
import { LoroDoc, LoroTree, LoroTreeNode, TreeDiff } from "../src";
function assertEquals(a: any, b: any) {
expect(a).toStrictEqual(b);
@ -8,6 +8,7 @@ function assertEquals(a: any, b: any) {
describe("loro tree", () => {
const loro = new LoroDoc();
const tree = loro.getTree("root");
tree.setEnableFractionalIndex(0);
it("create", () => {
const root = tree.createNode();
@ -121,6 +122,7 @@ describe("loro tree", () => {
describe("loro tree node", ()=>{
const loro = new LoroDoc();
const tree = loro.getTree("root");
tree.setEnableFractionalIndex(0);
it("create", () => {
const root = tree.createNode();
@ -180,6 +182,26 @@ describe("loro tree node", ()=>{
assertEquals(child.index(), 1);
assertEquals(child2.index(), 0);
});
it("old parent", () => {
const root = tree.createNode();
const child = root.createNode();
const child2 = root.createNode();
loro.commit();
const subID = tree.subscribe((e)=>{
if(e.events[0].diff.type == "tree"){
const diff = e.events[0].diff as TreeDiff;
if (diff.diff[0].action == "move"){
assertEquals(diff.diff[0].old_parent, root.id);
assertEquals(diff.diff[0].old_index, 1);
}
}
});
child2.move(child);
loro.commit();
tree.unsubscribe(subID);
assertEquals(child2.parent()!.id, child.id);
});
});
function one_ms(): Promise<void> {