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)
pub opaque type Text

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 to_json(text: Text) -> json.Json

Encode text using the canonical sequence JSON envelope.

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))
pub fn value(text: Text) -> String

Return the visible text as a single string.

pub fn values(text: Text) -> List(String)

Return the visible graphemes as a list.

Search Document