# `Kotoba.Prompts`
[🔗](https://github.com/hattoriai/kotoba/blob/v0.8.0/lib/kotoba/prompts.ex#L1)

Prompts: a trigger character in the editor opens a menu of results, for
example `@` for people. A result inserts a mention, text (an emoji, a
snippet) or a node of the app. See the [Prompts](prompts.md) guide.

A prompt list has one of two forms:

    # The first prompt has the trigger "@", the second has "#".
    [people: &MyApp.People.search/1, work: &MyApp.Work.search/1]

    # Each trigger given explicitly.
    [{"@", :people, &MyApp.People.search/1}, {"#", :work, &MyApp.Work.search/1}]

A trigger is exactly one character that is not white space. The name of a
prompt (an atom or a string) is the `kind` of the mentions that it
inserts.

## A prompt

A prompt is a search callback, a callback with options, or a keyword list
with the callback (`:search`) or a list of items (`:items`):

    people: &MyApp.People.search/1
    people: {&MyApp.People.search/1, label: "People in the workshop"}
    people: [search: &MyApp.People.search/1, spaces: true, min_length: 2]
    emoji: [items: MyApp.Emoji.items(), insert: :text]

The options:

  * `:label` - the accessible name of the menu, "<name> suggestions"
    otherwise. A non-empty string of at most 200 characters.
  * `:spaces` - `true` to let the query have spaces ("Ada Lovelace"). A
    query ends at two spaces in a row, and cannot start with a space. The
    default is `false`: a space closes the menu.
  * `:min_length` - the characters of the query before the first search
    (0 to the `:max_length`, default 0). A shorter query shows "Keep
    typing".
  * `:max_length` - the longest query, 1 to 200 characters (default 64).
    A longer one closes the menu.
  * `:insert` - what a result inserts: `:mention` (the default), `:text`
    (the item's `:text`, or its label), `{:node, type}`: a node of the
    editor of that Lexical type, from the item's `:attrs`, or `:command`:
    the item's command, see "The insert menu" below.
  * `:with_text` - for an `insert: :command` prompt, what a block command
    does in a block that has text: `:below` (the default) makes a new
    block below it, `:turn_into` turns the block into the new one. In an
    empty paragraph it always turns it into the new block.
  * `:async` - `true` to run the search in a task of the LiveView, so
    that a slow search does not block it. The LiveView then needs a
    `handle_async/3` clause, see `Kotoba.Live.handle_prompt_async/3`. The
    callback of an async prompt takes only the query (arity 1).

## Searches

A `:search` callback gets the query (the text after the trigger) and
returns a list of items. A callback with arity 2 also gets the socket,
for example to scope the search to the current user:

    [people: fn query, socket -> MyApp.People.search(socket.assigns.scope, query) end]

A callback that raises, exits or throws, or that returns anything but a
list, fails: `search/3` logs a warning with the prompt name, and the
editor says "Results did not load". A callback runs in the LiveView
process, so a slow one blocks the LiveView while it runs (see `:async`).

## Items

With `:items`, the editor has the items (at most 500) and filters them as
the person types, with no request to the server: each word of the query
must start a word of the label or the hint, with no regard to case or
accents.

An item is a map with an `:id` (a string or an integer), a `:label` (a
string) and these optional keys, with atom or string keys, or an
`{id, label}` tuple:

  * `:hint` - a string shown next to the label;
  * `:text` - the text that an `insert: :text` prompt inserts (the label
    when there is none);
  * `:attrs` - the attributes of the node that an `insert: {:node, type}`
    prompt inserts: a map that encodes to JSON in at most 2048 bytes.

Items that do not have this shape are dropped, and at most 50 results go
to the menu. Control characters (such as a tab or a line break) are
removed from the id, the label, the hint and the text.

## The insert menu

A prompt with `insert: :command` is an insert menu: its items run a
command of the editor on the block of the caret, as one undo step, after
the trigger and the query are removed:

    prompts={[{"/", :insert, insert: :command}]}

Its items are the app's. Two atoms in its `:items` stand for the editor's
own, at their place: `:builtin`, the built-in items of the field's
features (`commands/1`), and `:extensions`, the insert items of the
editor's extensions. With no `:items` and no `:search`, the items are
`[:builtin, :extensions]`. An item of an insert menu has one of:

  * `:command` - a built-in command (`commands/1` lists them: "h2",
    "table", "upload"...), the block of an extension (`"ext:<type>"`), or
    an extension's insert item (`"<extension>:<command>"`);
  * `:node` - a node to insert, as a document has it (a map with a
    `"type"`, at most 2048 bytes in JSON): an embed, a node of the app;
  * `:text` - text to insert (a snippet);
  * `:assist` - the action of a suggestion to ask for, as the Assist menu
    does (`kotoba:assist`, with the text of the block as `"block"`): the
    app streams the answer, which the person accepts or rejects. Only in
    an editor with `assist`. See the [Suggestions](suggestions.md) guide.

and may have a `:group` (the menu shows a heading where the group
changes: "Basic", "Media", "Embeds", the app's own) and `:keywords`, more
words that find it (`/title` finds Heading 1). The editor leaves out an
item whose command or node needs a feature the field does not have.

The component `Kotoba.Components.kotoba/1` sends the triggers, the names,
the options and the items to the editor; the search callbacks stay in the
LiveView. `Kotoba.Live.handle_prompt/3` runs them.

# `callback`

```elixir
@type callback() ::
  (String.t() -&gt; list()) | (String.t(), Phoenix.LiveView.Socket.t() -&gt; list())
```

A prompt callback.

# `insert`

```elixir
@type insert() :: :mention | :text | {:node, String.t()} | :command
```

What a result inserts.

# `item`

```elixir
@type item() :: %{
  :id =&gt; String.t(),
  :label =&gt; String.t(),
  optional(:hint) =&gt; String.t(),
  optional(:text) =&gt; String.t(),
  optional(:attrs) =&gt; map(),
  optional(:command) =&gt; String.t(),
  optional(:node) =&gt; map(),
  optional(:group) =&gt; String.t(),
  optional(:keywords) =&gt; [String.t()],
  optional(:assist) =&gt; String.t()
}
```

An item as the editor receives it.

# `options`

```elixir
@type options() :: %{
  trigger: String.t(),
  name: String.t(),
  search: callback(),
  items: [item() | :builtin | :extensions] | nil,
  label: String.t() | nil,
  spaces: boolean(),
  min_length: non_neg_integer(),
  max_length: pos_integer(),
  insert: insert(),
  with_text: :below | :turn_into,
  async: boolean()
}
```

A prompt with its options, normalized.

# `prompt`

```elixir
@type prompt() :: {String.t(), String.t(), callback()}
```

A prompt, normalized: `{trigger, name, callback}`.

# `prompts`

```elixir
@type prompts() :: [
  {atom() | String.t(), spec()} | {String.t(), atom() | String.t(), spec()}
]
```

A prompt list, in one of the two forms of the module doc.

# `spec`

```elixir
@type spec() :: callback() | {callback(), keyword()} | keyword()
```

A prompt: a callback, a callback with options, or a keyword list.

# `commands`

```elixir
@spec commands(keyword()) :: [item()]
```

Returns the built-in items of an insert menu, for the field's features:
the blocks of text (Text, Heading 1 to 4, Quote, the lists, Code block),
Table, Divider, Image or file and Embed, each with its `:command`,
`:group` and `:keywords`. An item of a feature that the field does not
have is left out, Image or file without `uploads: true`, and Embed
without `embeds: true`.

The options: `:features` (every feature but `blocks` by default, see
`Kotoba.Features`), `:uploads` and `:embeds` (booleans, `false` by
default). `<.kotoba>` gives the field's own.

## Examples

    iex> Kotoba.Prompts.commands(features: [:bold, :headings]) |> Enum.map(& &1.command)
    ["paragraph", "h1", "h2", "h3", "h4"]

# `config`

```elixir
@spec config(prompts() | nil, keyword()) :: %{required(String.t()) =&gt; map()}
```

Returns the options that the editor reads (`data-prompt-config`): for each
prompt with an option that is not the default, a map of the options that
are not, and the items of an `:items` prompt.

## Examples

    iex> Kotoba.Prompts.config(
    ...>   people: [search: fn _ -> [] end, spaces: true],
    ...>   emoji: [items: [%{id: "tada", label: "tada", text: "🎉"}], insert: :text]
    ...> )
    %{
      "emoji" => %{insert: "text", items: [%{id: "tada", label: "tada", text: "🎉"}]},
      "people" => %{spaces: true}
    }

# `filter`

```elixir
@spec filter([item()], String.t()) :: [item()]
```

Filters items for a query as the editor does for an `:items` prompt: each
word of the query must start a word of the label, the hint or the
keywords, with no
regard to case or accents. The items whose label starts with the query
come first.

## Examples

    iex> items = [%{id: "1", label: "Ada Lovelace"}, %{id: "2", label: "Grace Hopper"}]
    iex> Kotoba.Prompts.filter(items, "lov ad")
    [%{id: "1", label: "Ada Lovelace"}]

# `find`

```elixir
@spec find(prompts() | nil, String.t()) :: prompt() | nil
```

Finds the prompt with a name, in a prompt list.

# `find_spec`

```elixir
@spec find_spec(prompts() | nil, String.t()) :: options() | nil
```

Finds the prompt with a name, with its options, in a prompt list.

# `item`

```elixir
@spec item(term()) :: [item()]
```

Normalizes one result item. Returns `[item]`, or `[]` when the item is
not valid. An optional key that is not valid is dropped.

## Examples

    iex> Kotoba.Prompts.item(%{id: 7, label: "Ada", hint: "Engineering"})
    [%{id: "7", label: "Ada", hint: "Engineering"}]

    iex> Kotoba.Prompts.item({"u1", "Grace"})
    [%{id: "u1", label: "Grace"}]

    iex> Kotoba.Prompts.item(%{id: "tada", label: "tada", text: "🎉"})
    [%{id: "tada", label: "tada", text: "🎉"}]

    iex> Kotoba.Prompts.item(%{id: 1, label: "urgent", attrs: %{label: "#urgent"}})
    [%{id: "1", label: "urgent", attrs: %{"label" => "#urgent"}}]

    iex> Kotoba.Prompts.item(%{label: "No id"})
    []

# `labels`

```elixir
@spec labels(prompts() | nil) :: %{required(String.t()) =&gt; String.t()}
```

Returns the map from name to label, for the prompts that have a label,
that the editor reads (`data-prompt-labels`).

## Examples

    iex> Kotoba.Prompts.labels(people: {fn _ -> [] end, label: "People"}, work: fn _ -> [] end)
    %{"people" => "People"}

# `normalize`

```elixir
@spec normalize(prompts() | nil) :: [prompt()]
```

Normalizes a prompt list to `{trigger, name, callback}` tuples. The
callback of an `:items` prompt filters its items.

Raises `ArgumentError` when the list is not valid: a trigger that is not
one character, a name or a trigger that is given twice, a callback that
is not a function of arity 1 or 2, an option that is not one of the
module doc or that has a value that is not valid, more than two prompts
without explicit triggers, or the two forms mixed.

## Examples

    iex> [{trigger, name, _fun}] = Kotoba.Prompts.normalize(people: fn _query -> [] end)
    iex> {trigger, name}
    {"@", "people"}

# `run`

```elixir
@spec run(prompt() | options(), String.t(), Phoenix.LiveView.Socket.t() | nil) :: [
  item()
]
```

Runs the callback of a prompt for a query, and returns the items for the
editor, or `[]` when the callback fails. See `search/3`.

# `search`

```elixir
@spec search(prompt() | options(), String.t(), Phoenix.LiveView.Socket.t() | nil) ::
  {:ok, [item()]} | :error
```

Runs the callback of a prompt for a query. Returns `{:ok, items}`, or
`:error` when the callback fails.

A query that is longer than the prompt's `:max_length` gives no items,
and the callback is not called.

A callback that raises, exits or throws, or that does not return a list,
fails, and a warning with the prompt name goes to the log.

# `specs`

```elixir
@spec specs(prompts() | nil) :: [options()]
```

Normalizes a prompt list to its prompts with their options (see
`normalize/1` for the errors).

# `triggers`

```elixir
@spec triggers(prompts() | nil) :: %{required(String.t()) =&gt; String.t()}
```

Returns the map from trigger to name that the editor reads
(`data-prompts`).

## Examples

    iex> Kotoba.Prompts.triggers(people: fn _ -> [] end, work: fn _ -> [] end)
    %{"#" => "work", "@" => "people"}

---

*Consult [api-reference.md](api-reference.md) for complete listing*
