Quillon.Selection (Quillon v0.4.0)

Copy Markdown View Source

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?()
true

Selections 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

t()

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

kind()

@type kind() :: :text | :node

Whether the selection covers characters or a whole node

resolved()

@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

t()

@type t() :: %Quillon.Selection{
  anchor: Quillon.Selection.Point.t(),
  head: Quillon.Selection.Point.t(),
  kind: kind()
}

A selection

Functions

clamp(doc, selection)

@spec clamp(tuple(), t()) :: {:ok, t()} | {:error, :invalid_path}

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}

collapsed?(selection)

@spec collapsed?(t()) :: boolean()

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

cursor(path, offset)

A collapsed text selection - a plain cursor.

Examples

iex> Quillon.Selection.cursor([0], 0) |> Quillon.Selection.collapsed?()
true

from_json(arg1)

@spec from_json(any()) :: {:ok, t()} | {:error, :invalid_selection}

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}

node(path)

@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}
}

node?(selection)

@spec node?(t()) :: boolean()

Whether this is a node selection.

Examples

iex> Quillon.Selection.node([0]) |> Quillon.Selection.node?()
true

normalize(selection)

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}

point(path, offset)

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}

resolve(doc, selection)

@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}

single_block?(selection)

@spec single_block?(t()) :: boolean()

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

text(anchor, head)

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

text?(selection)

@spec text?(t()) :: boolean()

Whether this is a text selection.

Examples

iex> Quillon.Selection.cursor([0], 0) |> Quillon.Selection.text?()
true

to_json(selection)

@spec to_json(t()) :: map()

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}
}

valid?(doc, selection)

@spec valid?(tuple(), t()) :: boolean()

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