Collaborative text
Task: keep a document that several people edit at the same time, where each editor sends edits typed against the version it last saw.
One leader applies every edit in log order, so there is never a merge. The
problem is position: by the time “insert at 12” is applied, someone may have
typed before position 12. Text solves it the way collaborative editors do: it
is a Fugue sequence in which every character
has a stable id. An insert names the id of the character it follows, a
delete names ids, so an edit lands where its author meant, and two runs typed
after the same character never interleave (crates/sdk/src/collections/text.rs).
#[app::state(version = 1)]pub struct Docs { body: Text, rich: RichText,}Edit by id
Section titled “Edit by id”The client keeps the ids it read (chars() returns (id, char) pairs) and
sends edits against them:
/// Inserts `text` after the character `after` (`None`: at the start); returns the new ids.pub fn insert(&mut self, after: Option<u64>, text: String) -> app::Result<Vec<String>> { let ids = self.body.insert_after(after, &text)?; Ok(ids.iter().map(u64::to_string).collect())}
/// Deletes the characters `ids`; ids already deleted are skipped, an id that never/// existed is `UnknownId` and deletes nothing.pub fn delete(&mut self, ids: Vec<String>) -> app::Result<u64> { let ids = ids .iter() .map(|id| id.parse::<u64>().map_err(|_| AppError::new("BadId"))) .collect::<Result<Vec<_>, _>>()?; Ok(self.body.delete_ids(&ids)?)}insert(index, str) and delete(index, n) edit by position for a client that
holds the current text; id_at(index) and index_of(id) convert between the
two.
read() gives the text, chars() the characters with their ids, len() the
length in one read.
Rich text
Section titled “Rich text”RichText is a Text plus marks: ranges with a key and an optional value
(bold, a link, a comment thread).
| Method | Does |
|---|---|
text() / text_mut() |
The underlying Text. |
mark(start, end, key, value, expand) |
Marks the range by position; expand makes it grow when text is typed at its end. |
mark_ids(..) |
The same by character ids. |
marks() |
The marks with their ids. |
spans() |
The text cut into spans, each with its attributes: what a renderer draws. |
clear() |
Removes the text and its marks. |
Reads and edits load every node of the text, 1,000 per host read; len is one
read. Keep one Text per document rather than one large text for everything,
and use a table for document metadata.