lattice_text/text
A plain-text CRDT backed by lattice_sequence.
Text is stored as a sequence of single-grapheme items: every inserted
string is split into graphemes and each grapheme becomes one sequence
item, so indices, anchors, and length are all grapheme-based. Insert,
delete, merge, and delta operations delegate to lattice_sequence.
Use lattice_sequence/sequence directly when you need a generic list CRDT.
Example
import lattice_core/replica_id
import lattice_text/text
let doc = text.new(replica_id.new("node-a"))
text.value(doc) // -> ""
Types
An error returned when a grapheme range does not satisfy
0 <= start <= end <= length.
pub type RangeError {
RangeOutOfBounds(start: Int, end: Int, length: Int)
}
Constructors
-
RangeOutOfBounds(start: Int, end: Int, length: Int)
Values
pub fn anchor_at(
text: Text,
index: Int,
bias: sequence.Bias,
) -> Result(sequence.Anchor, sequence.AnchorError)
Create an anchor at the gap before the grapheme at index.
Anchors are stable positions that survive concurrent edits and merges:
resolve one back to a current grapheme index with resolve_anchor.
Before bias glues the anchor to the grapheme at index, so inserts at
the gap push it right; After bias glues it to the grapheme at
index - 1, so inserts at the gap land after it.
Examples
let assert Ok(doc) = text.insert(text.new(replica_id.new("A")), 0, "hello")
let assert Ok(cursor) = text.anchor_at(doc, 5, sequence.After)
let assert Ok(doc) = text.insert(doc, 0, "say ")
text.resolve_anchor(doc, cursor) // -> Ok(9)
pub fn anchor_from_json(
json_string: String,
) -> Result(sequence.Anchor, json.DecodeError)
Decode an anchor from a JSON string produced by anchor_to_json.
pub fn anchor_to_json(anchor: sequence.Anchor) -> json.Json
Encode an anchor as a self-describing JSON value.
pub fn append(
text: Text,
value: String,
) -> Result(Text, sequence.InsertError)
Insert a value at the end of the text.
Returns the insertion result, like insert.
Examples
let assert Ok(doc) = text.insert(text.new(replica_id.new("A")), 0, "ab")
let assert Ok(doc) = text.append(doc, "cd")
text.value(doc)
// -> "abcd"
pub fn append_with_delta(
text: Text,
value: String,
) -> Result(#(Text, Text), sequence.InsertError)
Append a value and return both the updated text and insertion delta.
pub fn bind(text: Text, replica: replica_id.ReplicaId) -> Text
Select the local editor without rebuilding the underlying sequence.
Preserves historical item IDs, counters, and compaction metadata. Independent writers must use distinct replica IDs.
Examples
let assert Ok(remote) = text.insert(text.new(replica_id.new("A")), 0, "hello")
text.bind(remote, replica_id.new("B")) |> text.value()
// -> "hello"
pub fn compact(
text: Text,
stable: version_vector.VersionVector,
) -> #(Text, sequence.ForwardingMap)
Compact everything at or below a stability frontier.
Delegates to sequence.compact: stable tombstones are dropped, runs of
stable graphemes are merged into compact blocks, and every dropped ID
gets a forwarding entry so anchors and rebased operations still resolve.
See lattice_sequence/sequence.compact for the stability contract.
pub fn delete(
text: Text,
index: Int,
) -> Result(Text, sequence.DeleteError)
Delete the value at the visible character index.
Returns DeleteIndexOutOfBounds when index is outside [0, length).
pub fn delete_range(
text: Text,
start: Int,
end: Int,
) -> Result(Text, RangeError)
Delete the graphemes in [start, end).
Examples
let assert Ok(doc) = text.insert(text.new(replica_id.new("A")), 0, "abcd")
let assert Ok(doc) = text.delete_range(doc, 1, 3)
text.value(doc)
// -> "ad"
Returns RangeOutOfBounds when the range is outside [0, length]
or start > end.
pub fn delete_range_with_delta(
text: Text,
start: Int,
end: Int,
) -> Result(#(Text, Text), RangeError)
Delete a grapheme range and return both the updated text and deletion delta.
Returns RangeOutOfBounds when the range is outside [0, length]
or start > end.
pub fn delete_with_delta(
text: Text,
index: Int,
) -> Result(#(Text, Text), sequence.DeleteError)
Delete a value and return both the updated text and deletion delta.
Returns DeleteIndexOutOfBounds when index is outside [0, length).
pub fn end_anchor() -> sequence.Anchor
Create an anchor at the end of the text. Always resolves to the current grapheme length, tracking growth.
pub fn from_json(
json_string: String,
) -> Result(Text, json.DecodeError)
Decode text from the canonical sequence JSON envelope.
Retains historical IDs and raises an understated allocation counter to
cover retained IDs and the compaction frontier, as sequence.from_json
does. Use bind with the local identity before editing an adopted state.
pub fn frontier(text: Text) -> version_vector.VersionVector
The stability frontier this text was last compacted at.
pub fn insert(
text: Text,
index: Int,
value: String,
) -> Result(Text, sequence.InsertError)
Insert a value at the visible character index.
Returns IndexOutOfBounds when index is outside [0, length].
pub fn insert_with_delta(
text: Text,
index: Int,
value: String,
) -> Result(#(Text, Text), sequence.InsertError)
Insert a value and return both the updated text and insertion delta.
Returns IndexOutOfBounds when index is outside [0, length].
pub fn length(text: Text) -> Int
Count the visible graphemes in the text.
Examples
let assert Ok(doc) = text.insert(text.new(replica_id.new("A")), 0, "a👍")
text.length(doc)
// -> 2
pub fn merge(
a: Text,
b: Text,
replica: replica_id.ReplicaId,
) -> Text
Merge two text CRDT states.
Pass the identity used for subsequent local edits. Operand order does not select the identity. Deltas and decoded snapshots retain their sender’s identity; merge them under your local identity before editing. Independent writers must use distinct replica IDs.
Examples
let local = replica_id.new("A")
text.merge(text.new(local), text.new(replica_id.new("B")), local)
|> text.value()
// -> ""
pub fn merge_as(
a: Text,
b: Text,
replica: replica_id.ReplicaId,
) -> Text
Alias for merge, with the same explicit output replica identity.
Examples
text.merge_as(a, b, local) == text.merge(a, b, local)
// -> True
pub fn move(
text: Text,
from_index: Int,
to_index: Int,
) -> Result(Text, sequence.MoveError)
Move the grapheme at from_index to to_index.
The to_index is interpreted after removing the grapheme from
from_index.
Examples
let assert Ok(doc) = text.insert(text.new(replica_id.new("A")), 0, "abc")
let assert Ok(doc) = text.move(doc, 0, 2)
text.value(doc)
// -> "bca"
Returns a MoveError when either index is out of bounds.
pub fn move_with_delta(
text: Text,
from_index: Int,
to_index: Int,
) -> Result(#(Text, Text), sequence.MoveError)
Move a grapheme and return both the updated text and move delta.
Returns a MoveError when either index is out of bounds.
The to_index is interpreted after removing the grapheme from
from_index.
pub fn new(replica_id: replica_id.ReplicaId) -> Text
Create an empty text CRDT for a replica.
pub fn remove_forwardings(
text: Text,
map: sequence.ForwardingMap,
) -> Text
Remove previously emitted forwarding entries from the text.
Forwardings are bounded by the host’s retention policy: keep the map
returned by each compact round and expire old rounds by passing them
here.
pub fn replace_range(
text: Text,
start: Int,
end: Int,
value: String,
) -> Result(Text, RangeError)
Replace the graphemes in [start, end) with a value.
Examples
let assert Ok(doc) = text.insert(text.new(replica_id.new("A")), 0, "abcd")
let assert Ok(doc) = text.replace_range(doc, 1, 3, "XY")
text.value(doc)
// -> "aXYd"
Returns RangeOutOfBounds when the range is outside [0, length]
or start > end.
pub fn replace_range_with_delta(
text: Text,
start: Int,
end: Int,
value: String,
) -> Result(#(Text, Text), RangeError)
Replace a grapheme range and return both the updated text and replacement delta.
Returns RangeOutOfBounds when the range is outside [0, length]
or start > end.
pub fn resolve_anchor(
text: Text,
anchor: sequence.Anchor,
) -> Result(Int, sequence.AnchorError)
Resolve an anchor to a current grapheme index in [0, length].
Anchors on deleted graphemes still resolve: they collapse to the gap where the grapheme used to be. Anchors follow moved graphemes.
Anchors to compacted graphemes resolve through the forwarding map to the gap the grapheme left behind — semantically the same as tombstone collapse.
Returns Error(UnknownAnchorTarget) when the anchor references a
grapheme this replica has never seen (created remotely and not yet
merged), or one that was compacted away and whose forwarding entry has
since been removed by the host’s retention policy. Either way the anchor
is unusable and the holder should re-anchor.
pub fn start_anchor() -> sequence.Anchor
Create an anchor at the start of the text. Always resolves to 0.
pub fn substring(text: Text, start: Int, end: Int) -> String
Return the graphemes in [start, end), clamping both indexes to the
text bounds. An empty range (including start > end) yields "".
Examples
let assert Ok(doc) = text.insert(text.new(replica_id.new("A")), 0, "abcd")
text.substring(doc, 1, 3)
// -> "bc"
pub fn try_substring(
text: Text,
start: Int,
end: Int,
) -> Result(String, RangeError)
Return the graphemes in [start, end), or an error when the range does
not satisfy 0 <= start <= end <= length.
Examples
let assert Ok(doc) = text.insert(text.new(replica_id.new("A")), 0, "abc")
text.try_substring(doc, 0, 4)
// -> Error(text.RangeOutOfBounds(start: 0, end: 4, length: 3))