# `Flicker.FacetSuggest`
[🔗](https://github.com/team-alembic/flicker/blob/v0.1.0/lib/flicker/facet_suggest.ex#L1)

Shared facet-suggestion machinery ([Spec 003](https://github.com/team-alembic/flicker/blob/main/docs/specs/spec-003-faceted-search.md)) —
the pure functions both `Flicker.search/1` and the `facets` attr on
`Flicker.select/1` call to turn a `Flicker.CursorContext` classification
into a suggestion list, and to splice a chosen suggestion back into the
typed text.

This is the "same machinery" the spec calls for: neither component
reimplements key/value suggestion filtering or token replacement, they
both drive this module off `Flicker.CursorContext.classify/3`.

Every suggestion is a `Flicker.Result` — reusing the same display struct
the record-search listbox already renders — tagged
`meta: %{flicker_facet: true, insert: text}`: `:insert` is the literal
text that replaces the current token when the suggestion is chosen
(`"status:"` for a key suggestion, `"status:active "` for a value one).
A caller distinguishes a facet suggestion from an ordinary record result
by checking `result.meta[:flicker_facet]`.

# `suggestion`

```elixir
@type suggestion() :: Flicker.Result.t()
```

A facet suggestion — a `Flicker.Result` tagged for token insertion.

# `classify`

```elixir
@spec classify(
  String.t(),
  [Flicker.Facet.t()],
  non_neg_integer() | nil,
  Flicker.Trigger.t() | nil
) ::
  Flicker.CursorContext.t()
```

Classifies `text` against `facets` at `cursor` — a UTF-16 code-unit
offset, exactly what a real `<input>`'s `selectionStart` reports.

Both components' colocated hooks report the real caret position on
every keystroke/click/selection change (Spec 003's cursor-tracking
follow-up, now closed) — `cursor` is that reported value, converted to
the codepoint offset `Flicker.CursorContext.classify/3` expects via
`Flicker.CursorContext.from_utf16_offset/2`.

`cursor` defaults to (and an explicit `nil` also falls back to) the end
of `text` — the documented fallback for the dead render / very first
event, before the hook has reported a real position yet. Mid-token
editing (moving the caret back into an already-typed token) is
otherwise classified exactly where the caret sits, not "at the end" —
`Flicker.CursorContext` itself has always supported arbitrary cursor
positions and is exercised at every position by its own unit/property
tests; this function is what threads a real one into it.

`trigger` is a `Flicker.Trigger` (Spec 024) or `nil` — with one configured,
facet-key suggestions only fire for a token opening with a trigger grapheme.

# `enum_value_suggestions`

```elixir
@spec enum_value_suggestions(Flicker.Facet.t(), String.t()) :: [suggestion()]
```

Value suggestions for `facet`'s closed value set, filtered by `prefix`.

Two facet types have a closed set to suggest from (Spec 003's type
table):

  * `:enum` — the picklist in `:values`, filtered by `prefix` against the
    value's own key or label (case-insensitive substring).
  * `:boolean` — the fixed pair `true` / `false`, filtered by `prefix`
    (case-insensitive) against each literal's own text — `"f"` suggests
    only `false`.

Returns `[]` for any other facet type — a plain string/numeric/date
facet has no closed picklist (a relationship facet's values come from
`related_search/4` instead).

# `key_suggestions`

```elixir
@spec key_suggestions(String.t(), [Flicker.Facet.t()]) :: [suggestion()]
```

Facet-key suggestions matching `prefix` — 'stat' → 'status:'.

Filters `facets` to those whose key starts with `prefix` (case-sensitive,
matching how facet keys are typed) and maps each to a suggestion whose
`:insert` is `"<key>:"` — landing the cursor right after the operator, so
the very next classification is `{:value, facet, ""}`.

# `related_search`

```elixir
@spec related_search(Flicker.Facet.t(), String.t(), keyword()) ::
  {:ok, [suggestion()]} | {:error, term()}
```

Runs the nested, actor-scoped search for a relationship facet's values
(Spec 003) — a plain `Flicker.Providers.AshResource` search over
`facet.related.resource`, filtered by `prefix`, honouring `actor` and
`tenant` (ADR-004): a record the actor can't read never appears as a
facet-value suggestion.

Only compiles when `ash` is present (ADR-006) — a relationship facet's
`:related` is itself an Ash-only concept.

# `replace_current_token`

```elixir
@spec replace_current_token(String.t(), String.t(), non_neg_integer() | nil) ::
  String.t()
```

Splices `replacement` in for the token at `cursor` (a UTF-16 code-unit
offset, same convention as `classify/3`) — the token boundary is found
the same quote-aware way `CursorContext.classify/3` finds it
(`CursorContext.token_bounds/2`), so a token that is a double-quoted
span containing whitespace (`worker:"Casey N`) is replaced whole rather
than just its trailing word, and anything *after* the replaced token
(free text, or another facet token) survives untouched — completing a
token the cursor was moved back into (mid-token editing, Spec 003's
cursor-tracking follow-up, now closed) never clobbers what follows it.

`cursor` defaults to (and an explicit `nil` also falls back to) the end
of `text`, same fallback `classify/3` documents — every existing caller
that picks a suggestion right after typing it (the overwhelming common
case) needs nothing else.

## Examples

    iex> Flicker.FacetSuggest.replace_current_token("status:acti", "status:active ")
    "status:active "

    iex> Flicker.FacetSuggest.replace_current_token("foo bar stat", "status:")
    "foo bar status:"

A Spec 024 trigger grapheme is part of the token, so it disappears as a
consequence of replacing the token — there is no stripping step, and no way
for `@` to end up in the query:

    iex> Flicker.FacetSuggest.replace_current_token("@stat", "status:")
    "status:"

    iex> Flicker.FacetSuggest.replace_current_token(~s(worker:"Casey N), "worker:123 ")
    "worker:123 "

    iex> Flicker.FacetSuggest.replace_current_token(
    ...>   "status:acti tier:legendary",
    ...>   "status:active ",
    ...>   10
    ...> )
    "status:active tier:legendary"

# `resolve_facets`

```elixir
@spec resolve_facets(map()) :: [Flicker.Facet.t()]
```

Resolves the facet registry `Flicker.select/1`'s `facets:` attr and
`Flicker.search/1` share.

Precedence:

  * `resource:` set (Tier 1) — `facets:` is a list of bare keys /
    `{key, overrides}` pairs, expanded by
    `Flicker.Providers.AshResource.facets/1`.
  * `source:` set (Tier 2) to a provider that implements the optional
    `c:Flicker.Provider.facets/0` callback — that provider's own facet
    registry.
  * `facets:` already a list of `Flicker.Facet` structs — used as-is (a
    hand-built registry, no resource/provider introspection).
  * anything else — `[]` (no facets configured).

# `scoped_facets`

```elixir
@spec scoped_facets(
  String.t(),
  [Flicker.Facet.t()],
  non_neg_integer() | nil,
  Flicker.Trigger.t() | nil
) ::
  [Flicker.Facet.t()]
```

The facets in play for the token under `cursor` — the whole registry, or just
the scope of the trigger that token opens with (Spec 024).

`Flicker.CursorContext.classify/4` applies this scoping internally to decide
*what* the cursor is typing; a caller needs it again to build the suggestion
list itself, and getting it from here rather than from the returned state
keeps `Flicker.CursorContext.t/0` unchanged and every existing `case` over it
exhaustive.

Falls back to the whole registry when no trigger is configured, or when the
current token doesn't open with one — in the latter case the caller is in
`{:value, _, _}` or `:text`, neither of which lists keys.

## Examples

    iex> facets = [Flicker.Facet.new(key: :worker), Flicker.Facet.new(key: :tag)]
    ...>
    ...> Flicker.FacetSuggest.scoped_facets("@wor", facets, nil, %{"@" => [:worker]})
    ...> |> Enum.map(& &1.key)
    [:worker]

    iex> facets = [Flicker.Facet.new(key: :worker), Flicker.Facet.new(key: :tag)]
    ...>
    ...> Flicker.FacetSuggest.scoped_facets("wor", facets, nil, %{"@" => [:worker]})
    ...> |> Enum.map(& &1.key)
    [:worker, :tag]

---

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