Where the caret is, and what it covers.
A selection is two Quillon.Selection.Points. The anchor is where the
selection began; the head is where the caret is now. They are equal for a
plain cursor, and the head may come before the anchor when the selection was
dragged backwards - nothing here assumes an order, so use normalize/1 before
treating a selection as a range.
iex> Quillon.Selection.cursor([0], 3)
%Quillon.Selection{
kind: :text,
anchor: %Quillon.Selection.Point{path: [0], offset: 3},
head: %Quillon.Selection.Point{path: [0], offset: 3}
}Two kinds
A :text selection covers characters. A :node selection covers one node as a
unit, which is what atomic: true exists for - a footnote marker or an inline
chip occupies exactly one position specifically so it can be selected rather
than being zero-width. A range of characters cannot express that.
iex> Quillon.Selection.node([2]) |> Quillon.Selection.node?()
trueSelections are not document content
A selection lives beside a document, never inside it. Storing carets in the AST would put ephemeral view state into undo history and into whatever syncs the document. Several users' carets are a plain map of selections, one per user, held wherever presence is tracked.
Resolving
resolve/2 turns a selection into the block it points at plus offsets in the
form the transform layer takes, which is how Quillon.Edit bridges the two:
iex> doc = Quillon.document([Quillon.paragraph("Hello")])
iex> {:ok, resolved} = Quillon.Selection.resolve(doc, Quillon.Selection.text(
...> Quillon.Selection.point([0], 1),
...> Quillon.Selection.point([0], 4)
...> ))
iex> {resolved.start_offset, resolved.end_offset}
{1, 4}
Summary
Types
Whether the selection covers characters or a whole node
What resolve/2 reports back
A selection
Functions
Pull a selection's offsets back inside its block.
Whether the selection covers nothing - a cursor rather than a range.
A collapsed text selection - a plain cursor.
Rebuild a selection from a JSON-friendly map.
A selection of one whole node.
Whether this is a node selection.
Return the two ends in document order.
Build a point. A shorthand for Quillon.Selection.Point.new/2.
Resolve a selection against a document.
Whether both ends sit in the same block.
A text selection from an anchor to a head.
Whether this is a text selection.
Convert a selection to a JSON-friendly map.
Whether a selection resolves against a document.
Types
@type kind() :: :text | :node
Whether the selection covers characters or a whole node
@type resolved() :: %{ kind: :text, path: Quillon.Selection.Point.path(), block: tuple(), start_offset: non_neg_integer(), end_offset: non_neg_integer() } | %{kind: :node, path: Quillon.Selection.Point.path(), node: tuple()}
What resolve/2 reports back
@type t() :: %Quillon.Selection{ anchor: Quillon.Selection.Point.t(), head: Quillon.Selection.Point.t(), kind: kind() }
A selection
Functions
Pull a selection's offsets back inside its block.
Useful after an edit made elsewhere shortened the block a stored caret was in. Only offsets move - a path that no longer exists is an error, not something to guess at.
Examples
iex> doc = Quillon.document([Quillon.paragraph("Hello")])
iex> {:ok, clamped} = Quillon.Selection.clamp(doc, Quillon.Selection.cursor([0], 99))
iex> Quillon.Selection.Point.offset(clamped.head)
5
iex> doc = Quillon.document([Quillon.paragraph("Hello")])
iex> Quillon.Selection.clamp(doc, Quillon.Selection.cursor([3], 0))
{:error, :invalid_path}
Whether the selection covers nothing - a cursor rather than a range.
A node selection is never collapsed: it always covers its node.
Examples
iex> Quillon.Selection.cursor([0], 3) |> Quillon.Selection.collapsed?()
true
iex> Quillon.Selection.node([0]) |> Quillon.Selection.collapsed?()
false
@spec cursor(Quillon.Selection.Point.path(), non_neg_integer()) :: t()
A collapsed text selection - a plain cursor.
Examples
iex> Quillon.Selection.cursor([0], 0) |> Quillon.Selection.collapsed?()
true
Rebuild a selection from a JSON-friendly map.
Examples
iex> json = Quillon.Selection.to_json(Quillon.Selection.cursor([0, 1], 4))
iex> {:ok, selection} = Quillon.Selection.from_json(json)
iex> Quillon.Selection.Point.path(selection.head)
[0, 1]
iex> Quillon.Selection.from_json(%{"kind" => "sideways"})
{:error, :invalid_selection}
@spec node(Quillon.Selection.Point.path()) :: t()
A selection of one whole node.
Examples
iex> Quillon.Selection.node([1])
%Quillon.Selection{
kind: :node,
anchor: %Quillon.Selection.Point{path: [1], offset: 0},
head: %Quillon.Selection.Point{path: [1], offset: 0}
}
Whether this is a node selection.
Examples
iex> Quillon.Selection.node([0]) |> Quillon.Selection.node?()
true
@spec normalize(t()) :: {Quillon.Selection.Point.t(), Quillon.Selection.Point.t()}
Return the two ends in document order.
Examples
iex> backwards = Quillon.Selection.text(
...> Quillon.Selection.point([0], 7),
...> Quillon.Selection.point([0], 2)
...> )
iex> {from, to} = Quillon.Selection.normalize(backwards)
iex> {Quillon.Selection.Point.offset(from), Quillon.Selection.Point.offset(to)}
{2, 7}
@spec point(Quillon.Selection.Point.path(), non_neg_integer()) :: Quillon.Selection.Point.t()
Build a point. A shorthand for Quillon.Selection.Point.new/2.
Examples
iex> Quillon.Selection.point([0], 2)
%Quillon.Selection.Point{path: [0], offset: 2}
@spec resolve(tuple(), t()) :: {:ok, resolved()} | {:error, :invalid_path | :out_of_range | :cross_block}
Resolve a selection against a document.
A text selection reports the block it points into and its offsets in document
order, ready for Quillon.Transform. A node selection reports the node.
Fails with :invalid_path when the path does not exist, :out_of_range when an
offset falls outside the block, and :cross_block for a text selection whose
ends are in different blocks - representable, but not yet something the editing
functions act on.
Examples
iex> doc = Quillon.document([Quillon.paragraph("Hello")])
iex> {:ok, resolved} = Quillon.Selection.resolve(doc, Quillon.Selection.cursor([0], 2))
iex> resolved.kind
:text
iex> doc = Quillon.document([Quillon.paragraph("Hello")])
iex> Quillon.Selection.resolve(doc, Quillon.Selection.cursor([9], 0))
{:error, :invalid_path}
iex> doc = Quillon.document([Quillon.paragraph("Hello")])
iex> Quillon.Selection.resolve(doc, Quillon.Selection.cursor([0], 99))
{:error, :out_of_range}
Whether both ends sit in the same block.
Examples
iex> Quillon.Selection.cursor([0], 3) |> Quillon.Selection.single_block?()
true
iex> sel = Quillon.Selection.text(
...> Quillon.Selection.point([0], 0),
...> Quillon.Selection.point([1], 0)
...> )
iex> Quillon.Selection.single_block?(sel)
false
@spec text(Quillon.Selection.Point.t(), Quillon.Selection.Point.t()) :: t()
A text selection from an anchor to a head.
Examples
iex> sel = Quillon.Selection.text(
...> Quillon.Selection.point([0], 1),
...> Quillon.Selection.point([0], 4)
...> )
iex> Quillon.Selection.collapsed?(sel)
false
Whether this is a text selection.
Examples
iex> Quillon.Selection.cursor([0], 0) |> Quillon.Selection.text?()
true
Convert a selection to a JSON-friendly map.
Carets have to cross the wire for several people to see each other's, so a selection serializes the same way a document does.
Examples
iex> Quillon.Selection.to_json(Quillon.Selection.cursor([0], 3))
%{
"kind" => "text",
"anchor" => %{"path" => [0], "offset" => 3},
"head" => %{"path" => [0], "offset" => 3}
}
Whether a selection resolves against a document.
Examples
iex> doc = Quillon.document([Quillon.paragraph("Hello")])
iex> Quillon.Selection.valid?(doc, Quillon.Selection.cursor([0], 5))
true
iex> doc = Quillon.document([Quillon.paragraph("Hello")])
iex> Quillon.Selection.valid?(doc, Quillon.Selection.cursor([0], 6))
false