Skip to content

Commit

Permalink
feat: refine undo impl (#365)
Browse files Browse the repository at this point in the history
* feat: refine undo impl

- Add "unfo" origin for undo and redo event
- Allow users to skip certain local operations
- Skip undo/redo ops that're not visible to users

* feat: add returned bool value to indicate whether undo/redo is executed
  • Loading branch information
zxch3n committed May 21, 2024
1 parent c124efc commit 1a347ca
Show file tree
Hide file tree
Showing 14 changed files with 438 additions and 211 deletions.
94 changes: 77 additions & 17 deletions crates/loro-internal/src/loro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use fxhash::FxHashMap;
use itertools::Itertools;
use loro_common::{ContainerID, ContainerType, HasIdSpan, IdSpan, LoroResult, LoroValue, ID};
use rle::HasLength;
use tracing::{debug, info_span, instrument};
use tracing::{info_span, instrument};

use crate::{
arena::SharedArena,
Expand Down Expand Up @@ -295,26 +295,21 @@ impl LoroDoc {
/// Afterwards, the users need to call `self.renew_txn_after_commit()` to resume the continuous transaction.
#[inline]
pub fn commit_then_stop(&self) {
self.commit_with(None, None, false)
self.commit_with(CommitOptions::new().immediate_renew(false))
}

/// Commit the cumulative auto commit transaction.
/// It will start the next one immediately
#[inline]
pub fn commit_then_renew(&self) {
self.commit_with(None, None, true)
self.commit_with(CommitOptions::new().immediate_renew(true))
}

/// Commit the cumulative auto commit transaction.
/// This method only has effect when `auto_commit` is true.
/// If `immediate_renew` is true, a new transaction will be created after the old one is committed
#[instrument(skip_all)]
pub fn commit_with(
&self,
origin: Option<InternalString>,
timestamp: Option<Timestamp>,
immediate_renew: bool,
) {
pub fn commit_with(&self, config: CommitOptions) {
if !self.auto_commit.load(Acquire) {
// if not auto_commit, nothing should happen
// because the global txn is not used
Expand All @@ -329,16 +324,16 @@ impl LoroDoc {
};

let on_commit = txn.take_on_commit();
if let Some(origin) = origin {
if let Some(origin) = config.origin {
txn.set_origin(origin);
}

if let Some(timestamp) = timestamp {
if let Some(timestamp) = config.timestamp {
txn.set_timestamp(timestamp);
}

txn.commit().unwrap();
if immediate_renew {
if config.immediate_renew {
let mut txn_guard = self.txn.try_lock().unwrap();
assert!(!self.detached.load(std::sync::atomic::Ordering::Acquire));
*txn_guard = Some(self.txn().unwrap());
Expand Down Expand Up @@ -778,16 +773,18 @@ impl LoroDoc {
self.state.lock().unwrap().start_recording();
}
self.start_auto_commit();

self.apply_diff(diff, container_remap).unwrap();
Ok(CommitWhenDrop { doc: self })
self.apply_diff(diff, container_remap, true).unwrap();
Ok(CommitWhenDrop {
doc: self,
options: CommitOptions::new().origin("undo"),
})
}

/// Calculate the diff between the current state and the target state, and apply the diff to the current state.
pub fn diff_and_apply(&self, target: &Frontiers) -> LoroResult<()> {
let f = self.state_frontiers();
let diff = self.diff(&f, target)?;
self.apply_diff(diff, &mut Default::default())
self.apply_diff(diff, &mut Default::default(), false)
}

/// Calculate the diff between two versions so that apply diff on a will make the state same as b.
Expand Down Expand Up @@ -840,6 +837,7 @@ impl LoroDoc {
&self,
mut diff: DiffBatch,
container_remap: &mut FxHashMap<ContainerID, ContainerID>,
skip_unreachable: bool,
) -> LoroResult<()> {
if self.is_detached() {
return Err(LoroError::EditWhenDetached);
Expand All @@ -850,12 +848,20 @@ impl LoroDoc {
let idx = self.arena.id_to_idx(cid).unwrap();
self.arena.get_depth(idx).unwrap().get()
});

for mut id in containers {
let mut remapped = false;
let diff = diff.0.remove(&id).unwrap();

while let Some(rid) = container_remap.get(&id) {
remapped = true;
id = rid.clone();
}

if skip_unreachable && !remapped && !self.state.lock().unwrap().get_reachable(&id) {
continue;
}

let h = self.get_handler(id);
h.apply_diff(diff, &mut |old_id, new_id| {
container_remap.insert(old_id, new_id);
Expand Down Expand Up @@ -1276,11 +1282,65 @@ fn find_last_delete_op(oplog: &OpLog, id: ID, idx: ContainerIdx) -> Option<ID> {
#[derive(Debug)]
pub struct CommitWhenDrop<'a> {
doc: &'a LoroDoc,
options: CommitOptions,
}

impl<'a> Drop for CommitWhenDrop<'a> {
fn drop(&mut self) {
self.doc.commit_then_renew()
self.doc.commit_with(std::mem::take(&mut self.options));
}
}

#[derive(Debug, Clone)]
pub struct CommitOptions {
origin: Option<InternalString>,
immediate_renew: bool,
timestamp: Option<Timestamp>,
commit_msg: Option<Box<str>>,
}

impl CommitOptions {
pub fn new() -> Self {
Self {
origin: None,
immediate_renew: true,
timestamp: None,
commit_msg: None,
}
}

pub fn origin(mut self, origin: &str) -> Self {
self.origin = Some(origin.into());
self
}

pub fn immediate_renew(mut self, immediate_renew: bool) -> Self {
self.immediate_renew = immediate_renew;
self
}

pub fn timestamp(mut self, timestamp: Timestamp) -> Self {
self.timestamp = Some(timestamp);
self
}

pub fn commit_msg(mut self, commit_msg: &str) -> Self {
self.commit_msg = Some(commit_msg.into());
self
}

pub fn set_origin(&mut self, origin: Option<&str>) {
self.origin = origin.map(|x| x.into())
}

pub fn set_timestamp(&mut self, timestamp: Option<Timestamp>) {
self.timestamp = timestamp;
}
}

impl Default for CommitOptions {
fn default() -> Self {
Self::new()
}
}

Expand Down
1 change: 0 additions & 1 deletion crates/loro-internal/src/oplog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ use std::cell::RefCell;
use std::cmp::Ordering;
use std::mem::take;
use std::rc::Rc;
use tracing::debug;

use crate::change::{get_sys_timestamp, Change, Lamport, Timestamp};
use crate::configure::Configure;
Expand Down
31 changes: 31 additions & 0 deletions crates/loro-internal/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ pub(crate) trait ContainerState: Clone {
#[allow(unused)]
fn get_child_index(&self, id: &ContainerID) -> Option<Index>;

#[allow(unused)]
fn contains_child(&self, id: &ContainerID) -> bool;

#[allow(unused)]
fn get_child_containers(&self) -> Vec<ContainerID>;

Expand Down Expand Up @@ -194,6 +197,10 @@ impl<T: ContainerState> ContainerState for Box<T> {
self.as_ref().get_child_index(id)
}

fn contains_child(&self, id: &ContainerID) -> bool {
self.as_ref().contains_child(id)
}

#[allow(unused)]
fn get_child_containers(&self) -> Vec<ContainerID> {
self.as_ref().get_child_containers()
Expand Down Expand Up @@ -984,6 +991,30 @@ impl DocState {
}
}

pub(crate) fn get_reachable(&self, id: &ContainerID) -> bool {
let Some(mut idx) = self.arena.id_to_idx(id) else {
return false;
};
loop {
let id = self.arena.idx_to_id(idx).unwrap();
if let Some(parent_idx) = self.arena.get_parent(idx) {
let Some(parent_state) = self.states.get(&parent_idx) else {
return false;
};
if !parent_state.contains_child(&id) {
return false;
}
idx = parent_idx;
} else {
if id.is_root() {
return true;
}

return false;
}
}
}

// the container may be override, so it may return None
fn get_path(&self, idx: ContainerIdx) -> Option<Vec<(ContainerID, Index)>> {
let mut ans = Vec::new();
Expand Down
12 changes: 12 additions & 0 deletions crates/loro-internal/src/state/list_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,14 @@ impl ListState {
}
}

pub fn contains_child_container(&self, id: &ContainerID) -> bool {
let Some(&leaf) = self.child_container_to_leaf.get(id) else {
return false;
};

self.list.get_elem(leaf).is_some()
}

pub fn get_child_container_index(&self, id: &ContainerID) -> Option<usize> {
let leaf = *self.child_container_to_leaf.get(id)?;
self.list.get_elem(leaf)?;
Expand Down Expand Up @@ -472,6 +480,10 @@ impl ContainerState for ListState {
self.get_child_container_index(id).map(Index::Seq)
}

fn contains_child(&self, id: &ContainerID) -> bool {
self.contains_child_container(id)
}

fn get_child_containers(&self) -> Vec<ContainerID> {
let mut ans = Vec::new();
for elem in self.list.iter() {
Expand Down
19 changes: 12 additions & 7 deletions crates/loro-internal/src/state/map_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,24 +130,29 @@ impl ContainerState for MapState {
}

fn get_child_index(&self, id: &ContainerID) -> Option<Index> {
let s = tracing::span!(tracing::Level::INFO, "Get child index ", id = ?id);
let _e = s.enter();
for (key, value) in self.map.iter() {
let s = tracing::span!(tracing::Level::INFO, "Key Value", key = ?key, value = ?value);
let _e = s.enter();
if let Some(LoroValue::Container(x)) = &value.value {
tracing::info!("Cmp {:?} with {:?}", &x, &id);
if x == id {
tracing::info!("Same");
return Some(Index::Key(key.clone()));
}
tracing::info!("Diff");
}
}

None
}

fn contains_child(&self, id: &ContainerID) -> bool {
for (_, value) in self.map.iter() {
if let Some(LoroValue::Container(x)) = &value.value {
if x == id {
return true;
}
}
}

false
}

fn get_child_containers(&self) -> Vec<ContainerID> {
let mut ans = Vec::new();
for (_, value) in self.map.iter() {
Expand Down
8 changes: 8 additions & 0 deletions crates/loro-internal/src/state/movable_list_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,10 @@ mod inner {
ans
}

pub fn contains_child_container(&self, id: &ContainerID) -> bool {
self.get_child_index(id, IndexType::ForUser).is_some()
}

#[inline]
pub fn get_list_item_by_id(&self, id: IdLp) -> Option<&ListItem> {
self.id_to_list_leaf
Expand Down Expand Up @@ -1283,6 +1287,10 @@ impl ContainerState for MovableListState {
.map(Index::Seq)
}

fn contains_child(&self, id: &ContainerID) -> bool {
self.inner.contains_child_container(id)
}

#[allow(unused)]
fn get_child_containers(&self) -> Vec<ContainerID> {
self.inner
Expand Down
4 changes: 4 additions & 0 deletions crates/loro-internal/src/state/richtext_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,10 @@ impl ContainerState for RichtextState {
Vec::new()
}

fn contains_child(&self, _id: &ContainerID) -> bool {
false
}

#[doc = " Get a list of ops that can be used to restore the state to the current state"]
fn encode_snapshot(&self, mut encoder: StateSnapshotEncoder) -> Vec<u8> {
let iter: &mut dyn Iterator<Item = &RichtextStateChunk>;
Expand Down
9 changes: 9 additions & 0 deletions crates/loro-internal/src/state/tree_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1020,6 +1020,15 @@ impl ContainerState for TreeState {
}
}

fn contains_child(&self, id: &ContainerID) -> bool {
let id = id.as_normal().unwrap();
let tree_id = TreeID {
peer: *id.0,
counter: *id.1,
};
self.trees.contains_key(&tree_id) && !self.is_node_deleted(&tree_id)
}

fn get_child_containers(&self) -> Vec<ContainerID> {
self.trees
.keys()
Expand Down
4 changes: 4 additions & 0 deletions crates/loro-internal/src/state/unknown_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ impl ContainerState for UnknownState {
None
}

fn contains_child(&self, _id: &ContainerID) -> bool {
false
}

#[allow(unused)]
fn get_child_containers(&self) -> Vec<ContainerID> {
vec![]
Expand Down

0 comments on commit 1a347ca

Please sign in to comment.