loro/crates/loro-internal/src/cursor.rs

83 lines
2.2 KiB
Rust
Raw Normal View History

Cursors (#290) This PR introduces support for retrieving and querying cursors. ## Motivation Using "index" to denote cursor positions can be unstable, as positions may shift with document edits. To reliably represent a position or range within a document, it is more effective to leverage the unique ID of each item/character in a List CRDT or Text CRDT. ## Updating Cursors Loro optimizes State metadata by not storing the IDs of deleted elements. This approach, while efficient, complicates tracking cursor positions since they rely on these IDs for precise locations within the document. The solution recalculates position by replaying relevant history to update stable positions accurately. To minimize the performance impact of history replay, the system updates cursor info to reference only the IDs of currently present elements, thereby reducing the need for replay. Each position has a "Side" information, indicating the actual cursor position is on the left, right, or directly in the center of the target ID. Note: In JavaScript, the offset returned when querying a Stable Position is based on the UTF-16 index. # Example ```ts const loro = new Loro(); const list = loro.getList("list"); list.insert(0, "a"); const pos0 = list.getStablePos(0); list.insert(1, "b"); { const ans = loro.queryStablePos(pos0!); expect(ans.offset).toEqual(0); expect(ans.side).toEqual(0); expect(ans.update).toBeUndefined(); } list.insert(0, "c"); { const ans = loro.queryStablePos(pos0!); expect(ans.offset).toEqual(1); expect(ans.side).toEqual(0); expect(ans.update).toBeUndefined(); } list.delete(1, 1); { const ans = loro.queryStablePos(pos0!); expect(ans.offset).toEqual(1); expect(ans.side).toEqual(-1); expect(ans.update).toBeDefined(); } ```
2024-04-09 08:01:37 +00:00
use loro_common::{ContainerID, ID};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Cursor {
// It's option because it's possible that the given container is empty.
pub id: Option<ID>,
pub container: ContainerID,
/// The target position is at the left, middle, or right of the given id.
///
/// 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,
Cursors (#290) This PR introduces support for retrieving and querying cursors. ## Motivation Using "index" to denote cursor positions can be unstable, as positions may shift with document edits. To reliably represent a position or range within a document, it is more effective to leverage the unique ID of each item/character in a List CRDT or Text CRDT. ## Updating Cursors Loro optimizes State metadata by not storing the IDs of deleted elements. This approach, while efficient, complicates tracking cursor positions since they rely on these IDs for precise locations within the document. The solution recalculates position by replaying relevant history to update stable positions accurately. To minimize the performance impact of history replay, the system updates cursor info to reference only the IDs of currently present elements, thereby reducing the need for replay. Each position has a "Side" information, indicating the actual cursor position is on the left, right, or directly in the center of the target ID. Note: In JavaScript, the offset returned when querying a Stable Position is based on the UTF-16 index. # Example ```ts const loro = new Loro(); const list = loro.getList("list"); list.insert(0, "a"); const pos0 = list.getStablePos(0); list.insert(1, "b"); { const ans = loro.queryStablePos(pos0!); expect(ans.offset).toEqual(0); expect(ans.side).toEqual(0); expect(ans.update).toBeUndefined(); } list.insert(0, "c"); { const ans = loro.queryStablePos(pos0!); expect(ans.offset).toEqual(1); expect(ans.side).toEqual(0); expect(ans.update).toBeUndefined(); } list.delete(1, 1); { const ans = loro.queryStablePos(pos0!); expect(ans.offset).toEqual(1); expect(ans.side).toEqual(-1); expect(ans.update).toBeDefined(); } ```
2024-04-09 08:01:37 +00:00
}
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Side {
Left = -1,
#[default]
Middle = 0,
Right = 1,
}
impl Side {
pub fn from_i32(i: i32) -> Option<Self> {
match i {
-1 => Some(Side::Left),
0 => Some(Side::Middle),
1 => Some(Side::Right),
_ => None,
}
}
pub fn to_i32(&self) -> i32 {
*self as i32
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PosQueryResult {
pub update: Option<Cursor>,
pub current: AbsolutePosition,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AbsolutePosition {
pub pos: usize,
/// The target position is at the left, middle, or right of the given pos.
pub side: Side,
}
#[derive(Debug, Clone, Copy, thiserror::Error)]
pub enum CannotFindRelativePosition {
#[error("Cannot find relative position. The container is deleted.")]
ContainerDeleted,
#[error("Cannot find relative position. It may be that the given id is deleted and the relative history is cleared.")]
HistoryCleared,
#[error("Cannot find relative position. The id is not found.")]
IdNotFound,
}
impl Cursor {
pub fn new(id: Option<ID>, container: ContainerID, side: Side, origin_pos: usize) -> Self {
Cursors (#290) This PR introduces support for retrieving and querying cursors. ## Motivation Using "index" to denote cursor positions can be unstable, as positions may shift with document edits. To reliably represent a position or range within a document, it is more effective to leverage the unique ID of each item/character in a List CRDT or Text CRDT. ## Updating Cursors Loro optimizes State metadata by not storing the IDs of deleted elements. This approach, while efficient, complicates tracking cursor positions since they rely on these IDs for precise locations within the document. The solution recalculates position by replaying relevant history to update stable positions accurately. To minimize the performance impact of history replay, the system updates cursor info to reference only the IDs of currently present elements, thereby reducing the need for replay. Each position has a "Side" information, indicating the actual cursor position is on the left, right, or directly in the center of the target ID. Note: In JavaScript, the offset returned when querying a Stable Position is based on the UTF-16 index. # Example ```ts const loro = new Loro(); const list = loro.getList("list"); list.insert(0, "a"); const pos0 = list.getStablePos(0); list.insert(1, "b"); { const ans = loro.queryStablePos(pos0!); expect(ans.offset).toEqual(0); expect(ans.side).toEqual(0); expect(ans.update).toBeUndefined(); } list.insert(0, "c"); { const ans = loro.queryStablePos(pos0!); expect(ans.offset).toEqual(1); expect(ans.side).toEqual(0); expect(ans.update).toBeUndefined(); } list.delete(1, 1); { const ans = loro.queryStablePos(pos0!); expect(ans.offset).toEqual(1); expect(ans.side).toEqual(-1); expect(ans.update).toBeDefined(); } ```
2024-04-09 08:01:37 +00:00
Self {
id,
container,
side,
origin_pos,
Cursors (#290) This PR introduces support for retrieving and querying cursors. ## Motivation Using "index" to denote cursor positions can be unstable, as positions may shift with document edits. To reliably represent a position or range within a document, it is more effective to leverage the unique ID of each item/character in a List CRDT or Text CRDT. ## Updating Cursors Loro optimizes State metadata by not storing the IDs of deleted elements. This approach, while efficient, complicates tracking cursor positions since they rely on these IDs for precise locations within the document. The solution recalculates position by replaying relevant history to update stable positions accurately. To minimize the performance impact of history replay, the system updates cursor info to reference only the IDs of currently present elements, thereby reducing the need for replay. Each position has a "Side" information, indicating the actual cursor position is on the left, right, or directly in the center of the target ID. Note: In JavaScript, the offset returned when querying a Stable Position is based on the UTF-16 index. # Example ```ts const loro = new Loro(); const list = loro.getList("list"); list.insert(0, "a"); const pos0 = list.getStablePos(0); list.insert(1, "b"); { const ans = loro.queryStablePos(pos0!); expect(ans.offset).toEqual(0); expect(ans.side).toEqual(0); expect(ans.update).toBeUndefined(); } list.insert(0, "c"); { const ans = loro.queryStablePos(pos0!); expect(ans.offset).toEqual(1); expect(ans.side).toEqual(0); expect(ans.update).toBeUndefined(); } list.delete(1, 1); { const ans = loro.queryStablePos(pos0!); expect(ans.offset).toEqual(1); expect(ans.side).toEqual(-1); expect(ans.update).toBeDefined(); } ```
2024-04-09 08:01:37 +00:00
}
}
pub fn encode(&self) -> Vec<u8> {
postcard::to_allocvec(self).unwrap()
}
pub fn decode(data: &[u8]) -> Result<Self, postcard::Error> {
postcard::from_bytes(data)
}
}