A stable position in a collaborative document — where someone's caret or selection sits, so you can show remote collaborators' cursors.
A Cursor remembers a position by the identity of the characters around it, not by
a numeric offset. That is what makes it survive concurrent editing: if a collaborator's
caret is between characters 3 and 4 and someone else inserts text earlier in the line,
the caret still sits between the same two characters after the edits merge — it does not
drift. You create one with cursorAt (from a text ref and an offset) and turn it
back into a current offset, against your own converged document, with cursorOffset.
Cursors are presence data, not part of the document. You broadcast them on the
Crdt.Presence channel and every viewer resolves them locally; there is nothing to
merge. A typical setup carries each peer's caret as an optional presence field:
peerCodec =
Presence.codec Peer
|> Presence.optional "caret" .caret Presence.cursor
|> Presence.buildCodecA Range is a text selection: the pair of cursors marking its two ends — the anchor
where the selection started and the focus where the caret now reaches (see
Range for why the two ends aren't interchangeable). Each end resolves
independently, so a
selection stretches and shifts correctly as the text changes underneath it.
Make a stable cursor at character offset in a text field. Because the cursor is
anchored to the characters around it (not to the number), it keeps pointing at the same
spot as the text is edited concurrently. Nothing if the ref doesn't resolve to a text
field in this document.
Making a cursor doesn't change the document — it's a read against the current text — so
broadcast it on the Crdt.Presence channel to show collaborators' carets.
Like cursorAt, but for a rich-text field (a Crdt.richText ref). The offset
is a document-wide character offset over the flattened text (block boundaries don't count
as characters), matching the offsets Crdt.Edit.mark and the span stream use. The
resulting cursor resolves and merges exactly like a plain-text one.
Resolve a cursor back to its current character offset in this document, or Nothing
if it no longer points anywhere. The inverse of cursorAt.
Deleting the character a cursor sits after does not unresolve it — the caret lands at
the nearest live spot, because the tombstone still marks the position. Nothing means this
document genuinely cannot place the anchor: the cursor's text field is gone, or the anchor
character is unknown here because Crdt.Doc.compact dropped its tombstone or the insert
carrying it has not arrived yet. Render no caret in that case; cursors are ephemeral, so
the peer's next broadcast resolves normally.
The identity of the character a cursor sits just after, if any (Nothing when it is
at the very start). Because it is an identity rather than an offset, it keeps pointing at
the same character as the text is edited around it.
Do two cursors point into the same container (the same text field or list), regardless of where within it? Useful when rendering per-field carets: keep only the peers whose cursor is in the field you are currently drawing.
A text selection — the pair of cursors marking its two ends.
Think of selecting text by dragging: the point where you first pressed down stays put,
and the other end follows your pointer. Those are the two ends of a Range:
Keeping them apart, rather than just storing "leftmost" and "rightmost", matters for two reasons. It records the selection's direction: dragging left-to-right puts the focus after the anchor, dragging right-to-left puts it before — a "backwards" selection is simply one whose focus sits before its anchor. And it is what lets you grow or shrink a selection from the moving end (shift-clicking, or holding shift and pressing an arrow key) while the anchor holds fast.
If all you want is the selected span itself, resolve both ends to offsets with
cursorOffset and take the smaller and larger of the two.
Build a Range from its two ends: the anchor (the end that stays put, where the
selection started) and the focus (the moving end, where the caret now is).
The end of the selection that stays put — where it started.
The moving end of the selection — where the caret currently is.
Serialize a cursor to JSON, to send on the presence channel.
Decode a cursor received from a peer.
Serialize a selection to JSON.
Decode a selection received from a peer.
A stable position within a text field. Build one with
cursorAtand resolve it back to a current offset withcursorOffset. Opaque.