demo.mov
This package provides async fuzzy completion for Emacs backed by
fzf-native.
A shell command streams candidates into a background reader thread;
fzf scores and sorts them in parallel
across all available CPU cores; results land in a standard completing-read
UI incrementally as the shell command is still running.
Built for candidate pools in the hundreds of thousands to millions of lines or files. Scoring runs in a C dynamic module across every available core, never blocking the Emacs main thread.
Design goals that distinguish fzfa from the existing
completion packages like helm, counsel, and consult:
When a single user query drives both a producer-side filter (regex,
shell pattern, …) and a local re-filter (Emacs completion-style,
ivy matcher, helm :match, or orderless filter), the two stages
can disagree about what counts as a match — and the user has no way
to reconcile them from inside one input field.
The pattern recurs across the major async completion packages:
consult-line-multicompiles input to a (potentially non-fuzzy) regexp viaconsult--compile-regexp, scans buffers; surviving lines are re-filtered by the activecompletion-style.counsel-ag/counsel-rg/counsel-git-grep(counsel.el:3246et al.) transform input viaivy--regex, run the shell tool, thenivy’s active matcher (e.g.flx,ivy--regex-plus, fuzzy, …) re-filters the results.helm-grep-ag(helm-grep.el:1641) feedshelm-patterntoagdirectly;helm’s source-level match functions re-filter what comes back.
Basically, the pipeline issue boils down to pre-filtering with a non-fuzzy regexp and then filtering/scoring/sorting with a fuzzy regexp. These two things are incompatible with each other if correctness is important.
Consult’s consult-ripgrep / consult-find / consult-fd /
consult-grep address the gap with an explicit split — input can be
parsed as #SHELL-CMD#QUERY via consult--command-split, so the
user can address each stage independently.
fzfa runs the shell command without the user’s query initially
(e.g. empty query) (unconditionally emitting ALL candidates) and hands
the query only to fzf, which does both filtering and scoring in a
single pass — equivalent to piping the producer’s stdout into fzf
in a shell.
This is conceptually similar to what consult does with
#SHELL-CMD#QUERY except fzfa:
- by default, runs with an empty query to stream all candidates immediately, e.g. a user’s first input is modifying the QUERY vs modifying the SHELL-CMD.
- uses
fzf-nativeunder the hood, so is fast enough to do this without incurring sluggish typing performance. - allows the user to edit the SHELL-CMD fully — see *Editing the shell command mid-session below.
- applies this approach to every
fzfa-*command so the pipeline incongruence is avoided. - each command runs with an empty query to gather candidates, so they can be easily composed into a bigger multi-source command
fzfa uses fzf’s matching algorithm under the hood with fzf-native.
fzf uses a modified Smith-Waterman sequence alignment algorithm.
Every character of the query must appear in the candidate in order,
but not necessarily consecutively — standard fuzzy matching. Bonus
points are awarded for matches at word boundaries, path component
separators, and camelCase transitions, so semantically better
matches rank higher than incidental character matches.
Compared to substring or regex matchers, the algorithm surfaces the
candidate the user usually wanted with less typing. Typing fhf
matches fzfa-helm-flymake ahead of fzfa-find-foo because of
word-boundary alignment.
The alternatives in Emacs today usually have various tradeoffs that
make them unsuitable for large (e.g. 100K+ to millions of files)
codebases. For example, flex in Emacs 31 may have smart fuzzy
matching, but isn’t multithreaded, and doesn’t have multi component
matching. flx may have smart matching but is incredibly slow, being
written in Elisp. orderless does not have smart matching but has
multi component matching. hotfuzz is multithreaded, has smart fuzzy
matching, but has no multi component matching. fzf-native on the
other hand, has smart fuzzy matching, multithreading, and multi
component matching, while also being an industry standard matching
algorithm.
- fzf — original
fzfwritten in Go - telescope-fzf-native.nvim
— C port of
fzf - fzf-native — Emacs wrapper
fzf syntax is supported:
| Token | Match type | Description |
|---|---|---|
sbtrkt | fuzzy-match | Items that match sbtrkt |
'wild | exact-match (quoted) | Items that include wild |
^music | prefix-exact-match | Items that start with music |
.mp3$ | suffix-exact-match | Items that end with .mp3 |
!fire | inverse-exact-match | Items that do not include fire |
!^music | inverse-prefix-exact-match | Items that do not start with music |
!.mp3$ | inverse-suffix-exact-match | Items that do not end with .mp3 |
A single bar character term acts as an OR operator. For example,
the following query matches entries that start with core and end
with either go, rb, or py:
^core go$ | rb$ | py$
Refer to: telescope-fzf-native query syntax
Scoring runs in a C dynamic module
(fzf-native) with worker
threads across all available cores. The shell command streams
candidates into the scoring pool incrementally rather than blocking
on a full materialization, so a million-line rg output is
filterable while it’s still arriving.
Both fussy and fzfa are multithreaded at the C layer. With
fussy, Emacs blocks until the full candidate list is scored and
returned — suitable for in-memory lists. fzfa runs the shell
command in a background process and incrementally refreshes the
completion UI as candidates arrive and are scored.
In casual testing, fzfa and fzf.el are the only packages that
handle 10M+ candidates without visible degradation. Numbers depend on
the candidate shape and the host machine — see *Comparison for the
full matrix.
Most async completion packages are tied to one frontend: helm-rg
needs helm, counsel-rg needs ivy, consult-ripgrep runs through
plain completing-read (so vertico / icomplete but not ivy or
helm). Switching frontends usually means switching command surfaces
— helm-X vs. counsel-X vs. consult-X. Although there’s no
lock-in between packages, it’s likely most users “migrate” to one
package and then use another for specific commands.
fzfa commands, on the other hand, run unchanged under vertico /
icomplete (plain completing-read), ivy / counsel (ivy-mode),
and helm (helm-mode). Each frontend gets a dedicated extension
(fzfa-vertico.el, fzfa-ivy.el, fzfa-helm.el) so users can choose
their preferred completing-read package without feeling locked in to
the ecosystem.
See *Cross-frontend integration for design and implementation.
fzfa-multi-read composes any combination of async producers
(shell commands streamed into the scorer) and sync candidate lists
(static collections, history rings, etc.) into a single picker, with
per-source narrowing, preview, and apply (helm persistent actions / ivy-call).
consult supports mixed async + sync multi-source via consult--multi,
but consult-buffer and its variants are the only core callers and use
only :items (sync) sources. Third-party packages (consult-omni,
consult-web) exercise the async-in-multi path.
ivy/counsel has no multi-source primitive (counsel-buffer-or-recentf concatenates two lists into a
flat collection, no per-source semantics);
helm has native multi-source but re-shells per keystroke on async sources, which
limits the candidate counts it can handle. See Multi-source for
the comparison.
Required dependency: fzf-native (provides the C scoring module).
(use-package fzf-native
:vc (:url "https://github.com/dangduc/fzf-native" :rev :newest))Minimal setup:
(use-package fzfa
:vc (:url "https://github.com/jojojames/fzfa" :rev :newest))Recommended:
(use-package fzf-native
:vc (:url "https://github.com/dangduc/fzf-native" :rev :newest))
(use-package fussy
:vc (:url "https://github.com/jojojames/fussy" :rev :newest)
:config
(fussy-setup-fzf)
(fussy-eglot-setup)
(fussy-company-setup))
(use-package fzfa
:vc (:url "https://github.com/jojojames/fzfa" :rev :newest))This sets up a consistent completing-read and completion-in-region
experience using fzf as the core filtering/scoring algorithm.
;; No explicit setup needed — just call any command:
M-x fzfa-find-any
or:
fzfa-find-some
fzfa-rg
fzfa-fd
fzfa-find
fzfa-git-ls-filesAll commands open the selected candidate in Emacs. The tables
below cross-reference fzfa commands with their closest equivalents
in other completion packages, on a best-effort basis.
fzfa | counsel | consult | helm |
|---|---|---|---|
fzfa-find | counsel-find-file | consult-find | helm-find-files |
fzfa-fd | counsel-fd | consult-fd | helm-find-files |
fzfa-rg-files | — | consult-find | helm-find-files |
fzfa-ag-files | — | — | — |
fzfa-git-ls-files | counsel-git | — | helm-ls-git |
fzfa-hg-files | — | — | — |
fzfa-recent-file | counsel-recentf | consult-recent-file | helm-recentf |
fzfa-locate | counsel-locate | consult-locate | helm-locate |
fzfa-shell-command | — | — | — |
fzfa-shell-project-command | — | — | — |
(macOS Spotlight commands — fzfa-spotlight, -spotlight-apps,
-spotlight-audio — ship in the spotlight extension, on by default.
See Extensions below.)
fzfa | counsel | consult | helm |
|---|---|---|---|
fzfa-buffer | ivy-switch-buffer | consult-buffer | helm-buffers-list |
fzfa-bookmark | counsel-bookmark | consult-bookmark | helm-filtered-bookmarks |
fzfa-M-x | counsel-M-x | — | helm-M-x |
fzfa-M-x-for-buffer | — | consult-mode-command | — |
fzfa-minor-mode-menu | counsel-minor | consult-minor-mode-menu | — |
fzfa-yank-pop | counsel-yank-pop | consult-yank-pop | helm-show-kill-ring |
fzfa-theme | counsel-load-theme | consult-theme | helm-themes |
fzfa-tramp | counsel-tramp | consult-tramp | helm-tramp |
fzfa-imenu | counsel-imenu | consult-imenu | helm-imenu |
fzfa-imenu-all | — | consult-imenu-multi | helm-imenu-in-all-buffers |
fzfa-imenu-all-but-current | — | — | — |
fzfa-eglot-symbols | — | consult-eglot-symbols | helm-lsp-workspace-symbol |
fzfa-outline | counsel-outline | consult-outline | helm-outline |
fzfa-org-heading | counsel-org-goto | consult-org-heading | helm-org-in-buffer-headings |
fzfa-org-heading-all | counsel-org-goto-all | consult-org-heading (with prefix) | helm-org-agenda-files-headings |
fzfa-org-agenda | — | consult-org-agenda | helm-org-agenda-files-headings |
fzfa-org-todo | counsel-org-todo (related) | — | — |
fzfa-org-tags-view | — | — | helm-org-tags |
fzfa-org-insert-link | counsel-org-link | — | — |
fzfa-org-any | — | — | — |
fzfa-mark | counsel-mark-ring | consult-mark | helm-mark-ring |
fzfa-global-mark | counsel-mark-ring | consult-global-mark | helm-global-mark-ring |
fzfa-register | counsel-register | consult-register-load | helm-register |
fzfa-flymake | — | consult-flymake | helm-flymake |
fzfa-flymake-project | — | consult-flymake (with prefix arg) | — |
fzfa-compile-error | counsel-compilation-errors | consult-compile-error | — |
fzfa-shell-history | counsel-shell-history / counsel-esh-history | consult-history | helm-eshell-history |
fzfa-project-find-file | counsel-projectile-find-file | — | helm-projectile-find-file |
fzfa-project-find-dir | counsel-projectile-find-dir | — | helm-projectile-find-dir |
fzfa-project-buffer | counsel-projectile-switch-to-buffer | consult-project-buffer | helm-projectile-switch-to-buffer |
fzfa-project-recentf | counsel-projectile-recentf | — | helm-projectile-recentf |
fzfa-project-switch-project | counsel-projectile-switch-project | — | helm-projectile-switch-project |
Grep-style commands parse FILE:LINE:CONTENT output and jump directly to
the matching line.
fzfa | counsel / ivy | consult | helm |
|---|---|---|---|
fzfa-rg | counsel-rg | consult-ripgrep | helm-rg |
fzfa-ag | counsel-ag | — | helm-do-ag |
fzfa-git-grep | counsel-git-grep | consult-git-grep | helm-grep-do-git-grep |
fzfa-git-log-grep | counsel-git-log | consult-git-log-grep | — |
fzfa-grep | counsel-grep | consult-grep | helm-do-grep-ag (any backend) |
fzfa-grep-current-file | counsel-grep | consult-line | — |
fzfa-ugrep | — | — | — |
fzfa-swiper | swiper | consult-line | helm-occur |
fzfa-swiper-all | swiper-all | consult-line-multi | helm-occur-visible-buffers |
fzfa-regexp | — | — | — |
fzfa-regexp scans the current buffer for an Elisp regexp (the CMD
half of the prompt); FZF re-scores the matched lines against the
FILTER half. fzfa-regexp-match underlines the matched region.
Empty CMD emits every line so FZF becomes the primary filter.
The hungry commands derive their search scope from the currently open buffers.
They collect buffer-file-name for every file-visiting buffer, extract the
parent directory of each, then deduplicate: if directory A is a prefix of B,
B is dropped since A’s recursive search already covers it. The resulting
directory list is passed as arguments to a single shell command.
| Command | Tool | What it searches |
|---|---|---|
fzfa-hungry-find | fd / find | Files under all derived directories |
fzfa-hungry-swiper | rg / grep | Line content under all derived dirs |
fzfa | counsel | consult | helm |
|---|---|---|---|
fzfa-find-any | counsel-buffer-or-recentf | consult-buffer | helm-mini / helm-multi-files |
fzfa-find-some | counsel-buffer-or-recentf | consult-buffer | helm-mini / helm-multi-files |
fzfa-passwords | — | — | — |
fzfa-evil-any | — | — | — |
fzfa-find-any merges several sources into a single
completing-read (groups for buffers, recent files, hungry-find, …),
ranks each group by its top fzf score, and dispatches the chosen
candidate back to the originating command’s action.
The command list lives in fzfa-find-any-commands — by default
(fzfa-imenu fzfa-buffer fzfa-recent-file fzfa-hungry-find
fzfa-imenu-all-but-current fzfa-M-x fzfa-hungry-swiper fzfa-locate).
Add any other arg-less fzfa command to that list and it shows up as
its own group, with no source plist or factory function to maintain:
the source is derived from the command’s existing definition via the
:extract / :inject dispatch in fzfa-completing-read (see
architecture.org for the full flow).
(setq fzfa-find-any-commands
'(fzfa-buffer
fzfa-recent-file
fzfa-hungry-find
fzfa-spotlight-apps))For ad-hoc combinations, call fzfa-multi-read directly:
(defun my/fzfa-files-only ()
(interactive)
(fzfa-multi-read
'(fzfa-recent-file fzfa-hungry-find fzfa-locate)
:prompt "files: "))fzfa-passwords is a built-in multi over fzfa-pass-copy
and fzfa-chrome-pass-copy — one prompt covers both
password-store and Chrome’s password manager, with the chosen
entry’s password copied to the kill ring via the originating source.
Requires the chrome extension (and on macOS, the deps listed for
Chrome passwords above).
A multi-source session has three states. The narrow menu is a transient modal — it captures one keystroke, dispatches, and goes away. Outside the menu the minibuffer is always in normal query-input mode.
+------ source letter ------+
v |
[1] Widened ----<----> [2] Narrow menu ----<-----> [3] Narrowed
| |
+-- other key cancels back -+
| State | Menu visible? | Prompt overlay | Minibuffer input |
|---|---|---|---|
| 1 Widened | no | PROMPT [N/](T) | query against all |
| 2 Narrow menu | yes | KEY:NAME ... <:widen | (captured by handler) |
| 3 Narrowed | no | PROMPT {NAME} [N/](T) | query within one source |
Opening the menu. Press fzfa-multi-narrow-key (default <)
from state 1 or state 3. The menu replaces the prompt overlay
with every source as KEY:NAME (active source highlighted with
the minibuffer-prompt face) and a trailing <:widen marker
faced shadow.
From the menu (state 2), three exits:
| Key | Action | Result |
|---|---|---|
| source letter | Narrow or switch to that source | -> state 3 |
fzfa-multi-narrow-key | Widen — restore all sources | -> state 1 |
| anything else | Cancel — leave narrow state unchanged | -> prior |
“Anything else” includes RET, ESC, SPC, and any pool char
that isn’t allocated. No error, no message — the menu just
dismisses.
Switching while narrowed. Use the menu: e.g. from state 3
narrowed to recent-file, press < to open the menu, then b
to switch to buffer (-> state 3 with the new source).
Wrapping. Long source lists wrap to multiple lines based on the
active minibuffer window width; resize-mini-windows (Emacs
default) grows the minibuffer to fit. An oversize single entry
stays whole on its own line — no mid-entry splits.
Narrow keys are allocated automatically — one length-1 character
per source, derived from the source’s :name via a two-pass
algorithm:
- Any explicit
:narrowin the command list is reserved first. - Each remaining source tries the first character of each word in
its hyphen-split name (case preserved), then falls back to a
pool of
a-z/A-Z/0-9.
So the default fzfa-find-any-commands resolves to:
| Source | Key |
|---|---|
fzfa-imenu | i |
fzfa-buffer | b |
fzfa-recent-file | r |
fzfa-hungry-find | h |
fzfa-imenu-all-but-current | a |
fzfa-M-x | M |
fzfa-hungry-swiper | s |
fzfa-locate | l |
To override a derived key, replace a bare symbol with a
(COMMAND :narrow KEY) form. KEY is a single character — symbol,
?char, or string:
(setq fzfa-find-any-commands
'((fzfa-vc-modified-files :narrow V)
(fzfa-imenu :narrow I)
fzfa-buffer
(fzfa-recent-file :narrow R)
fzfa-hungry-find
fzfa-imenu-all-but-current
fzfa-M-x
fzfa-hungry-swiper
fzfa-locate))Duplicate explicit keys signal an error at session start so collisions surface loudly instead of silently shadowing.
Demo below:
narrowing.mov
A meaningful difference from the alternatives is that fzfa-multi-read
can mix both synchronous and asynchronous sources in a single picker.
On every keystroke each source is re-queried — sync sources via
fzf-native-score-all, async sources via fzf-native-async-candidates
against the live producer pool — and the per-source top results are
merged with a per-source rank for the final order. This lets one
prompt combine, say, recentf entries (sync list) alongside a
streaming rg search (async producer) and have both groups score
against the same query as the user types:
| Frontend | Mixed async + sync multi-source |
|---|---|
fzfa (fzfa-multi-read) | yes — any combination of :candidates and :command sources |
helm (helm :sources) | yes — candidates + candidates-process sources side by side |
consult (consult--multi) | supported; no built-in commands uses it |
ivy / counsel | no multi-source mechanism |
Optional integrations are located in extension files (fzfa-pass.el,
fzfa-notmuch.el, etc.) and are opt-in via fzfa-extensions — a
list of short symbols.
| Symbol | Library | Soft dependency | Commands |
|---|---|---|---|
chrome | fzfa-chrome | python3 (history); macOS dependencies (passwords) | bookmarks: fzfa-chrome-bookmarks (default), -edit, -bookmark-copy-url, -refresh — JSON-parses Chromium-family Bookmarks file. history: fzfa-chrome-history (default), -history-copy-url — async stream from Chrome’s History SQLite DB via an embedded Python helper (lock-copy + sqlite3). passwords (macOS only): fzfa-chrome-pass-copy (default), -copy-username, -url, -refresh — decrypts entries from Chrome’s Login Data DB using the keychain entry Chrome Safe Storage. All URLs open via fzfa-chrome-browser-function (defaults to open -a "Google Chrome" on darwin, google-chrome on linux) so picks land in Chrome regardless of the OS default browser |
company | fzfa-company | company | fzfa-company — fuzzy-filter current company-mode candidates and finish |
eglot | fzfa-eglot | eglot | fzfa-eglot-symbols — workspace-symbol picker via the running eglot server. Async-firing producer using jsonrpc-async-request; CMD-half is the query string, FZF re-scores the snapshot. Async producers are not supported under helm-mode |
embark | fzfa-embark | embark | Sub-keymap on embark-general-map under the Z prefix (mnemonic: f*Z*fa) puts every fzfa command behind a single key chord reachable from any embark target. Direct (non-prefixed) target-aware bindings on embark-region-map / -identifier-map (R rg, G grep, A ag, U ugrep, S swiper — target seeds the fzf filter), embark-file-map (G grep this file, R rg in file’s dir), embark-buffer-map (I imenu, S swiper in target buffer), embark-flymake-map (F flymake, P flymake-project), embark-email-map (N notmuch, T notmuch-tree on the address). Also wires Z on embark-become-match-map for sync source-swap. |
evil | fzfa-evil | evil | fzfa-evil-marks (jump to a buffer-local or global mark, candidate includes BUFFER:LINE: <content> so fzf scores against the line preview), fzfa-evil-registers (paste text registers / execute macro-vector registers), fzfa-evil-jumps (window jump list — C-o~/~C-i history — with line preview when the buffer is loaded), fzfa-evil-ex-history (re-run an entry from evil-ex-history via evil-ex-execute), fzfa-evil-search-history (re-run a pattern from evil-ex-search-history and update evil-ex-search-pattern so n~/~N continue working), fzfa-evil-command-window (unified ex + search history picker grouped by source), fzfa-evil-any (multi-source picker over the other commands, configurable via fzfa-evil-any-commands) |
firefox | fzfa-firefox | sqlite3 CLI (bookmarks); python3 (history) | bookmarks: fzfa-firefox-bookmarks (default), -bookmark-copy-url, -refresh — reads places.sqlite via sqlite3 (lock-copy + recursive CTE for folder breadcrumbs, tags subtree excluded). history: fzfa-firefox-history (default), -history-copy-url — async stream from moz_places via an embedded Python helper. Profile auto-detection: globs Profiles/*.default-release first, falls back to profiles.ini (prefers [Install*] Default= over legacy [Profile*] Default=1). URLs open via fzfa-firefox-browser-function (open -a Firefox on darwin, firefox on linux). Passwords intentionally out of scope (NSS / key4.db has no clean shell pipeline) |
flymake | fzfa-flymake | none (built-in) | fzfa-flymake (current buffer), fzfa-flymake-project (all buffers in the current project) |
info | fzfa-info | none (built-in) | fzfa-info-emacs, fzfa-info-elisp, fzfa-info-org, fzfa-info-cl, fzfa-info-eieio, fzfa-info-magit — pick an index entry from a specific Info manual; fzfa-info is the multi-source picker over fzfa-info-commands; fzfa-info-at-point delegates to `info-lookup-symbol’ for the symbol at point |
mail | fzfa-mail | macOS Mail.app | fzfa-mail, fzfa-mail-refresh — browse and open inbox messages |
make | fzfa-make | make / ninja (CLI) | fzfa-make (locate Makefile~/~build.ninja and call make) |
music | fzfa-music | macOS Music.app | fzfa-music, -by-artist, -by-genre, -playlist, -playlist-shuffle, -refresh |
notmuch | fzfa-notmuch | notmuch (CLI + Emacs package) | fzfa-notmuch, fzfa-notmuch-tree — run a notmuch query and fuzzy-pick a thread to open |
org | fzfa-org | none (built-in) | fzfa-org-heading (current buffer), fzfa-org-heading-all (all live org buffers), fzfa-org-agenda (all `org-agenda-files’), fzfa-org-todo (TODO-state headings in agenda files, excludes DONE-class), fzfa-org-tags-view (pick a tag then a tagged entry), fzfa-org-insert-link (pick an entry, insert an org-link at point), fzfa-org-any (multi-source over `fzfa-org-any-commands’). Candidates render SOURCE:LINE:STARS [TODO] HEADING :tags:; selection reveals folded entries. |
pass | fzfa-pass | password-store | fzfa-pass (copy), fzfa-pass-edit, -rename, -delete, -add, -generate, -url |
project | fzfa-project | none (built-in) | fzfa-project-find-file, fzfa-project-find-dir, fzfa-project-buffer, fzfa-project-recentf, fzfa-project-switch-project — candidate sets driven by project-files / project-buffers / project-known-project-roots |
regexp | fzfa-regexp | none (built-in) | fzfa-regexp — scan the current buffer for an Elisp regexp. Sync-firing producer; CMD-half is the regexp pattern, FZF re-scores matched lines against FILTER. Empty CMD emits every line. fzfa-regexp-match face underlines the match |
safari | fzfa-safari | macOS only; python3 (history) | bookmarks: fzfa-safari-bookmarks (default), -bookmark-copy-url, -refresh — parses Bookmarks.plist via plutil -convert json and walks WebBookmarkTypeList~/~Leaf nodes. Special roots remapped: BookmarksBar → “Favorites”, BookmarksMenu → “Bookmarks Menu”, com.apple.ReadingList → “Reading List”. history: fzfa-safari-history (default), -history-copy-url — async stream from History.db joining history_visits on history_items. Requires Emacs to have Full Disk Access (System Settings ▸ Privacy & Security) — both loaders surface a clear FDA-pointing user-error on permission denial. URLs open via fzfa-safari-browser-function (open -a Safari). Passwords intentionally out of scope (delegated to the macOS Keychain) |
spotlight | fzfa-spotlight | macOS mdfind | fzfa-spotlight, fzfa-spotlight-apps, fzfa-spotlight-audio — application launcher |
vc | fzfa-vc | none (built-in) | fzfa-vc-modified-files — multi-source picker over the current repository’s modified / added / staged / HEAD files. Detects the backend with vc-responsible-backend (cheap — walks parents) and dispatches to per-backend commands shipped by sibling extensions: fzfa-git-modified-locally, fzfa-git-added-files, fzfa-git-staged-for-commit, fzfa-git-modified-in-head (in git extension) and fzfa-hg-modified-locally, fzfa-hg-added-files, fzfa-hg-modified-in-head (in hg extension). Per-backend commands are also callable directly without going through the dispatcher. Extend to other backends via fzfa-vc-modified-files-sources |
Default value of fzfa-extensions:
(ag chrome company eglot emacs embark evil fd find firefox flymake git grep
helm hg hungry imenu info ivy locate mail make music notmuch org pass
project regexp rg safari shell spotlight ugrep vertico vc)To modify which extensions are enabled, modify fzfa-extensions:
Removing an extension from fzfa-extensions skips its
-setup call but leaves the extension’s commands autoloaded —
they still appear in commands like M-x.
To hide excluded extensions, call fzfa-sync-autoloads after setting
fzfa-extensions — typically from :init so the prune happens before
any fzfa command can be invoked:
(use-package fzfa
:vc (:url "https://github.com/jojojames/fzfa" :rev :newest)
:defer t
:init
(setq fzfa-extensions '(ag rg vertico embark))
(fzfa-sync-autoloads))fzfa-sync-autoloads walks fzfa--extension-registry and
unbinds autoloads for any extension not present in fzfa-extensions.
Quick example — drop firefox from the default list:
(use-package fzfa
:vc (:url "https://github.com/jojojames/fzfa" :rev :newest)
:defer t
:init
(setq fzfa-extensions (delq 'firefox (default-value 'fzfa-extensions)))
(fzfa-sync-autoloads))After :init runs: M-x fzfa-firefox- no longer completes,
(fboundp 'fzfa-firefox-bookmarks) returns nil, every other
extension stays untouched.
Every async command (fzfa-rg, fzfa-grep, fzfa-fd, fzfa-find,
…) splits its minibuffer input into a shell-COMMAND part (re-runs
the underlying process on change) and an fzf-FILTER part
(rescores/sorts in place via fzf-native). With the default perl
split style the buffer text has shape #COMMAND#FILTER.
By default the COMMAND region is invisible and only the FILTER
portion appears editable. Press > inside the minibuffer to cycle
through three display modes:
hidden → the COMMAND region is hidden behind a display "" overlay
(default; the typing experience is just FILTER).
compact → only the program name + trailing argument slot show;
the flags collapse into ` ... '.
full → the whole #COMMAND#FILTER buffer is shown verbatim.
Concretely, for fzfa-rg (whose underlying command is roughly
rg --line-number --no-heading --with-filename %s ''):
hidden: | compact: #rg ... '|'# full: #rg --line-number --no-heading --with-filename %s '|'#
Editing inside compact or full replaces the active shell command
(debounced via fzfa-shell-command-debounce /
fzfa-shell-command-throttle). Demoting back to hidden keeps
those edits live — there is no commit/discard step; the subprocess
has already restarted on each meaningful change.
Customize fzfa-display-key to change the cycling key, or
set it to nil to disable the feature entirely (the session is
stuck in whatever :display the caller passed; default = hidden).
< remains reserved for fzfa-multi-narrow-key (source
narrow/widen in multi-source sessions).
The main difference compared to consult’s asynchronous commands is
that fzfa lets you modify the entire shell command, not just an
argument slot. Refer to https://github.com/minad/consult#asynchronous-search
for a more exhaustive set of features/advantages consult may
provide over this implementation.
Comparison demo below (first is consult-ripgrep, second is
fzfa-rg with the > key revealing the underlying command, using
fussy as the completion-style.):
demo2.mov
Some tasks have multiple interchangeable backends — finding files
(fd, rg --files, ag -g, plain find) and content search (rg,
ugrep, ag, plain grep) are the canonical examples.
fzfa-smart-define generates a single command that resolves to
whichever backend is actually installed on the current host.
Two commands are pre-defined in fzfa.el:
(fzfa-smart-define
'find
'((fzfa-fd :executable "fd")
(fzfa-rg-files :executable "rg")
(fzfa-ag-files :executable "ag")
(fzfa-find :executable "find")))
(fzfa-smart-define
'grep
'((fzfa-ugrep :executable "ugrep")
(fzfa-rg :executable "rg")
(fzfa-ag :executable "ag")
(fzfa-grep :executable "grep")))These define fzfa-smart-find and fzfa-smart-grep. Each clause is
(CMD &key executable predicate). Clauses are tried in order; the
first whose :executable is on PATH (and whose :predicate, if
supplied, returns non-nil) wins, provided CMD is fboundp. A clause
with neither key is an unconditional fallback.
Because the generated command simply funcall s the resolved backend
(rather than reimplementing it), it composes transparently with
multi-source dispatch: drop fzfa-smart-find into
fzfa-find-any-commands (or any other list passed to fzfa-multi-read)
and the multi-source machinery extracts the chosen backend’s
:command / :category / :group as if it were that backend
directly. Inline shell editing via > works the same — the smart
command transparently inherits the display cycling of the underlying
backend.
fzfa registers and uses its own completion-style named fzfa.
This style is only a passthrough: it accepts the query string as-is and forwards
it directly to the fzf scoring layer without applying any transformation.
No setup is required on your part. Every fzfa entry point dynamically
binds completion-styles to (~fzfa)~ for the duration of its
completing-read, so the resolved style list inside fzfa is exactly
(~fzfa)~ regardless of what you have in global completion-styles or
in completion-category-overrides. This means fzfa is unaffected by,
and also does not affect other style packages (e.g. orderless, hotfuzz,
fussy, flex, etc.)
The recommended companion package is
fussy,
for general completing-read and completion-at-point (e.g. code
completion). Both fussy and fzfa are backed by the same
fzf-native
module, giving you consistent fuzzy matching semantics with fzf across
both synchronous and asynchronous contexts.
fussy operates synchronously on in-memory candidate lists and integrates
with company, corfu, eglot, and all standard completing-read
frontends. fzfa handles the case where candidates come from a shell
command (e.g. find or ripgrep) and must be streamed incrementally.
(use-package fzf-native
:vc (:url "https://github.com/dangduc/fzf-native" :rev :newest))
;; Synchronous fuzzy completion for code, buffers, M-x, etc.
(use-package fussy
:vc (:url "https://github.com/jojojames/fussy" :rev :newest)
:config
(fussy-setup-fzf)
(fussy-eglot-setup)
(fussy-company-setup))
;; Async fuzzy completion for large file/grep searches.
(use-package fzfa
:vc (:url "https://github.com/jojojames/fzfa" :rev :newest))fzfa works through the standard Emacs completing-read API and is
compatible with any frontend that calls the completion table function on
each input change.
| Frontend | Status | Notes |
|---|---|---|
vertico | Supported | Recommended. Generation-based refresh via vertico--exhibit. |
fido | Supported | Built on icomplete; works without extra configuration. |
ivy/counsel | Supported | Push model via ivy--set-candidates. See below. |
helm | Supported | Dedicated handler path with stats / live preview / annotations / narrow-by-source. |
icomplete | Supported | Refreshes via icomplete-exhibit. |
Caveat: I mostly use vertico these days so wasn’t exhaustive with using the
other completion systems.
There’s also a vertico extension that wires up
fzfa-vertico-columns-mode, a per-group column layout for multi-source
sessions. Each completion group renders as its own vertical column instead
of vertico’s default stacked layout, which keeps every source visible at a
glance and removes the need to scroll past one group to reach another.
grid.mov
When the number of groups exceeds fzfa-vertico-columns-max (3 by
default), the overflow groups wrap into additional bands stacked below.
Per-source scrolling kicks in when a column has more candidates than the band’s row budget — the column containing the selection scrolls to keep it visible while the other columns stay parked at the top.
Source-column ordering follows fzf score by default
(fzfa-vertico-columns-source-sort = scored): with a query active,
each group is sorted by its first candidate’s score, so the
strongest-matching source’s column floats to the left and the cursor
at flat index 0 sits at column 0 row 0. On empty input, fall back to
declared order. Set the option to declared to
pin declared order in all cases, alphabetical for lexicographic
group names, or a function for custom ordering.
Navigation while the mode is active:
| Key | Action |
|---|---|
M-l | Next source in reading order (wraps at band edge) |
M-h | Previous source |
M-j | Next band (same column-in-band; whole-band jump) |
M-k | Previous band |
<up> / <down> | Within the current source’s column |
<left> / <right> | Alias for M-h / M-l |
Activation is per-category via vertico-multiform-mode rather than
global: fzfa-vertico-setup adds each category listed in
fzfa-vertico-multiform-categories (defaults to (fzfa-multi)) to
vertico-multiform-categories as (CATEGORY fzfa-vertico-columns-mode),
and enables vertico-multiform-mode if not already on. Columns then
activate inside fzfa-multi-read / fzfa-find-any / fzfa-find-some
sessions and tear down on exit — single-source pickers, file finders,
and unrelated minibuffer prompts are untouched.
Set fzfa-vertico-columns-auto to nil to skip all auto-wiring (e.g. to
enable the mode globally yourself or scope it differently). Add other
categories to fzfa-vertico-multiform-categories to extend the layout
to other multi-group pickers (for example imenu when its buckets are
group-functioned).
| Variable | Default | Description |
|---|---|---|
fzfa-vertico-columns-max | 3 | Maximum number of columns rendered per band; overflow wraps into additional bands. |
fzfa-vertico-columns-page-size | 6 | Maximum sources displayed simultaneously; pagination kicks in beyond. |
fzfa-vertico-columns-min-width | 12 | Minimum width per column, in characters. |
fzfa-vertico-columns-max-width | 60 | Maximum width per column, in characters. |
fzfa-vertico-columns-separator | ” | ” | String inserted between columns. |
fzfa-vertico-columns-headers | t | When non-nil, render group names as a header row above each column. |
fzfa-vertico-columns-header-face | fzfa-vertico-columns-header | Face applied to the column header row. |
fzfa-vertico-columns-auto | t | When non-nil, fzfa-vertico-setup wires per-category activation; nil = manual setup. |
fzfa-vertico-multiform-categories | (fzfa-multi) | Completion categories that auto-activate columns mode. |
fzfa-vertico-columns-truncate | auto | How to truncate candidates exceeding column width (auto / ellipsis / nil). |
fzfa-vertico-columns-ellipsis | “…” | String used to mark truncated candidates (falls back to “…” on systems lacking “…”). |
fzfa-vertico-columns-source-sort | scored | Column order: scored (top-fzf-score first), declared, alphabetical, or a function. |
Ivy support lives in fzfa-ivy.el, loaded as part of
fzfa-extensions (which includes ivy by default). Loading the
file alone is a no-op; fzfa-ivy-setup — called from fzfa-setup
— defers its work via (with-eval-after-load '~ivy …)~ so the
display-transformer registration kicks in only when ivy itself is
actually loaded.
ivy.mov
The fzfa entry points share their main code path with the
other completing-read frontends, with ivy-specific bypass + push
plumbing layered on:
| Entry point | Ivy plumbing |
|---|---|
fzfa-completing-read | :dynamic-collection t + ivy-push closure mirroring the cmd-split + restart logic of the ('t) body |
fzfa-multi-read | :dynamic-collection t + ivy-push-multi per-source iterate/score/rank/concat + display transformer + actions for narrow |
Ivy’s push model means typing doesn’t re-call the collection
function the way vertico/icomplete do — so the polling timer’s
refresh path dispatches to the corresponding ivy-push-* closure
under ivy-mode, which forces an ivy--set-candidates +
ivy--exhibit + ivy--insert-prompt against the active minibuffer
window. The :dynamic-collection t binding stops ivy from running
its re-builder regex on top of fzf-scored output (which would
silently drop matches for multi-word and fzf-extended-syntax
queries).
Ivy ignores the group-function completion-metadata key that
vertico / icomplete support, so multi-source sessions render as a
flat merged list by default. fzfa-ivy.el registers a display
transformer under ivy’s t fallback caller that prepends
~[source-name] ~ to each candidate instead.
The face for the label is fzfa-ivy-multi-source-label (inherits
font-lock-comment-face by default).
Under ivy-mode, pressing fzfa-multi-narrow-key (default <)
triggers ivy-dispatching-call rather than fzfa’s in-house
narrow-handler. Each source’s :narrow letter is registered
into ivy--actions-list under the t fallback caller, plus a
widen entry on the narrow-prefix key itself — so the existing
<< muscle memory still widens. ivy renders the action menu in
its native UI (no contention with the prompt area), the chosen
action mutates narrow-idx, and ivy-call semantics keep you in
the picker rather than exiting.
Post-restrict typing semantics: after S-SPC, ivy flips
ivy-state-dynamic-collection to nil and switches the session to a
static list — fzfa is no longer in the loop.
Further typing is filtered by ivy-re-builders-alist (substring /
regex by default). If you want the same fuzzy matching algorithm
to continue against the restricted set, install fussy and call
fussy-ivy-setup — fussy’s matcher overrides ivy’s regex filter
and keeps the fuzzy-match feel after S-SPC. Setup example below.
To get a consistent sorting/filtering experience in ivy, use fussy
and call fussy-ivy-setup. This handles the regular completing-read
case and fzfa handles the async completing-read cases.
(use-package fussy
:vc (:url "https://github.com/jojojames/fussy" :rev :newest)
:config
(fussy-setup-fzf)
(fussy-eglot-setup)
(fussy-company-setup)
(fussy-ivy-setup))
(use-package fzfa
:vc (:url "https://github.com/jojojames/fzfa" :rev :newest))
(use-package ivy
:ensure t
:bind (:map ivy-minibuffer-map
("M-x" . ivy-dispatching-done))
:config
(setq ivy-initial-inputs-alist nil)
(setq ivy-use-selectable-prompt t)
(define-key ivy-minibuffer-map (kbd "RET") 'ivy-alt-done)
(define-key ivy-minibuffer-map (kbd "C-j") 'ivy-done)
(define-key ivy-minibuffer-map (kbd "<C-return>") 'ivy-immediate-done)
(setq ivy-count-format "")
(setq ivy-height 9)
(ivy-mode))Helm support lives in fzfa-helm.el, loaded as part of
fzfa-extensions (which includes helm by default). Once loaded,
fzfa.el’s dispatch sites pick up the helm entry points via fboundp,
gated on helm-mode — no extra registration step.
helm.mov
When helm-mode is active, each fzfa entry point dispatches through
a handler that bypasses completing-read entirely and runs
(~helm :sources …)~ directly:
| Entry point | Handler |
|---|---|
fzfa-completing-read | fzfa-helm--completing-read |
fzfa-multi-read | fzfa-helm--multi-read |
fzfa-helm--completing-read is a single entry point covering all
three types of sources.
:command (shell pipeline) routes to fzfa-helm-make-async-source
over a process pipe.
:candidates as a static list or zero-arg fn routes to
fzfa-helm-make-sync-source with helm-pattern as the whole FILTER
(no CMD concept).
:candidates as a 2-arg (INPUT CALLBACK) producer fn also routes to
fzfa-helm-make-sync-source, but helm-pattern is split via
fzfa--split-input into (CMD . FILTER) — CMD goes to the producer’s
INPUT slot (refire only when CMD changes), FILTER scores the snapshot
via fzf-native-score-all. Producer-kind detection (sync- vs async-firing)
runs once at construction via a test fire with empty input;
async callbacks update a closure-scoped snapshot and schedule
helm-force-update. The :display kwarg (hidden / compact / full)
controls how the CMD half is rendered in the minibuffer;
fzfa-display-key (default >) cycles between states.
Sync sources are built with :match-dynamic t (helm asks for
filtered candidates on each input change rather than holding its own
filtered list) and :nohighlight t (so helm doesn’t paint over the
completions-common-part faces already embedded by fzf-native).
For :command sources a polling timer checks
fzf-native-async-generation and calls helm-force-update as new
candidates stream in. The multi handler uses a single shared timer
that watches every source’s handle, throttled to one
helm-force-update per fzfa-input-throttle, and skipped when
input-pending-p — keystrokes always win against producer-driven
refreshes.
| Variable | Default | Description |
|---|---|---|
fzfa-helm-candidate-limit | 2000 | Per-source cap for SINGLE-source helm paths. Default is fzfa-helm-multi-source-candidate-limit × 10. |
fzfa-helm-multi-source-candidate-limit | 200 | Per-source cap for MULTI helm paths. Helm renders every candidate eagerly; tighter than fzfa-max-candidates. |
fzfa-helm-kill-buffer-on-exit | t | Kill the helm session buffer on exit so it doesn’t linger in buffer-list and surface as a candidate in fzfa-buffer-backed pickers downstream. Set nil to defer to helm’s default (bury). |
Use fzfa-helm-source-from-command to lift an existing
fzfa-* command into a helm source — useful when you want to extend
a helm-mini-style picker with fzf-native-scored sources mixed in
with built-in helm sources (helm-source-buffers-list,
helm-source-bookmarks, …). The function returns a LIST (one
source for single-source commands, N sources for multi-source
commands like fzfa-find-any), so callers can compose with append:
Example below:
(defun my/helm-mini ()
(interactive)
(require 'helm-x-files)
(unless helm-source-buffers-list
(setq helm-source-buffers-list
(helm-make-source "Buffers" 'helm-source-buffers)))
(helm :sources
(append helm-mini-default-sources
(fzfa-helm-source-from-command 'fzfa-find)
;; Multi-source fzfa command — expands into one
;; helm source per inner vc command (modified-locally,
;; added-files, staged-for-commit, modified-in-head).
(fzfa-helm-source-from-command 'fzfa-vc-any))
:buffer "*helm mini+fzfa*"))The selection is routed back to the original command via fzfa’s
:inject mode so each command’s post-action (find-file, grep jump,
history push) still fires.
If every source in your picker is itself an fzfa-* command,
fzfa-multi-read is the better tool: one definition works across
helm / ivy / vertico / icomplete from a single call, and under
helm-mode it dispatches through fzfa-helm--multi-read with the
shared polling timer and per-source caps. Reach for
fzfa-helm-source-from-command only when you specifically want fzfa
sources mixed with non-~fzfa~ helm sources (e.g. adding to helm-mini).
To get a consistent sorting/filtering experience in helm where each helm
source matches using a consistent algorithm (e.g. fzf-native),
use fussy for non-~fzfa~ sources and fzfa otherwise. Example below.
(use-package fussy
:config
(fussy-setup-fzf)
(fussy-eglot-setup)
(fussy-company-setup))
(use-package fzfa
:vc (:url "https://github.com/jojojames/fzfa" :rev :newest))
(use-package helm
:ensure t
:init
(helm-mode)
:config
(setq helm-completion-style 'emacs) ;; Needed for `fussy'.
(setq helm-input-idle-delay 0.05)
(setq helm-autoresize-max-height 30)
(setq helm-autoresize-min-height 30)
(setq helm-truncate-lines t)
(setq helm-split-window-inside-p t
helm-ff-file-name-history-use-recentf t)
(setq helm-move-to-line-cycle-in-source nil)
(helm-autoresize-mode 1)
(setq helm-candidate-number-limit 900)
(set-face-attribute 'helm-source-header nil :height 1.0)
(setq helm-display-header-line nil)
(setq helm-mini-default-sources
'(helm-source-bookmarks
helm-source-buffers-list
helm-source-recentf
helm-source-buffer-not-found)))
(defun my/helm-mini ()
(interactive)
(require 'helm-x-files)
(unless helm-source-buffers-list
(setq helm-source-buffers-list
(helm-make-source "Buffers" 'helm-source-buffers)))
(helm :sources
(append helm-mini-default-sources
(fzfa-helm-source-from-command 'fzfa-find)
;; Multi-source fzfa command — expands into one
;; helm source per inner vc command (modified-locally,
;; added-files, staged-for-commit, modified-in-head).
(fzfa-helm-source-from-command 'fzfa-vc-any))
:buffer "*helm mini+fzfa*"))icomplete-vertical has a few defaults that may not work very well with fzfa.
I use this config when using fzfa with icomplete-vertical.
(use-package icomplete-vertical-mode
:ensure nil
:bind (:map icomplete-minibuffer-map
("RET" . icomplete-fido-ret)
("C-j" . icomplete-fido-exit)
("DEL" . icomplete-fido-backward-updir))
:init
(add-hook 'icomplete-minibuffer-setup-hook
(lambda () (setq-local truncate-lines t)))
(setq icomplete-hide-common-prefix nil)
(setq icomplete-tidy-shadowed-file-names t)
(setq icomplete-show-matches-on-no-input t)
(setq icomplete-compute-delay 0)
(setq icomplete-max-delay-chars 0)
(setq max-mini-window-height 10)
(setq resize-mini-windows t)
(setq completion-auto-help nil)
(setq icomplete-scroll t) ;; Don't wrap the results.
(setq enable-recursive-minibuffers t)
(setq completions-group t) ;; Show group headings.
(minibuffer-depth-indicate-mode 1)
(icomplete-vertical-mode))fido
(use-package icomplete
:ensure nil
:bind (:map icomplete-minibuffer-map
("RET" . icomplete-fido-ret)
("C-j" . icomplete-fido-exit)
("DEL" . icomplete-fido-backward-updir))
:init
(defun fussy-fido-setup ()
"Use `fussy' with `fido-mode'."
(setq-local completion-styles '(fussy basic)))
(advice-add 'icomplete--fido-mode-setup :after 'fussy-fido-setup)
(add-hook 'icomplete-minibuffer-setup-hook
(lambda () (setq-local truncate-lines t)))
(setq icomplete-hide-common-prefix nil)
(setq icomplete-tidy-shadowed-file-names t)
(setq icomplete-show-matches-on-no-input t)
(setq icomplete-compute-delay 0)
(setq icomplete-max-delay-chars 0)
(setq max-mini-window-height 10)
(setq resize-mini-windows t)
(setq completion-auto-help nil)
(setq icomplete-scroll t) ;; Don't wrap the results.
(setq enable-recursive-minibuffers t)
(setq completions-group t) ;; Show group headings.
(minibuffer-depth-indicate-mode 1)
(fido-vertical-mode))A matcha transient
is defined for invoking all fzfa commands from a single keybinding
via matcha.
Persistent actions (helm) and ivy-call semantics are integrated.
e.g. Executing an action on the candidate without exiting completing-read.
The binding depends on your completion frontend. fzfa just plugs into
whatever your frontend already uses for this concept:
| Frontend | Key | Mechanism |
|---|---|---|
ivy / counsel | C-M-m | ivy-call (ivy default) |
helm | C-j | helm-execute-persistent-action |
vertico / icomplete | C-M-m | fzfa-apply-key (configurable defcustom) |
| Variable | Default | Description |
|---|---|---|
fzfa-max-candidates | 10000 | Max candidates returned to Elisp (see note below). |
fzfa-refresh-delay | 0.05 | Seconds between generation polls. |
fzfa-input-debounce | 0.1 | Idle seconds to retry after an interrupted scoring. |
fzfa-input-throttle | 0.2 | Min seconds between UI refreshes driven by new data. |
fzfa-preview-delay | 0.8 | Idle seconds before live-preview fires (nil = disabled; see note below). |
fzfa-preview-functions | (buffer, file, grep, …) | Category → preview handler plist (see note below). |
fzfa-preview-file-size-limit | 1 MiB | Skip file preview when the file exceeds this size (nil/0 disables; see note). |
fzfa-directory | nil | Per-call directory override; supersedes project backend (see note below). |
fzfa-project-backend | project | How to resolve the root directory (see note below). |
fzfa-highlight | 200 | C-side match highlighting; nil/t/N (see note below). |
fzfa-max-line-length | 256 | Per-line character limit; nil/+N/-N (see note below). |
fzfa-cache-size | 40 | Per-session LRU cache entries (see note below). |
fzfa-case-mode | smart | Case sensitivity: smart / ignore / respect. |
fzfa-fuzzy | t | Fuzzy match (t) or exact/substring match (nil). |
fzfa-multi-narrow-key | < | Prefix key for narrowing in multi-source sessions; nil disables. |
fzfa-extensions | (see Extensions below) | Extensions to enable. |
fzfa-preview-delay controls live preview for commands that act on the
highlighted candidate as the selection moves (e.g. fzfa-theme loading
themes, fzfa-grep jumping to the matching line in a side window).
Scheduling uses run-with-idle-timer, so fast typing or arrow-key bursts
naturally suppress intermediate previews — the timer only fires once input
settles, and at fire time the current candidate is re-read (never a stale
one captured at schedule time). The default may be conservative:
It can be lowered for faster previews, set to 0 for immediate preview or
set to nil to disable the preview.
fzfa-preview-functions maps a completion category to a plist of
lifecycle handlers:
| Slot | Signature | When |
|---|---|---|
:setup | () | Once at minibuffer entry |
:preview | (CAND) | Each debounced candidate change (required) |
:exit | () | Just before minibuffer closes (still live) |
:return | (CAND-OR-NIL) | After minibuffer closes; nil = aborted |
Handlers share state across the lifecycle via fzfa-preview-get /
fzfa-preview-put (a plist scoped to the active preview session).
Only :preview is required. Most commands wire up preview
automatically by declaring a :category that’s in this alist — no
per-command code needed. Built-in defaults:
| Category | Behavior |
|---|---|
fzfa-buffer | Show the buffer in a side window without stealing focus. |
fzfa-file | Open the file in a side window; kill on abort if not previously loaded (see note). |
fzfa-grep | Parse FILE:LINE:CONTENT, open FILE at LINE in a side window. |
For previews, fzfa-file tracks which buffers it newly created vs.
which were already loaded. On abort, only the newly-created ones
are killed — buffers you had open before invoking the picker are
untouched.
On accept, the chosen file’s buffer is promoted (not killed) so the
caller’s find-file reuses it instead of re-reading from disk.
Files larger than fzfa-preview-file-size-limit (default 1 MiB)
are skipped silently so that landing on a big binary doesn’t stall
selection movement.
Categories whose candidates carry auxiliary state (e.g. an in-memory
lookup hash table — fzfa-imenu, fzfa-evil-mark, fzfa-evil-jump,
fzfa-flymake) can’t be wired through the global registry alone
because the handler needs access to that closure-captured state.
Those commands pass a closure-bound :preview lambda directly (see
next paragraph) and so don’t appear in the registry default.
For one-off previews that don’t warrant a shared registry entry
(e.g. fzfa-theme for theme snapshot/restore, fzfa-bookmark which
reuses the file opener for file-targeting bookmarks), pass the
handler directly via the :preview keyword to
fzfa-completing-read. It accepts a function (shorthand for a
`:preview’-only handler) or a full handler plist, and takes
precedence over the registry lookup.
To customize placement (side window, popup frame, fresh split),
configure display-buffer-alist at the Emacs level keyed on the
buffer/mode you want to redirect. That rule will apply uniformly
to both preview and the post-selection action, keeping them
together. Example:
;; All file buffers — preview AND selection — land in a right side window.
(add-to-list 'display-buffer-alist
'((lambda (buf _) (with-current-buffer buf buffer-file-name))
(display-buffer-in-side-window)
(side . right) (window-width . 0.5)))fzfa-highlight controls C-side match highlighting. After the scoring
pass, the C module calls fzf_get_positions for the top N candidates and
applies the completions-common-part face to each contiguous run of matched
characters via put-text-property. This happens entirely inside the C module
before strings are handed to Emacs, so there is no Elisp regex overhead.
The defcustom accepts three forms:
| Value | Behavior |
|---|---|
nil | No highlighting. |
t | Highlight every returned candidate. |
| N | Highlight the top N candidates (default 200). |
Setting a cap rather than always highlighting all candidates is intentional:
fzf_get_positions is cheap but not free, and users cannot see more than
~20–50 candidates without scrolling. 200 provides comfortable headroom.
(setq fzfa-highlight nil) ; disable entirely
(setq fzfa-highlight t) ; highlight all returned candidates
(setq fzfa-highlight 500) ; highlight top 500fzfa-max-line-length filters lines from the subprocess before they
enter the candidate pool. Minified JavaScript, base64 payloads, and other
pathologically long lines slow scoring and produce unreadable candidates.
Lowering this number can be a huge performance improvement.
| Value | Behavior |
|---|---|
nil | No limit — every line is accepted unchanged. |
+N | Exclude lines longer than N characters (default 256). |
-N | Include but truncate to N characters. |
The check fires in the reader thread immediately after ANSI stripping, before any allocation, so oversized lines never reach the scoring path.
(setq fzfa-max-line-length nil) ; no limit
(setq fzfa-max-line-length 300) ; exclude lines > 300 chars
(setq fzfa-max-line-length -300) ; truncate to 300 chars, keep allfzfa-max-candidates caps only the number of strings consed into the
Emacs list returned to the completion UI. The C layer always scores and
sorts all candidates: every matching string is passed through the fzf
scoring threads, the full scored set is counting-sorted, and then only the
top N are handed back to Elisp. The [FILTERED] count in the prompt
always reflects the true number of matches, not the capped return value.
Lowering this number can be a huge performance improvement.
fzfa-cache-size controls a per-session LRU result cache inside the
C module. Each entry stores the top-K results and the full matched-
candidate index for one query. Three lookup outcomes:
| Outcome | When | Effect |
|---|---|---|
| Exact-fresh | Same query, pool unchanged | Return cached results; no scoring scheduled. |
| Exact-stale | Same query, more candidates streamed in since | Return cached top-K immediately, refine in BG. |
| Prefix | New query is a refinement of a cached one | Return prefix’s top-K, refine on prior matches. |
Refinement scoring scans only the prior match set + delta candidates
instead of the full pool — for typing past the first 2-3 chars this
typically drops scan size by 100-1000×. Subsumption uses both
byte-prefix matching (fo → foo, fo → fo bar) and term-set
comparison (fo → x fo, fo bar → bar foo). OR queries
(containing |) are excluded — adding an OR alternate widens the
match set unpredictably.
A larger cache keeps a longer typing trail in LRU. Helps backspace: backing up several keystrokes still hits cached entries as long as those intermediate queries weren’t evicted by unrelated lookups.
Read once at session start; changing it does not affect running sessions.
This is done because anything interfacing with Emacs itself is easily the slowest part of the algorithm. Even converting C strings to Emacs strings can be a burden when the total collection size is millions of candidates. In practice, the cap should not be an issue (and is configurable anyways) since it’s returning the top N candidates at any one time.
fzfa-case-mode controls how the fzf scorer treats letter case.
Read on every scoring call; changes take effect immediately.
| Value | Behavior |
|---|---|
smart | Case-insensitive when the query is all lowercase; case-sensitive once it contains any uppercase character (fzf’s default). |
ignore | Always case-insensitive. |
respect | Always case-sensitive. |
(setq fzfa-case-mode 'smart) ; default
(setq fzfa-case-mode 'ignore)
(setq fzfa-case-mode 'respect)fzfa-fuzzy controls whether queries are matched fuzzily or as
exact substrings. Read at the start of every scoring
call; let-bind it dynamically to flip the matcher per-call.
| Value | Behavior |
|---|---|
t | Fuzzy matching (default). Prefix a term with ' to force exact match. |
nil | Exact/substring matching. Prefix a term with ' to force fuzzy match. |
Operator filters (!, ^, $, |, space-separated AND terms) work
identically regardless of this setting — fzfa-fuzzy only changes the
default matcher used for bare terms.
(setq fzfa-fuzzy t) ; default
(setq fzfa-fuzzy nil) ; exact/substring matchingfzfa-directory is a let-bindable override that takes priority over
project detection. Use it when extending a built-in command that you want
to run in the current directory rather than the project root:
(defun my-rg-here ()
(interactive)
(let ((fzfa-directory default-directory))
(fzfa-rg)))The full priority chain is:
fzfa-directory > fzfa-project-backend > default-directory
fzfa-project-backend controls which directory file-search and grep
commands run in. The default project matches the behavior of consult.
Available values:
| Value | Behavior |
|---|---|
project | Uses project.el (project-current / project-root). Default. |
projectile | Uses projectile-project-root when projectile-mode is active. |
nil | Uses default-directory unchanged (no project detection). |
| function | Calls the function with no arguments; it should return a directory. |
Example with a custom function:
(setq fzfa-project-backend
(lambda () (locate-dominating-file default-directory ".git")))The prompt overlay shows live status during a search:
DIR IDX/[FILTERED](TOTAL)
DIR— abbreviated working directoryIDX— current selection index (vertico,ivy)FILTERED— candidates passing the currentfzfqueryTOTAL— total candidates collected from the shell command so far
fzfa is designed to be extensible. New commands are thin wrappers
around fzfa-completing-read. A session has exactly one source —
either :command (shell pipeline, streamed) or :candidates (Elisp
side, list / zero-arg fn / 2-arg producer fn). The two are mutually
exclusive.
| Argument | Default | Description |
|---|---|---|
:prompt | derived from source | Minibuffer prompt string. |
:command | — | Shell command whose stdout becomes candidates (mutually exclusive with :candidates). |
:candidates | — | Static list, zero-arg fn returning a list, or 2-arg (INPUT CALLBACK) producer fn (mutually exclusive with :command). |
:directory | default-directory | Working directory for :command sources. |
:category | 'auto | completion-category-overrides key. 'auto resolves to fzfa-file for :command, fzfa-misc for :candidates. |
:require-match | 'auto | Passed to completing-read. 'auto resolves to nil for :command (free-form input), t for :candidates. |
:resolve-paths | 'auto | If non-nil, expand-file-name the result against :directory. 'auto resolves to t for :command, nil for :candidates. |
:default | nil | Initial DEFAULT for completing-read. |
:history | nil | History variable; recency boosts empty-FILTER ranking under ivy. |
:annotate | nil | (CAND) -> STRING function attached as completion metadata annotation-function. |
:affix | nil | (CAND) -> (PREFIX CAND SUFFIX) function for the metadata affixation-function. |
:group | nil | group-function for candidate grouping in the metadata. |
:preview | registry lookup by :category | Function or handler plist; takes precedence over the global fzfa-preview-handlers registry. |
:apply | registry lookup by :category | Custom action for the selected candidate. |
:display | hidden | Initial display state for the #COMMAND#FILTER split (hidden / compact / full). |
:skip-executable-check | nil | Skip the built-in executable-find guard on the first token of :command. |
The 'auto sentinel on :category, :require-match, and
:resolve-paths keys the default off whichever of :command /
:candidates is set. Pass an explicit value (nil, t, a category
symbol) to override.
For :command sources the string is passed verbatim to
shell-file-name (-c), so pipes, redirections, and shell quoting
all work as expected. fzfa-completing-read automatically checks
that the first token of :command is present in exec-path (via
executable-find) before starting the session. Pass
:skip-executable-check t when the command uses a shell builtin,
alias, or has already been validated by the caller.
fzfa-shell-command skips the check because the user’s shell
resolves aliases and builtins that executable-find cannot see.
For :candidates sources the picker splits its minibuffer input via
fzfa-separator into (CMD . FILTER) when the producer is a 2-arg
fn: CMD is passed as the producer’s INPUT slot (refire only when CMD
changes), FILTER scores the snapshot via fzf-native-score-all.
Static lists and zero-arg fns skip the split — the whole input is
the FILTER. Producer-kind detection runs once at session setup; a
sync-firing producer (callback returns before the producer fn does)
runs every CMD change inline, while an async-firing producer
(jsonrpc, url-retrieve) updates a closure-scoped snapshot and the
substrate re-renders on callback arrival. Async producers are not
supported under helm-mode — helm’s candidates contract has no
notion of deferred delivery.
For one-off queries, fzfa-shell-command prompts for a command
interactively and runs it in default-directory;
fzfa-shell-project-command does the same from the project root.
;;;###autoload
(defun fzfa-spotlight-pdfs ()
"Find a PDF file system-wide using Spotlight.
Opens the selected PDF with `open'."
(interactive)
(when-let* ((result (fzfa-completing-read
:prompt "spotlight pdfs: "
:command "mdfind 'kMDItemFSName == \"*.pdf\"'"
:directory default-directory)))
(start-process "default-app" nil "open" result)))A :candidates example — a zero-arg fn collecting buffer names:
;;;###autoload
(defun my/fzfa-non-file-buffers ()
"Pick a buffer that isn't visiting a file."
(interactive)
(when-let* ((name (fzfa-completing-read
:prompt "non-file buffer: "
:candidates (lambda ()
(cl-remove-if #'buffer-file-name
(buffer-list)
:key #'identity))
:history 'my/fzfa-non-file-buffer-history)))
(switch-to-buffer name)))A 2-arg producer example refires per CMD-half change. See
fzfa-regexp.el (sync-firing) and fzfa-eglot.el (async-firing,
jsonrpc-async-request) for in-tree consumers.
fzfa | affe | helm | counsel | consult | fzf.el | |
|---|---|---|---|---|---|---|
| Frontends supported | vertico/icomplete/helm/ivy | vertico/icomplete? | helm only | ivy only | vertico/icomplete | own UI |
| Smart fuzzy scoring | ✓ (fzf-native) | ✓ (emacs31 flex) | ✓ (emacs31 flex or flx) | flx | (emacs31 flex) | ✓ (binary) |
| Multi-component query | ✓ (fzfa) | ✓ (orderless) | ✓ (substring AND) | ✓ (regex AND) | ✓ (orderless) | ✓ (fzf) |
| Producer runs once | ✓** | ✓ | × | × | ×* | ✓ |
| Async multi-source | ✓ | × | ✓ | × | sync only | × |
| Scales to large repos/sources | ✓ | × | × | × | × | ✓ |
Integrates with completion-styles | ✓* | ✓ | ✓* | × | ✓ | × |
| Language | Elisp + C | Elisp | Elisp | Elisp | Elisp | Elisp + Go |
- There may be some fuzzy matching packages that try to interop with
helmorcounselthough it’s hard to say how performant they are. E.g.helm-fuzwhich bringsfuztohelm, orflxwhich is one of the original fuzzy matching packages for Emacs. flxis smart fuzzy matching, but slow and doesn’t scale to large codebases at all.affe,helm, andconsultcan take advantage ofcompletion-stylesand leverage Emacs 31’s Cfleximplementation using Gotoh’s algorithm. From my short testing, the C implementation doesn’t scale very well to large codebases, being single-threaded.fzfaandfzf.elwere the only packages capable of handling millions (10+~) of candidates without degraded performance. Caveat: Try the packages yourself to come to your own conclusion.- Multicomponent queries exist with these packages with varying
functionality. For example, using multicomponent matching is
possible with
orderless, but addingflexfiltering to the matching will slow Emacs to a crawl. If you useemacs31 flexinstead, you lose the multi component matching. fzfaintegrates withcompletion-stylesthoughfzfadoes it by taking over the variable (ignoring the other values in the list).helmintegrates withcompletion-stylesthrough a separate defcustom that doesn’t apply to every source.fzfa’s producer runs once for the default session, but it can be re-spawned mid-session when the user opts in to editing the underlying shell command — see *Editing the shell command mid-session.
(Best-effort matrix; corrections welcome.)
How the other packages handle the smart-fuzzy-vs-filter dimension covered in Smart fuzzy matching:
orderlesstreats query components as substring / literal / regex /flexpredicates. Itsflexdispatcher is the only mainstream pure-Elisp fuzzy implementation but is too slow for large candidate sets. It does integrate with other Emacs sorting like history/recency.affefilters viaall-completions+ optionalorderlessregex throughcompletion-styles. Pure filter; no per-character scoring; ranking is whateverall-completionsreturns.helm-sources usehelm’s multi-pattern matcher (substring or regex). A few external packages try to hook smart fuzzy matching intohelmbut with the downsides listed above.hotfuzzis written in C and pluggable tocompletion-styles.
- ~helm~ has native multi-source (helm-mini-style: stream from N producer processes concurrently, each filtered/scored live).
- vertico/consult has
consult--multi, which can mix sync and async sources (the dispatcher upgrades to an async pipeline when any source declares:async). All core commands using it (consult-bufferand variants) are sync only; Third party packages are examples. (e.g.consult-omni) - ivy/counsel has no multi-source primitive.
counsel-buffer-or-recentfand friends concatenate two lists into a flat collection — no per-source narrowing, filtering, or preview.ivy-readignores thegroup-functioncompletion- metadata key.fzfa-ivyadds genuine multi-source functionality via a display transformer (source label prefix) + aivy-dispatching-call-based narrow menu usingivy--actions-list.
consult and vertico integrate the most with base Emacs
(plain completing-read), but a consult function generally can’t
run in helm’s UI or vice versa. That’s why each frontend’s
ecosystem duplicates the same async-* surface — there’s a
helm-rg, a counsel-rg, and a consult-ripgrep.
fzfa commands run unchanged under all three. Each frontend gets a
dedicated extension (fzfa-helm.el, fzfa-ivy.el, fzfa-vertico.el)
that wires the same producer + scorer pair into its native UI
plumbing. Using helm as an example, fzfa can provide “sources”
that helm can read (e.g. with helm-mini), and fzfa-* commands
themselves render in helm’s UI when helm-mode is on.
- =helm-= (helm-rg, helm-find-files, etc.): mature, comprehensive
command set, native multi-source. Its multi-source functionality
can mix both async and sync sources together!
For async sources, it re-shells the command per keystroke so isn’t scalable. Feels slightly sluggish compared to alternatives even with things tuned down. Probably the most feature rich of the bunch with the most third-party extensions. It doesn’t use built-in Emacs APIs so can feel like you’re using ”
helm” instead of using “emacs”. Can be a little inconsistent in experience as its sources can all use different completion algorithms which is good or bad depending on who you ask. - =ivy/counsel-= (counsel-rg, counsel-fzf, etc.): also mature, comprehensive
counsel-fzfis a good illustrative example. It has to re-shell on input which means large datasets will feel sluggish to impossible to filter/search through. (PR #1151 for context) Since it doesn’t heavily integrate with the base Emacs packages (e.g.completion-styles) and development has slowed, it presents a tricky value proposition at this point. From my testing, the most typing responsive of the bunch (no benchmarking). - =vertico/consult-=: newest of the bunch, but mature/comprehensive
at this point. The various
consult-*integrates heavily with Emacs base packages so it’s probably the best in terms of a future-looking experience. Theall-completionsandcompletion-regexp-listapproach it takes to filtering means it “just works” with most packages as well asorderlessbut that’s the same reason it’s not yet? compatible withfzf-style matching, which mostly wants a clean/empty prefix to match against. It gets around this somewhat using a 2-pass approach (e.g.consult-rg), where you can set a specific ripgrep shell command to run and a separate query to filter. - affe: closest architectural
cousin to
fzfa— background producer + async filter. Matching can integrate withorderlessor take advantage of any of the Emacs plumbing. Filtering is single-threaded Elisp; plateaus around 1-2M candidates whilefzf-nativechews 10-20M in the same wall time (again, eye test). Strict pure-Elisp win compared tofzfa’s native dep.fzfatook the most learning from this project and its background producer + filter flow is whatfzfadoes. It doesn’t do smart fuzzy matching, and it’s still relatively “slow” (in the context of fuzzy matching, not prefix matching) compared to newer alternatives (e.g.fff,fzf). - fzf.el: shells out to the
fzfcommand-line binary as its UI. Doesn’t participate in Emacs’s completion machinery (nocompleting-readintegration, noembark, no marginalia, no category dispatch). Works if you want fzf-the-tool inside a buffer. It’s a great option if you work in a large repo and don’t want the C binary requirementfzfaimposes and also don’t care about how integrated it is with other Emacs packages.
For the Elisp pipeline this package owns — async and sync
completing-read entry points, custom completion style, frontend
abstraction, timer model, prompt overlay — see
architecture.org.
The C scoring module under fzfa is documented separately in
fzf-native’s architecture overview
(thread/lock model, AsyncSession, arena allocator, counting sort,
score_abort rule).
For fussy, the broader fuzzy completion-style framework that
fzfa deliberately bypasses, see
fussy’s architecture overview.
