# `Kotoba.Prompts`
[🔗](https://github.com/hattoriai/kotoba/blob/v0.5.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), or `{:node, type}`: a node of the
    editor of that Lexical type, from the item's `:attrs`.
  * `: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 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()}
```

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()
}
```

An item as the editor receives it.

# `options`

```elixir
@type options() :: %{
  trigger: String.t(),
  name: String.t(),
  search: callback(),
  items: [item()] | nil,
  label: String.t() | nil,
  spaces: boolean(),
  min_length: non_neg_integer(),
  max_length: pos_integer(),
  insert: insert(),
  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.

# `config`

```elixir
@spec config(prompts() | nil) :: %{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 or the hint, 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*
