Skip to content

Commit

Permalink
feat: Add event listener and native support of cursor transformation …
Browse files Browse the repository at this point in the history
…for undo/redo (#369)
  • Loading branch information
zxch3n committed May 23, 2024
1 parent 6d5083c commit 6700dad
Show file tree
Hide file tree
Showing 16 changed files with 866 additions and 90 deletions.
49 changes: 49 additions & 0 deletions crates/delta/src/delta_rope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,55 @@ impl<V: DeltaValue, Attr: DeltaAttr> DeltaRope<V, Attr> {
pub fn transform_(&mut self, other: &Self, left_prior: bool) {
*self = self.transform(other, left_prior);
}

/// Transforms the position based on self
pub fn transform_pos(&self, mut pos: usize, left_prior: bool) -> usize {
let mut index = 0;
let mut iter = self.iter_with_len();
while iter.peek().is_some() {
if iter.peek_is_retain() {
let DeltaItem::Retain { len, attr: _ } = iter.next().unwrap() else {
unreachable!()
};
index += len;
if index > pos || (index == pos && !left_prior) {
return pos;
}
} else if iter.peek_is_insert() {
if index == pos && !left_prior {
return pos;
}

let insert_len;
match iter.peek().unwrap() {
DeltaItem::Replace { value, .. } => {
insert_len = value.rle_len();
}
_ => {
unreachable!()
}
}

pos += insert_len;
index += insert_len;
iter.next_with(insert_len).unwrap();
} else {
// Delete
match iter.next().unwrap() {
DeltaItem::Replace { delete, .. } => {
pos = pos.saturating_sub(delete);
if pos < index {
pos = index;
return pos;
}
}
DeltaItem::Retain { .. } => unreachable!(),
}
}
}

pos
}
}

impl<V: DeltaValue + PartialEq, Attr: DeltaAttr + PartialEq> PartialEq for DeltaRope<V, Attr> {
Expand Down
58 changes: 35 additions & 23 deletions crates/loro-internal/src/container/richtext/richtext_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1388,8 +1388,6 @@ impl RichtextState {
///
/// - If feature="wasm", index is utf16 index,
/// - If feature!="wasm", index is unicode index,
///
// PERF: this is slow
pub(crate) fn cursor_to_event_index(&self, cursor: Cursor) -> usize {
if cfg!(feature = "wasm") {
let mut ans = 0;
Expand All @@ -1416,29 +1414,31 @@ impl RichtextState {

ans
} else {
let mut ans = 0;
self.tree
.visit_previous_caches(cursor, |cache| match cache {
generic_btree::PreviousCache::NodeCache(c) => {
ans += c.unicode_len;
self.cursor_to_unicode_index(cursor)
}
}

pub(crate) fn cursor_to_unicode_index(&self, cursor: Cursor) -> usize {
let mut ans = 0;
self.tree
.visit_previous_caches(cursor, |cache| match cache {
generic_btree::PreviousCache::NodeCache(c) => {
ans += c.unicode_len;
}
generic_btree::PreviousCache::PrevSiblingElem(c) => match c {
RichtextStateChunk::Text(s) => {
ans += s.unicode_len();
}
generic_btree::PreviousCache::PrevSiblingElem(c) => match c {
RichtextStateChunk::Text(s) => {
ans += s.unicode_len();
}
RichtextStateChunk::Style { .. } => {}
},
generic_btree::PreviousCache::ThisElemAndOffset { elem, offset } => {
match elem {
RichtextStateChunk::Text { .. } => {
ans += offset as i32;
}
RichtextStateChunk::Style { .. } => {}
}
RichtextStateChunk::Style { .. } => {}
},
generic_btree::PreviousCache::ThisElemAndOffset { elem, offset } => match elem {
RichtextStateChunk::Text { .. } => {
ans += offset as i32;
}
});
ans as usize
}
RichtextStateChunk::Style { .. } => {}
},
});
ans as usize
}

/// This method only updates `style_ranges`.
Expand Down Expand Up @@ -1902,6 +1902,18 @@ impl RichtextState {
self.cursor_to_event_index(cursor.cursor)
}

pub fn event_index_to_unicode_index(&self, index: usize) -> usize {
if !cfg!(feature = "wasm") {
return index;
}

let Some(cursor) = self.tree.query::<EventIndexQuery>(&index) else {
return 0;
};

self.cursor_to_unicode_index(cursor.cursor)
}

#[allow(unused)]
pub(crate) fn check(&self) {
if !cfg!(any(debug_assertions, test)) {
Expand Down
7 changes: 6 additions & 1 deletion crates/loro-internal/src/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ pub struct Cursor {
///
/// Side info can help to model the selection
pub side: Side,
/// The position of the cursor in the container when the cursor is created.
/// For text, this is the unicode codepoint index
/// This value is not encoded
pub(crate) origin_pos: usize,
}

#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
Expand Down Expand Up @@ -59,11 +63,12 @@ pub enum CannotFindRelativePosition {
}

impl Cursor {
pub fn new(id: Option<ID>, container: ContainerID, side: Side) -> Self {
pub fn new(id: Option<ID>, container: ContainerID, side: Side, origin_pos: usize) -> Self {
Self {
id,
container,
side,
origin_pos,
}
}

Expand Down
11 changes: 11 additions & 0 deletions crates/loro-internal/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use itertools::Itertools;
use loro_delta::{array_vec::ArrayVec, delta_trait::DeltaAttr, DeltaItem, DeltaRope};
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use tracing::trace;

use crate::{
container::richtext::richtext_state::RichtextStateChunk,
Expand Down Expand Up @@ -453,6 +454,16 @@ impl Diff {
_ => unreachable!(),
}
}

/// Transform the cursor based on this diff
pub(crate) fn transform_cursor(&self, pos: usize, left_prior: bool) -> usize {
let ans = match self {
Diff::List(list) => list.transform_pos(pos, left_prior),
Diff::Text(text) => text.transform_pos(pos, left_prior),
_ => pos,
};
ans
}
}

pub fn str_to_path(s: &str) -> Option<Vec<Index>> {
Expand Down
38 changes: 34 additions & 4 deletions crates/loro-internal/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1728,14 +1728,35 @@ impl TextHandler {
}
}

/// Get the stable position representation for the target pos
pub fn get_cursor(&self, event_index: usize, side: Side) -> Option<Cursor> {
self.get_cursor_internal(event_index, side, true)
}

/// Get the stable position representation for the target pos
pub(crate) fn get_cursor_internal(
&self,
index: usize,
side: Side,
get_by_event_index: bool,
) -> Option<Cursor> {
match &self.inner {
MaybeDetached::Detached(_) => None,
MaybeDetached::Attached(a) => {
let (id, len) = a.with_state(|s| {
let (id, len, origin_pos) = a.with_state(|s| {
let s = s.as_richtext_state_mut().unwrap();
(s.get_stable_position(event_index), s.len_event())
(
s.get_stable_position(index, get_by_event_index),
if get_by_event_index {
s.len_event()
} else {
s.len_unicode()
},
if get_by_event_index {
s.event_index_to_unicode_index(index)
} else {
index
},
)
});

if len == 0 {
Expand All @@ -1747,14 +1768,16 @@ impl TextHandler {
} else {
side
},
origin_pos: 0,
});
}

if len <= event_index {
if len <= index {
return Some(Cursor {
id: None,
container: self.id(),
side: Side::Right,
origin_pos: len,
});
}

Expand All @@ -1763,6 +1786,7 @@ impl TextHandler {
id: Some(id),
container: self.id(),
side,
origin_pos,
})
}
}
Expand Down Expand Up @@ -2129,6 +2153,7 @@ impl ListHandler {
} else {
side
},
origin_pos: 0,
});
}

Expand All @@ -2137,6 +2162,7 @@ impl ListHandler {
id: None,
container: self.id(),
side: Side::Right,
origin_pos: len,
});
}

Expand All @@ -2145,6 +2171,7 @@ impl ListHandler {
id: Some(id.id()),
container: self.id(),
side,
origin_pos: pos,
})
}
}
Expand Down Expand Up @@ -2778,6 +2805,7 @@ impl MovableListHandler {
} else {
side
},
origin_pos: 0,
});
}

Expand All @@ -2786,6 +2814,7 @@ impl MovableListHandler {
id: None,
container: self.id(),
side: Side::Right,
origin_pos: len,
});
}

Expand All @@ -2794,6 +2823,7 @@ impl MovableListHandler {
id: Some(id.id()),
container: self.id(),
side,
origin_pos: pos,
})
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/loro-internal/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ pub use error::{LoroError, LoroResult};
pub(crate) mod group;
pub(crate) mod macros;
pub(crate) mod state;
pub(crate) mod undo;
pub mod undo;
pub(crate) mod value;
pub(crate) use id::{PeerID, ID};

Expand Down
15 changes: 13 additions & 2 deletions crates/loro-internal/src/loro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1112,10 +1112,18 @@ impl LoroDoc {
state.log_estimated_size();
}

/// Get position in a seq container
pub fn query_pos(&self, pos: &Cursor) -> Result<PosQueryResult, CannotFindRelativePosition> {
self.query_pos_internal(pos, true)
}

/// Get position in a seq container
pub(crate) fn query_pos_internal(
&self,
pos: &Cursor,
ret_event_index: bool,
) -> Result<PosQueryResult, CannotFindRelativePosition> {
let mut state = self.state.lock().unwrap();
if let Some(ans) = state.get_relative_position(pos) {
if let Some(ans) = state.get_relative_position(pos, ret_event_index) {
Ok(PosQueryResult {
update: None,
current: AbsolutePosition {
Expand Down Expand Up @@ -1215,6 +1223,7 @@ impl LoroDoc {
id: None,
container: text.id(),
side: pos.side,
origin_pos: text.len_unicode(),
}),
current: AbsolutePosition {
pos: text.len_event(),
Expand All @@ -1229,6 +1238,7 @@ impl LoroDoc {
id: None,
container: list.id(),
side: pos.side,
origin_pos: list.len(),
}),
current: AbsolutePosition {
pos: list.len(),
Expand All @@ -1243,6 +1253,7 @@ impl LoroDoc {
id: None,
container: list.id(),
side: pos.side,
origin_pos: list.len(),
}),
current: AbsolutePosition {
pos: list.len(),
Expand Down
10 changes: 7 additions & 3 deletions crates/loro-internal/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1181,13 +1181,13 @@ impl DocState {
State::UnknownState(Box::new(UnknownState::new(idx)))
}

pub fn get_relative_position(&mut self, pos: &Cursor) -> Option<usize> {
pub fn get_relative_position(&mut self, pos: &Cursor, use_event_index: bool) -> Option<usize> {
let idx = self.arena.register_container(&pos.container);
let state = self.states.get_mut(&idx)?;
if let Some(id) = pos.id {
match state {
State::ListState(s) => s.get_index_of_id(id),
State::RichtextState(s) => s.get_event_index_of_id(id),
State::RichtextState(s) => s.get_text_index_of_id(id, use_event_index),
State::MovableListState(s) => s.get_index_of_id(id),
State::MapState(_) | State::TreeState(_) | State::UnknownState(_) => unreachable!(),
#[cfg(feature = "counter")]
Expand All @@ -1200,7 +1200,11 @@ impl DocState {

match state {
State::ListState(s) => Some(s.len()),
State::RichtextState(s) => Some(s.len_event()),
State::RichtextState(s) => Some(if use_event_index {
s.len_event()
} else {
s.len_unicode()
}),
State::MovableListState(s) => Some(s.len()),
State::MapState(_) | State::TreeState(_) | State::UnknownState(_) => unreachable!(),
#[cfg(feature = "counter")]
Expand Down

0 comments on commit 6700dad

Please sign in to comment.