Picker & marginalia
The picker: :picker <source> (files, grep, buffers, lines, outline, …) — one vertico-style fuzzy finder over every source, with typed marginalia columns and syntax-highlighted previews.On this page
The picker is one vertico-style fuzzy finder that works over every source. You type :picker <source>, the picker walks that source's candidate set, and the same query line, ranking, preview, and keymap apply no matter what you picked over — files, grep hits, buffers, symbols, registers, commands. Recently-used candidates float to the top automatically across every source.
Each row can carry marginalia: typed, colour-coded annotation columns (a file's size and mtime, a command's keybinding and latency class, a grep hit's path:line:col) that live beside the matchable text rather than baked into it. Rows that contain source code — grep hits, :picker lines, :picker outline — render that code in the buffer's own syntax colours.
Status: shipped — all first-party sources below, marginalia across every source, syntax-highlighted previews for
lines/outline/grep, and frecency (MRU) ranking with disk persistence. LSP-backed pickers (references, symbols, code actions) arepicker-lsp-locationsand live inlsp; the theme picker lives inthemes. Plugin-contributed sources are Phase 7.
Quick reference
Opening a picker
| Command | Opens |
|---|---|
:picker <source> [args] | The named source (see the table below). <Tab> after :picker lists sources. |
:files [root] | File picker (alias for :picker files [root]) |
:recent | Recently-edited files (alias for :picker recent) |
:buffers / :b | Buffer switcher (fuzzy picker; :ls is the static text listing) |
:colorscheme | Theme picker, with live preview (see themes) |
Inside an open picker
| Key | Action |
|---|---|
| (type) | Filter the candidate list (fuzzy match) |
<C-n> / <Down> / <Tab> | Move selection down |
<C-p> / <Up> / <S-Tab> | Move selection up |
<CR> | Accept the selected candidate |
<Esc> / <C-c> | Dismiss (nothing happens; state restored) |
<C-s> | Accept, opening the file in a horizontal split |
<C-v> | Accept, opening the file in a vertical split |
<C-t> | Accept, opening the file in a new tab |
<BS> | Delete the last query character |
<C-w> | Delete the previous word of the query — or, in a picker with depth (dir-pick), go up one level |
<C-u> | Clear the query |
<C-r> | Pick from your yank history and append it to the query |
<C-h> | Help for this picker — closes it and opens its own page |
<C-s> / <C-v> / <C-t> only change where a file-opening accept lands; for sources that don't open a file (registers, commands, …) they behave like <CR>.
Some keys mean something only in some pickers. <C-l> goes into a directory, <C-d> removes a row from the list behind it, <C-q> sends every remaining row somewhere editable, and <Tab> drills in rather than moving down where the picker has depth. Which of these a picker supports — and what exactly they do there — is on that picker's own page: press <C-h> inside it, or :help picker-<source> (:help picker-dir-pick).
The sources
Every id below is valid as :picker <id>. Sources marked "active buffer" read the buffer you were in when you opened the picker. A linked id has its own page — the same one <C-h> opens inside that picker.
| Source | Lists | <CR> does | Args |
|---|---|---|---|
files | Files under the workspace root | Open the file | [root] — directory to walk (default: the active buffer's project root) |
recent | Recently-edited files (MRU) | Open the file | — |
buffers | Every open buffer | Switch to it | — |
lines | Lines of the active buffer | Jump to the line | — |
outline | Tree-sitter symbols in the active buffer | Jump to the symbol | — |
grep | Live recursive text search (rg / ag / grep) | Jump to the hit | [pattern] — seeds the prompt; omit to start empty |
jumps | Position-history ring (jump list + mark ring, newest first) | Jump to the entry | — |
marks | Vim marks | Jump to the mark (same as `) | — |
registers | Vim registers (unnamed, numbered, named) | Paste the register at the cursor | — |
commands | The ex-command palette | Invoke the command | — |
history | Command-line history (newest first) | Load it into the : line — does not run it | — |
search-history | Search-line history (newest first) | Load it into the / line — does not run it | — |
pane-buffer-history | This pane's buffer trail | Walk to that entry | — |
snippets | Snippets for the active buffer's language | Expand the snippet at the cursor | — |
colorscheme | Registered themes (live preview) | Commit the theme | — (usually reached via bare :colorscheme) |
:picker grep is a live source: it re-runs the search backend as you type and streams hits in. It jumps to a single chosen hit — for a persistent, editable multibuffer of all matches, use :search instead.
The four history sources
Four different rings, four different questions, all pickers:
| Source | Reached by | The ring | <CR> |
|---|---|---|---|
history | q:, :history | Every : line you have run | Loads it into the : line, unexecuted |
search-history | q/, q?, :history searches | Every / pattern you have searched | Loads it into the / line, unexecuted |
jumps | <C-o> / <C-i> walk it; :picker jumps lists it | The position ring — jump list and mark ring unified | Jumps there |
pane-buffer-history | <C-6> / <C-7> walk it; :history pane-buffers lists it | Which buffers this pane has shown | Walks there |
:history takes commands (the default), searches or pane-buffers — the same three rings under friendlier names, and :history <Tab> completes them.
q: and q/ are vim's command-window keys, and they behave the way vim's window does in the one respect that matters: the chosen entry lands on the line for you to edit before running, rather than executing on <CR>. Recalling a near-miss and fixing it is the common case; re-running something verbatim is what <Up> on the : line is already for.
Both seed the filter when the line is already open. Type :magit-, realise you want something you ran before, press q: — the picker opens already narrowed to your history entries matching magit-. From an ordinary buffer, with no line open, the filter starts empty.
jumps and pane-buffer-history answer questions that sound alike and are not. jumps is where the cursor has been, across buffers, which <C-o> / <C-i> walk one step at a time. pane-buffer-history is what this pane has displayed — a buffer trail, not a cursor trail, so a file you scrolled through appears once rather than at every position you stopped at. See buffers for the pane trail and modal-editing for the jump list.
The magit sources
magit registers twelve more — one page covers them all, picker-magit. You rarely type these — they are what a magit menu row opens when it needs you to name something — but they are ordinary sources and :picker reaches them like any other.
Seven list git objects and take the ex-command to run on your pick as an argument:
| Source | Lists | Example |
|---|---|---|
magit-branch | Local branches | :picker magit-branch magit-merge |
magit-commit | The last 200 commits (git log) | :picker magit-commit magit-revert |
magit-revision | Branches, remote-tracking refs, tags, then recent commits | :picker magit-revision magit-find-file |
magit-ref | Everything git for-each-ref returns | :picker magit-ref magit-note-merge |
magit-tag | refs/tags/* | :picker magit-tag magit-tag-delete |
magit-remote | Configured remotes (origin, not origin/main) | :picker magit-remote magit-tag-prune |
magit-stash-pick | Stashes | :picker magit-stash-pick magit-stash-apply |
The pick is appended to the command, or substituted for a {} placeholder if there is one — which is how magit-find-file {} <path> puts the revision before the path it belongs in front of.
Five more are self-contained branch wizards, taking no argument because the operation is already decided: magit-branch-checkout-pick, magit-branch-pick-base, magit-branch-create-no-checkout-pick, magit-branch-rename-pick and magit-branch-delete-pick.
magit-revision is not magit-commit. A commit list is this branch's history, which cannot answer "show me this file as it is on origin/main" — a file living on another branch is not in the current branch's history at all, so no number of commits would surface it. magit-revision lists refs first for that reason, and because a branch name is something you recognise where a sha is something you have to look up. Cherry-pick, revert and reset use magit-commit deliberately: they genuinely want a commit, and offering them a branch would be offering the wrong noun.
Commit rows display an abbreviated sha and the subject but pass the full sha, since an abbreviation is ambiguous in principle and git resolves that ambiguity by refusing.
Why a picker and not a prompt
Magit's rule, and a good one to borrow:
Naming a thing that must already exist → picker. Naming a thing you are creating → prompt.
A prompt for an existing name is a typo waiting to happen — git reports it long after the keystroke that caused it, and the thing you wanted was on a list the editor could have shown. A picker for a new name is worse than useless: there is nothing to pick. So Merge branch offers a list and New branch name does not, even though both are about branches.
Tab completion inside :picker
:picker <Tab>lists every source id, each with its one-line summary as marginalia.:picker files <Tab>(and other sources that take an arg) completes the argument through the same machinery:e <Tab>uses — e.g. path completion forfiles.
A source whose argument is required says so, and opening it without one declines with a message naming what it wanted rather than opening an empty list.
Navigating & accepting
Type to narrow. Matching is fuzzy: characters must appear in order, but not adjacently, and the matched characters are highlighted in each row. Ranking blends the fuzzy-match score with a frecency bonus (see MRU ranking), so a strong match wins but recent picks break ties.
Space separates independent fragments. A query is a set of words, all of which must match, in whatever order you type them. So pick refil finds crates/lattice-picker/src/refilter.rs — and so does refil pick. This is the fastest way to narrow a deep tree: type the part of the directory you remember, a space, then the part of the filename you remember, and stop thinking about which came first. Every fragment is highlighted in the row.
Two more pieces of syntax:
| Type this | To get |
|---|---|
parse !test | rows matching parse but not containing test |
my\ file | one fragment with a literal space in it |
\!bang | one fragment starting with a literal ! |
Fragments that land in the order you typed them score slightly higher, so the natural left-to-right guess still floats to the top when several rows match equally well. A query with no space behaves exactly as it always has. If you'd rather have the old single-token behaviour, :set picker.orderless=false.
Live preview. Moving the selection previews the candidate. For lines, grep, and outline the preview scrolls the underlying buffer to (and centres) the target line, so you see the result in context before committing. For colorscheme the whole editor recolours to the highlighted theme; <Esc> restores exactly what you had. Previews never touch the file on disk — dismissing leaves everything as it was.
Where accepts land. By default a picked buffer or location opens in the active pane. :set picker.result.display=split-h (or split-v) makes every picker accept open in a new sibling pane instead; the per-accept <C-s> / <C-v> / <C-t> chords override it for one pick.
Pasting. Your terminal's paste shortcut fills the query. A multi-line paste becomes one line — each line break turns into a space, so pasting two paths gives you foo.rs bar.rs rather than foo.rsbar.rs. Pasting into a transient menu (C-c g and friends) does nothing: those rows are single keys and there is no query to fill.
Picking from your yank history rather than the system clipboard is <C-r> — see yank-ring.
Marginalia
Marginalia are the typed, colour-coded columns to the right of a row's matchable text. Instead of a source hand-padding metadata into one flat string, each source emits typed segments and the renderer resolves each segment's colour from the theme. The row's display (the part fuzzy match scores against) stays clean; the metadata rides alongside.
What each picker surfaces:
| Source | Matchable text | Marginalia columns |
|---|---|---|
commands | command name | bound keybinding · argument hint · doc summary · latency class |
buffers | path | buffer id (#N) · status (• active / + dirty) · kind |
files | path | permissions · size · mtime (eza-style) |
recent | path | permissions · size · mtime |
grep | matched line (preview) | path:line:col |
jumps | buffer label | line:col · source tag (auto / mark / plugin / 'x) |
outline | symbol name | line number |
lines | line text | line number |
marks | mark name | line:col |
registers | register contents | register name ("a, "0, …) |
snippets | prefix (trigger) | snippet name (kind) · description |
Unbound commands, un-stattable paths, and other missing values simply leave the cell blank — no placeholder, no error.
Marginalia is the same mechanism the insert-mode completion popup uses for its annotation column; see completion for that surface.
Colouring the columns
Every column resolves through the theme under the completion.annotation.* namespace, so :colorscheme recolours picker marginalia and buffer syntax in lockstep. The slots (with default dark tones):
| Slot | Default | Used for |
|---|---|---|
completion.annotation.location.path | dim | grep hit path |
completion.annotation.location.line | yellow | line number |
completion.annotation.location.col | dim | column number |
completion.annotation.status.dirty | red | dirty-buffer + |
completion.annotation.status.active | green | active-buffer • |
completion.annotation.latency.reflex | green | [reflex] commands |
completion.annotation.latency.display | blue | [display] commands |
completion.annotation.latency.background | orange | [background] commands |
completion.annotation.args | subtext | command argument hint |
completion.annotation.buffer-id | dim | buffer #N |
completion.annotation.register | purple | register / mark name |
The file-metadata columns (permissions, size, mtime) in the files and recent pickers reuse the permission / size / mtime slots documented in file-tree-mode. Picker rows don't carry a file-type icon column today — icons appear in the file tree and multibuffer headers, not here.
Syntax-highlighted previews
Rows that contain source code render it in the buffer's real syntax colours rather than one flat tone:
:picker linesand:picker outlinecolour the active buffer's line/symbol text using the tree it has already parsed — no extra parsing, no cost on the UI thread.:picker grepcolours each hit's preview by detecting the file's language from its extension and parsing just that one line, off the UI thread alongside the search.
The colours come from the same syntax.* theme slots as the editor, so :colorscheme recolours previews too. Where the fuzzy match overlaps a syntax span, the match highlight wins — you always see what matched first. Files with no supported grammar fall back to a plain single-colour preview.
MRU ranking
The picker keeps a per-source frecency history (recency × frequency): candidates you accept float toward the top next time, decaying over days. This is a property of the picker itself — every source gets it for free, including future plugin sources, as long as the candidate has a stable identity (a file path, a buffer, a command, a register, a mark). Coordinate-only picks (a specific grep line, a jump entry) have no stable identity and are not ranked by history.
The index persists to ~/.config/lattice/cache/ (or the $XDG_CONFIG_HOME equivalent) between sessions. It's a derived cache, not config — if it's ever corrupt the picker discards it and starts fresh rather than refusing to open.
Configuration
| Option | Default | Meaning |
|---|---|---|
picker.orderless | true | Read a query as space-separated fragments that must all match, in any order. Off = the whole query is one literal token. |
picker.mru.enabled | true | Frecency ranking on/off. Off = rank by pure match score. |
picker.mru.recency-half-life-days | 7 | Days for a pick's recency weight to halve. |
picker.mru.cap-per-namespace | 1000 | Max history entries per source before the lowest-frecency one is evicted. |
picker.mru.persist | true | Write the index to disk between runs. |
picker.display | minibuffer | minibuffer (vertico-style, list above the : line) or popup (centred overlay). Also governs transient-mode menus — magit's dispatch menu, its confirmation dialogs, and any future which-key-style overlay built on the same picker substrate — which used to always render as a popup regardless of this setting; they now honor it like every other picker surface. |
picker.result.display | active-pane | Where an accepted buffer/location opens (active-pane, split-h, split-v). |
picker.grep.backend | auto | Grep binary: auto picks the first of rg / ag / grep on PATH; or force one by name. |
picker.grep.max-hits | 2000 | Cap on hits surfaced per :picker grep. |
Set any of these with :set <name>=<value>, or edit them through :customize. See options.
Related pickers
Several features open the same picker over their own sources:
- LSP —
gr(references),:lsp-symbols(document symbols),:lsp-workspace-symbol,:lsp-code-action. Seelsp. - Themes — bare
:colorschemeopens the live-preview theme picker. Seethemes. - Buffers —
:bopens the buffer switcher;:ls/:buffersprint a flat listing. Seebuffers.
They all share this keymap, ranking, and marginalia machinery — the muscle memory carries across.
Not yet
- Plugin-contributed sources. The source trait is designed to be mirrored over WASM in Phase 7; today all sources are first-party.
- A theme colour swatch in the
colorschemepicker (the live full-editor preview ships; a per-row colour chip does not). - Keybinding marginalia for non-command pickers (e.g. showing the open-in-split chord on file rows).