# `MingaEditor.UI.Picker`
[🔗](https://github.com/jsmestad/minga/blob/main/lib/minga_editor/ui/picker.ex#L1)

Generic filterable picker data structure with fuzzy/orderless matching.

A picker holds a list of items and lets the user filter them by typing
a query string, navigate with up/down, and select an item. The picker
is a pure data structure with no side effects — the editor owns the
rendering and action dispatch.

## Fuzzy matching

The query is split on whitespace into segments. Each segment must match
independently somewhere in the candidate label or description (orderless).
Candidates are scored by match quality and sorted best-first:

- Exact prefix match scores highest
- Contiguous substring match scores well
- Fuzzy character-by-character match scores lower
- Shorter candidates score higher (tighter match)

## Usage

    alias MingaEditor.UI.Picker.Item

    picker = Picker.new([
      %Item{id: "pid1", label: "README.md", description: "/project/README.md"},
      %Item{id: "pid2", label: "config.exs", description: "/project/config/config.exs [+]"}
    ], title: "Switch buffer")

    picker = Picker.type_char(picker, "r")
    # filtered to items matching "r"

    picker = Picker.move_down(picker)
    %Item{id: id} = Picker.selected_item(picker)

# `item`

```elixir
@type item() :: MingaEditor.UI.Picker.Item.t()
```

A picker item struct.

# `match_positions`

```elixir
@type match_positions() :: [non_neg_integer()]
```

0-based character indices of matched characters in a string.

# `option`

```elixir
@type option() :: {:title, String.t()} | {:max_visible, pos_integer()}
```

# `t`

```elixir
@type t() :: %MingaEditor.UI.Picker{
  filtered: [MingaEditor.UI.Picker.Item.t()],
  items: [MingaEditor.UI.Picker.Item.t()],
  marked: %{optional(term()) =&gt; true},
  max_visible: pos_integer(),
  query: String.t(),
  selected: non_neg_integer(),
  title: String.t()
}
```

Picker state. The `marked` map uses item ids as keys (values are `true`).

# `backspace`

```elixir
@spec backspace(t()) :: t()
```

Removes the last character from the query and refilters.

# `count`

```elixir
@spec count(t()) :: non_neg_integer()
```

Returns the number of filtered items.

# `filter`

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

Sets the query to an exact value and refilters.

# `marked?`

```elixir
@spec marked?(t(), MingaEditor.UI.Picker.Item.t()) :: boolean()
```

Returns whether an item is marked.

# `marked_items`

```elixir
@spec marked_items(t()) :: [MingaEditor.UI.Picker.Item.t()]
```

Returns all marked items. If none are marked, returns the selected item in a list.

# `match_positions`

```elixir
@spec match_positions(String.t(), String.t()) :: match_positions()
```

Returns the indices of characters in `text` that match the current query,
for use in highlighting matched characters during rendering.

Returns an empty list if the query is empty or doesn't match.

## Examples

    iex> MingaEditor.UI.Picker.match_positions("buffer-switch", "b sw")
    [0, 7, 8]

    iex> MingaEditor.UI.Picker.match_positions("README.md", "")
    []

# `move_down`

```elixir
@spec move_down(t()) :: t()
```

Moves the selection down by one (wraps around).

# `move_up`

```elixir
@spec move_up(t()) :: t()
```

Moves the selection up by one (wraps around).

# `new`

```elixir
@spec new([item()], [option()]) :: t()
```

Creates a new picker with the given items.

# `page_down`

```elixir
@spec page_down(t()) :: t()
```

Moves the selection down by one page (`max_visible` items), clamped to the last item.

# `page_up`

```elixir
@spec page_up(t()) :: t()
```

Moves the selection up by one page (`max_visible` items), clamped to the first item.

# `selected_id`

```elixir
@spec selected_id(t()) :: term()
```

Returns the selected item's id, or nil.

# `selected_item`

```elixir
@spec selected_item(t()) :: item() | nil
```

Returns the currently selected item, or nil if no items match.

# `toggle_mark`

```elixir
@spec toggle_mark(t()) :: t()
```

Toggles the mark on the currently selected item (for multi-select).

# `total`

```elixir
@spec total(t()) :: non_neg_integer()
```

Returns the total number of items (unfiltered).

# `type_char`

```elixir
@spec type_char(t(), String.t()) :: t()
```

Appends a character to the query and refilters.

# `visible_items`

```elixir
@spec visible_items(t()) :: {[item()], non_neg_integer()}
```

Returns the slice of filtered items visible in the picker window,
along with the index of the selected item within that slice.

Returns `{visible_items, selected_offset}`.

---

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