loro/crates/loro
Zixuan Chen 81ae2abacc
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 16:01:37 +08:00
..
src Cursors (#290) 2024-04-09 16:01:37 +08:00
tests Cursors (#290) 2024-04-09 16:01:37 +08:00
Cargo.toml chore: upgrade rust crates 2024-02-16 11:30:56 +08:00
LICENSE chore: publish mvp rust api 2023-11-28 21:29:11 +08:00
README.md refactor!: Add prelim support, making creating sub container easier (#300) 2024-03-30 11:38:24 +08:00

Loro

Loro is a high-performance CRDTs framework offering Rust and JavaScript APIs.

Designed for local-first software, it enables effortless collaboration in app states.

PS: Version control is forthcoming. Time travel functionality is already accessible at https://loro.dev/docs/tutorial/time_travel.

Examples

Map/List/Text

use loro::{LoroDoc, LoroList, LoroText, LoroValue, ToJson};
use serde_json::json;

let doc = LoroDoc::new();
let map = doc.get_map("map");
map.insert("key", "value").unwrap();
map.insert("true", true).unwrap();
map.insert("null", LoroValue::Null).unwrap();
map.insert("deleted", LoroValue::Null).unwrap();
map.delete("deleted").unwrap();
let list = map.insert_container("list", LoroList::new()).unwrap();
list.insert(0, "List").unwrap();
list.insert(1, 9).unwrap();
let text = map.insert_container("text", LoroText::new()).unwrap();
text.insert(0, "Hello world!").unwrap();
assert_eq!(
    doc.get_deep_value().to_json_value(),
    json!({
        "map": {
            "key": "value",
            "true": true,
            "null": null,
            "list": ["List", 9],
            "text": "Hello world!"
        }
    })
);

Rich Text

use loro::{ExpandType, LoroDoc, ToJson};
use serde_json::json;

let doc = LoroDoc::new();
let text = doc.get_text("text");
text.insert(0, "Hello world!").unwrap();
text.mark(0..5, "bold", true).unwrap();
assert_eq!(
    text.to_delta().to_json_value(),
    json!([
        { "insert": "Hello", "attributes": {"bold": true} },
        { "insert": " world!" },
    ])
);
text.unmark(3..5, "bold").unwrap();
assert_eq!(
    text.to_delta().to_json_value(),
    json!([
          { "insert": "Hel", "attributes": {"bold": true} },
          { "insert": "lo world!" },
    ])
);

Sync

use loro::{LoroDoc, ToJson, ExpandType};
use serde_json::json;

let doc = LoroDoc::new();
let text = doc.get_text("text");
text.insert(0, "Hello world!").unwrap();
let bytes = doc.export_from(&Default::default());
let doc_b = LoroDoc::new();
doc_b.import(&bytes).unwrap();
assert_eq!(doc.get_deep_value(), doc_b.get_deep_value());
let text_b = doc_b.get_text("text");
text_b
    .mark(0..5, "bold", true)
    .unwrap();
doc.import(&doc_b.export_from(&doc.oplog_vv())).unwrap();
assert_eq!(
    text.to_delta().to_json_value(),
    json!([
        { "insert": "Hello", "attributes": {"bold": true} },
        { "insert": " world!" },
    ])
);

Save

use loro::LoroDoc;

let doc = LoroDoc::new();
let text = doc.get_text("text");
text.insert(0, "123").unwrap();
let snapshot = doc.export_snapshot();

let new_doc = LoroDoc::new();
new_doc.import(&snapshot).unwrap();
assert_eq!(new_doc.get_deep_value(), doc.get_deep_value());