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,
) -> sequence.Anchor
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 doc = text.new(replica_id.new("A")) |> text.insert(0, "hello")
let cursor = text.anchor_at(doc, 5, sequence.After)
let doc = text.insert(doc, 0, "say ")
text.resolve_anchor(doc, cursor) // -> 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) -> Text
Insert a value at the end of the text. Appending is always valid, so no
try_ variant exists.
Examples
text.new(replica_id.new("A"))
|> text.insert(0, "ab")
|> text.append("cd")
|> text.value()
// -> "abcd"
pub fn append_with_delta(
text: Text,
value: String,
) -> #(Text, Text)
Append a value and return both the updated text and insertion delta.
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) -> Text
Delete the value at the visible character index.
Panics with DeleteIndexOutOfBounds when index is outside
[0, length). Use try_delete_with_delta to handle an untrusted index
without crashing.
pub fn delete_range(text: Text, start: Int, end: Int) -> Text
Delete the graphemes in [start, end).
Examples
text.new(replica_id.new("A"))
|> text.insert(0, "abcd")
|> text.delete_range(1, 3)
|> text.value()
// -> "ad"
Panics with RangeOutOfBounds when [start, end) is not a valid range in
[0, length]. Use try_delete_range_with_delta to handle untrusted
bounds without crashing.
pub fn delete_range_with_delta(
text: Text,
start: Int,
end: Int,
) -> #(Text, Text)
Delete a grapheme range and return both the updated text and deletion delta.
Panics with RangeOutOfBounds when [start, end) is not a valid range in
[0, length]. Use try_delete_range_with_delta to handle untrusted
bounds without crashing.
pub fn delete_with_delta(text: Text, index: Int) -> #(Text, Text)
Delete a value and return both the updated text and deletion delta.
Panics with DeleteIndexOutOfBounds when index is outside
[0, length). Use try_delete_with_delta to handle an untrusted index
without crashing.
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.
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) -> Text
Insert a value at the visible character index.
Panics with IndexOutOfBounds when index is outside [0, length]. Use
try_insert_with_delta to handle an untrusted index without crashing.
pub fn insert_with_delta(
text: Text,
index: Int,
value: String,
) -> #(Text, Text)
Insert a value and return both the updated text and insertion delta.
Panics with IndexOutOfBounds when index is outside [0, length]. Use
try_insert_with_delta to handle an untrusted index without crashing.
pub fn length(text: Text) -> Int
Count the visible graphemes in the text.
Examples
text.new(replica_id.new("A"))
|> text.insert(0, "a👍")
|> text.length()
// -> 2
pub fn move(text: Text, from_index: Int, to_index: Int) -> Text
Move the grapheme at from_index to to_index.
The to_index is interpreted after removing the grapheme from
from_index.
Examples
text.new(replica_id.new("A"))
|> text.insert(0, "abc")
|> text.move(0, 2)
|> text.value()
// -> "bca"
Panics with a MoveError when either index is out of bounds. Use
try_move_with_delta to handle untrusted indices without crashing.
pub fn move_with_delta(
text: Text,
from_index: Int,
to_index: Int,
) -> #(Text, Text)
Move a grapheme and return both the updated text and move delta.
Panics with a MoveError when either index is out of bounds. Use
try_move_with_delta to handle untrusted indices without crashing.
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,
) -> Text
Replace the graphemes in [start, end) with a value.
Examples
text.new(replica_id.new("A"))
|> text.insert(0, "abcd")
|> text.replace_range(1, 3, "XY")
|> text.value()
// -> "aXYd"
Panics with RangeOutOfBounds when [start, end) is not a valid range in
[0, length]. Use try_replace_range_with_delta to handle untrusted
bounds without crashing.
pub fn replace_range_with_delta(
text: Text,
start: Int,
end: Int,
value: String,
) -> #(Text, Text)
Replace a grapheme range and return both the updated text and replacement delta.
Panics with RangeOutOfBounds when [start, end) is not a valid range in
[0, length]. Use try_replace_range_with_delta to handle untrusted
bounds without crashing.
pub fn resolve_anchor(text: Text, anchor: sequence.Anchor) -> Int
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. Panics
with UnknownAnchorTarget when the target was never merged or was
compacted away and its forwarding has expired — hosts holding anchors
across compaction rounds should use try_resolve_anchor and treat
failure as “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
text.new(replica_id.new("A"))
|> text.insert(0, "abcd")
|> text.substring(1, 3)
// -> "bc"
pub fn try_anchor_at(
text: Text,
index: Int,
bias: sequence.Bias,
) -> Result(sequence.Anchor, sequence.AnchorError)
Safely create an anchor at the gap before the grapheme at index.
Valid positions are 0 <= index <= length.
pub fn try_delete_range_with_delta(
text: Text,
start: Int,
end: Int,
) -> Result(#(Text, Text), RangeError)
Safely delete a grapheme range and return both the updated text and deletion delta.
pub fn try_delete_with_delta(
text: Text,
index: Int,
) -> Result(#(Text, Text), sequence.DeleteError)
Safely delete a value and return both the updated text and deletion delta.
pub fn try_insert_with_delta(
text: Text,
index: Int,
value: String,
) -> Result(#(Text, Text), sequence.InsertError)
Safely insert a value and return both the updated text and insertion delta.
pub fn try_move_with_delta(
text: Text,
from_index: Int,
to_index: Int,
) -> Result(#(Text, Text), sequence.MoveError)
Safely move a grapheme and return both the updated text and move delta.
The to_index is interpreted after removing the grapheme from
from_index.
pub fn try_replace_range_with_delta(
text: Text,
start: Int,
end: Int,
value: String,
) -> Result(#(Text, Text), RangeError)
Safely replace a grapheme range and return both the updated text and replacement delta.
pub fn try_resolve_anchor(
text: Text,
anchor: sequence.Anchor,
) -> Result(Int, sequence.AnchorError)
Safely resolve an anchor to a current grapheme index in [0, length].
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 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
text.new(replica_id.new("A"))
|> text.insert(0, "abc")
|> text.try_substring(0, 4)
// -> Error(text.RangeOutOfBounds(start: 0, end: 4, length: 3))