Skip to content

jojojames/fzfa

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

348 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

fzfa

https://github.com/jojojames/fzfa/actions/workflows/test.yaml/badge.svg

./screenshots/fzfa.png

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.

Motivation

Design goals that distinguish fzfa from the existing completion packages like helm, counsel, and consult:

Single-stage filtering — one query, one filter

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-multi compiles input to a (potentially non-fuzzy) regexp via consult--compile-regexp, scans buffers; surviving lines are re-filtered by the active completion-style.
  • counsel-ag / counsel-rg / counsel-git-grep (counsel.el:3246 et al.) transform input via ivy--regex, run the shell tool, then ivy’s active matcher (e.g. flx, ivy--regex-plus, fuzzy, …) re-filters the results.
  • helm-grep-ag (helm-grep.el:1641) feeds helm-pattern to ag directly; 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:

  1. 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.
  2. uses fzf-native under the hood, so is fast enough to do this without incurring sluggish typing performance.
  3. allows the user to edit the SHELL-CMD fully — see *Editing the shell command mid-session below.
  4. applies this approach to every fzfa-* command so the pipeline incongruence is avoided.
  5. each command runs with an empty query to gather candidates, so they can be easily composed into a bigger multi-source command

Smart fuzzy matching

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.

Multi-component query syntax

fzf syntax is supported:

TokenMatch typeDescription
sbtrktfuzzy-matchItems that match sbtrkt
'wildexact-match (quoted)Items that include wild
^musicprefix-exact-matchItems that start with music
.mp3$suffix-exact-matchItems that end with .mp3
!fireinverse-exact-matchItems that do not include fire
!^musicinverse-prefix-exact-matchItems that do not start with music
!.mp3$inverse-suffix-exact-matchItems 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

Performance / Speed

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.

Cross-frontend support

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.

Mixed async + sync multi-source

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.

Installation

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.

Quick Start

;; 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-files

Commands

All 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.

File finders

fzfacounselconsulthelm
fzfa-findcounsel-find-fileconsult-findhelm-find-files
fzfa-fdcounsel-fdconsult-fdhelm-find-files
fzfa-rg-filesconsult-findhelm-find-files
fzfa-ag-files
fzfa-git-ls-filescounsel-githelm-ls-git
fzfa-hg-files
fzfa-recent-filecounsel-recentfconsult-recent-filehelm-recentf
fzfa-locatecounsel-locateconsult-locatehelm-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.)

Emacs based

fzfacounselconsulthelm
fzfa-bufferivy-switch-bufferconsult-bufferhelm-buffers-list
fzfa-bookmarkcounsel-bookmarkconsult-bookmarkhelm-filtered-bookmarks
fzfa-M-xcounsel-M-xhelm-M-x
fzfa-M-x-for-bufferconsult-mode-command
fzfa-minor-mode-menucounsel-minorconsult-minor-mode-menu
fzfa-yank-popcounsel-yank-popconsult-yank-pophelm-show-kill-ring
fzfa-themecounsel-load-themeconsult-themehelm-themes
fzfa-trampcounsel-trampconsult-tramphelm-tramp
fzfa-imenucounsel-imenuconsult-imenuhelm-imenu
fzfa-imenu-allconsult-imenu-multihelm-imenu-in-all-buffers
fzfa-imenu-all-but-current
fzfa-eglot-symbolsconsult-eglot-symbolshelm-lsp-workspace-symbol
fzfa-outlinecounsel-outlineconsult-outlinehelm-outline
fzfa-org-headingcounsel-org-gotoconsult-org-headinghelm-org-in-buffer-headings
fzfa-org-heading-allcounsel-org-goto-allconsult-org-heading (with prefix)helm-org-agenda-files-headings
fzfa-org-agendaconsult-org-agendahelm-org-agenda-files-headings
fzfa-org-todocounsel-org-todo (related)
fzfa-org-tags-viewhelm-org-tags
fzfa-org-insert-linkcounsel-org-link
fzfa-org-any
fzfa-markcounsel-mark-ringconsult-markhelm-mark-ring
fzfa-global-markcounsel-mark-ringconsult-global-markhelm-global-mark-ring
fzfa-registercounsel-registerconsult-register-loadhelm-register
fzfa-flymakeconsult-flymakehelm-flymake
fzfa-flymake-projectconsult-flymake (with prefix arg)
fzfa-compile-errorcounsel-compilation-errorsconsult-compile-error
fzfa-shell-historycounsel-shell-history / counsel-esh-historyconsult-historyhelm-eshell-history
fzfa-project-find-filecounsel-projectile-find-filehelm-projectile-find-file
fzfa-project-find-dircounsel-projectile-find-dirhelm-projectile-find-dir
fzfa-project-buffercounsel-projectile-switch-to-bufferconsult-project-bufferhelm-projectile-switch-to-buffer
fzfa-project-recentfcounsel-projectile-recentfhelm-projectile-recentf
fzfa-project-switch-projectcounsel-projectile-switch-projecthelm-projectile-switch-project

Content search

Grep-style commands parse FILE:LINE:CONTENT output and jump directly to the matching line.

fzfacounsel / ivyconsulthelm
fzfa-rgcounsel-rgconsult-ripgrephelm-rg
fzfa-agcounsel-aghelm-do-ag
fzfa-git-grepcounsel-git-grepconsult-git-grephelm-grep-do-git-grep
fzfa-git-log-grepcounsel-git-logconsult-git-log-grep
fzfa-grepcounsel-grepconsult-grephelm-do-grep-ag (any backend)
fzfa-grep-current-filecounsel-grepconsult-line
fzfa-ugrep
fzfa-swiperswiperconsult-linehelm-occur
fzfa-swiper-allswiper-allconsult-line-multihelm-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.

Hungry variants

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.

CommandToolWhat it searches
fzfa-hungry-findfd / findFiles under all derived directories
fzfa-hungry-swiperrg / grepLine content under all derived dirs

Multi-source

fzfacounselconsulthelm
fzfa-find-anycounsel-buffer-or-recentfconsult-bufferhelm-mini / helm-multi-files
fzfa-find-somecounsel-buffer-or-recentfconsult-bufferhelm-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).

Narrowing to a single source

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 -+
StateMenu visible?Prompt overlayMinibuffer input
1 WidenednoPROMPT [N/](T)query against all
2 Narrow menuyesKEY:NAME ... <:widen(captured by handler)
3 NarrowednoPROMPT {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:

KeyActionResult
source letterNarrow or switch to that source-> state 3
fzfa-multi-narrow-keyWiden — restore all sources-> state 1
anything elseCancel — 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:

  1. Any explicit :narrow in the command list is reserved first.
  2. 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:

SourceKey
fzfa-imenui
fzfa-bufferb
fzfa-recent-filer
fzfa-hungry-findh
fzfa-imenu-all-but-currenta
fzfa-M-xM
fzfa-hungry-swipers
fzfa-locatel

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

Mixing async and sync sources with fzfa-multi-read

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:

FrontendMixed 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 / counselno multi-source mechanism

Extensions

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.

SymbolLibrarySoft dependencyCommands
chromefzfa-chromepython3 (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
companyfzfa-companycompanyfzfa-company — fuzzy-filter current company-mode candidates and finish
eglotfzfa-egloteglotfzfa-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
embarkfzfa-embarkembarkSub-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.
evilfzfa-evilevilfzfa-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)
firefoxfzfa-firefoxsqlite3 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)
flymakefzfa-flymakenone (built-in)fzfa-flymake (current buffer), fzfa-flymake-project (all buffers in the current project)
infofzfa-infonone (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
mailfzfa-mailmacOS Mail.appfzfa-mail, fzfa-mail-refresh — browse and open inbox messages
makefzfa-makemake / ninja (CLI)fzfa-make (locate Makefile~/~build.ninja and call make)
musicfzfa-musicmacOS Music.appfzfa-music, -by-artist, -by-genre, -playlist, -playlist-shuffle, -refresh
notmuchfzfa-notmuchnotmuch (CLI + Emacs package)fzfa-notmuch, fzfa-notmuch-tree — run a notmuch query and fuzzy-pick a thread to open
orgfzfa-orgnone (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.
passfzfa-passpassword-storefzfa-pass (copy), fzfa-pass-edit, -rename, -delete, -add, -generate, -url
projectfzfa-projectnone (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
regexpfzfa-regexpnone (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
safarifzfa-safarimacOS 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)
spotlightfzfa-spotlightmacOS mdfindfzfa-spotlight, fzfa-spotlight-apps, fzfa-spotlight-audio — application launcher
vcfzfa-vcnone (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:

Opt-in extensions and fzfa-sync-autoloads

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.

Editing the shell command mid-session

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

Smart dispatch

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.

Completion style compatibility

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.)

Companion package — fussy

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))

Integrations

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.

FrontendStatusNotes
verticoSupportedRecommended. Generation-based refresh via vertico--exhibit.
fidoSupportedBuilt on icomplete; works without extra configuration.
ivy/counselSupportedPush model via ivy--set-candidates. See below.
helmSupportedDedicated handler path with stats / live preview / annotations / narrow-by-source.
icompleteSupportedRefreshes via icomplete-exhibit.

Caveat: I mostly use vertico these days so wasn’t exhaustive with using the other completion systems.

vertico

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:

KeyAction
M-lNext source in reading order (wraps at band edge)
M-hPrevious source
M-jNext band (same column-in-band; whole-band jump)
M-kPrevious 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).

Customization

VariableDefaultDescription
fzfa-vertico-columns-max3Maximum number of columns rendered per band; overflow wraps into additional bands.
fzfa-vertico-columns-page-size6Maximum sources displayed simultaneously; pagination kicks in beyond.
fzfa-vertico-columns-min-width12Minimum width per column, in characters.
fzfa-vertico-columns-max-width60Maximum width per column, in characters.
fzfa-vertico-columns-separator” | ”String inserted between columns.
fzfa-vertico-columns-headerstWhen non-nil, render group names as a header row above each column.
fzfa-vertico-columns-header-facefzfa-vertico-columns-headerFace applied to the column header row.
fzfa-vertico-columns-autotWhen 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-truncateautoHow 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-sortscoredColumn order: scored (top-fzf-score first), declared, alphabetical, or a function.

ivy / counsel

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 pointIvy 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).

Multi source disambiguation

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).

Multi source narrowing — ivy actions

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.

Restrict to matches (S-SPC)

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-setupfussy’s matcher overrides ivy’s regex filter and keeps the fuzzy-match feel after S-SPC. Setup example below.

Full setup example

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

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 pointHandler
fzfa-completing-readfzfa-helm--completing-read
fzfa-multi-readfzfa-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.

Customization

VariableDefaultDescription
fzfa-helm-candidate-limit2000Per-source cap for SINGLE-source helm paths. Default is fzfa-helm-multi-source-candidate-limit × 10.
fzfa-helm-multi-source-candidate-limit200Per-source cap for MULTI helm paths. Helm renders every candidate eagerly; tighter than fzfa-max-candidates.
fzfa-helm-kill-buffer-on-exittKill 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).

Adding fzfa sources to a helm command

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.

Frontend-portable multi via fzfa-multi-read

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).

Full setup example

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 / fido

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))

transient

A matcha transient is defined for invoking all fzfa commands from a single keybinding via matcha.

Persistent action

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:

FrontendKeyMechanism
ivy / counselC-M-mivy-call (ivy default)
helmC-jhelm-execute-persistent-action
vertico / icompleteC-M-mfzfa-apply-key (configurable defcustom)

Customization

VariableDefaultDescription
fzfa-max-candidates10000Max candidates returned to Elisp (see note below).
fzfa-refresh-delay0.05Seconds between generation polls.
fzfa-input-debounce0.1Idle seconds to retry after an interrupted scoring.
fzfa-input-throttle0.2Min seconds between UI refreshes driven by new data.
fzfa-preview-delay0.8Idle 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-limit1 MiBSkip file preview when the file exceeds this size (nil/0 disables; see note).
fzfa-directorynilPer-call directory override; supersedes project backend (see note below).
fzfa-project-backendprojectHow to resolve the root directory (see note below).
fzfa-highlight200C-side match highlighting; nil/t/N (see note below).
fzfa-max-line-length256Per-line character limit; nil/+N/-N (see note below).
fzfa-cache-size40Per-session LRU cache entries (see note below).
fzfa-case-modesmartCase sensitivity: smart / ignore / respect.
fzfa-fuzzytFuzzy 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:

SlotSignatureWhen
: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:

CategoryBehavior
fzfa-bufferShow the buffer in a side window without stealing focus.
fzfa-fileOpen the file in a side window; kill on abort if not previously loaded (see note).
fzfa-grepParse 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:

ValueBehavior
nilNo highlighting.
tHighlight every returned candidate.
NHighlight 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 500

fzfa-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.

ValueBehavior
nilNo limit — every line is accepted unchanged.
+NExclude lines longer than N characters (default 256).
-NInclude 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 all

fzfa-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:

OutcomeWhenEffect
Exact-freshSame query, pool unchangedReturn cached results; no scoring scheduled.
Exact-staleSame query, more candidates streamed in sinceReturn cached top-K immediately, refine in BG.
PrefixNew query is a refinement of a cached oneReturn 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 (fofoo, fofo bar) and term-set comparison (fox fo, fo barbar 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.

ValueBehavior
smartCase-insensitive when the query is all lowercase; case-sensitive once it contains any uppercase character (fzf’s default).
ignoreAlways case-insensitive.
respectAlways 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.

ValueBehavior
tFuzzy matching (default). Prefix a term with ' to force exact match.
nilExact/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 matching

fzfa-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:

ValueBehavior
projectUses project.el (project-current / project-root). Default.
projectileUses projectile-project-root when projectile-mode is active.
nilUses default-directory unchanged (no project detection).
functionCalls 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 directory
  • IDX — current selection index (vertico, ivy)
  • FILTERED — candidates passing the current fzf query
  • TOTAL — total candidates collected from the shell command so far

Writing your own command

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.

ArgumentDefaultDescription
:promptderived from sourceMinibuffer prompt string.
:commandShell command whose stdout becomes candidates (mutually exclusive with :candidates).
:candidatesStatic list, zero-arg fn returning a list, or 2-arg (INPUT CALLBACK) producer fn (mutually exclusive with :command).
:directorydefault-directoryWorking directory for :command sources.
:category'autocompletion-category-overrides key. 'auto resolves to fzfa-file for :command, fzfa-misc for :candidates.
:require-match'autoPassed to completing-read. 'auto resolves to nil for :command (free-form input), t for :candidates.
:resolve-paths'autoIf non-nil, expand-file-name the result against :directory. 'auto resolves to t for :command, nil for :candidates.
:defaultnilInitial DEFAULT for completing-read.
:historynilHistory variable; recency boosts empty-FILTER ranking under ivy.
:annotatenil(CAND) -> STRING function attached as completion metadata annotation-function.
:affixnil(CAND) -> (PREFIX CAND SUFFIX) function for the metadata affixation-function.
:groupnilgroup-function for candidate grouping in the metadata.
:previewregistry lookup by :categoryFunction or handler plist; takes precedence over the global fzfa-preview-handlers registry.
:applyregistry lookup by :categoryCustom action for the selected candidate.
:displayhiddenInitial display state for the #COMMAND#FILTER split (hidden / compact / full).
:skip-executable-checknilSkip 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-modehelm’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.

Comparison

fzfaaffehelmcounselconsultfzf.el
Frontends supportedvertico/icomplete/helm/ivyvertico/icomplete?helm onlyivy onlyvertico/icompleteown 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✓*✓*××
LanguageElisp + CElispElispElispElispElisp + Go
  • There may be some fuzzy matching packages that try to interop with helm or counsel though it’s hard to say how performant they are. E.g. helm-fuz which brings fuz to helm, or flx which is one of the original fuzzy matching packages for Emacs.
  • flx is smart fuzzy matching, but slow and doesn’t scale to large codebases at all.
  • affe, helm, and consult can take advantage of completion-styles and leverage Emacs 31’s C flex implementation using Gotoh’s algorithm. From my short testing, the C implementation doesn’t scale very well to large codebases, being single-threaded.
  • fzfa and fzf.el were 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 adding flex filtering to the matching will slow Emacs to a crawl. If you use emacs31 flex instead, you lose the multi component matching.
  • fzfa integrates with completion-styles though fzfa does it by taking over the variable (ignoring the other values in the list).
  • helm integrates with completion-styles through 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.)

Smart fuzzy vs filter-only

How the other packages handle the smart-fuzzy-vs-filter dimension covered in Smart fuzzy matching:

  • orderless treats query components as substring / literal / regex / flex predicates. Its flex dispatcher 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.
  • affe filters via all-completions + optional orderless regex through completion-styles. Pure filter; no per-character scoring; ranking is whatever all-completions returns.
  • helm- sources use helm’s multi-pattern matcher (substring or regex). A few external packages try to hook smart fuzzy matching into helm but with the downsides listed above.
  • hotfuzz is written in C and pluggable to completion-styles.

Multi-source

  • ~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-buffer and variants) are sync only; Third party packages are examples. (e.g. consult-omni)
  • ivy/counsel has no multi-source primitive. counsel-buffer-or-recentf and friends concatenate two lists into a flat collection — no per-source narrowing, filtering, or preview. ivy-read ignores the group-function completion- metadata key. fzfa-ivy adds genuine multi-source functionality via a display transformer (source label prefix) + a ivy-dispatching-call-based narrow menu using ivy--actions-list.

Cross-frontend integration

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.

Per-package notes (opinions mixed in)

  • =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-fzf is 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. The all-completions and completion-regexp-list approach it takes to filtering means it “just works” with most packages as well as orderless but that’s the same reason it’s not yet? compatible with fzf-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 with orderless or take advantage of any of the Emacs plumbing. Filtering is single-threaded Elisp; plateaus around 1-2M candidates while fzf-native chews 10-20M in the same wall time (again, eye test). Strict pure-Elisp win compared to fzfa’s native dep. fzfa took the most learning from this project and its background producer + filter flow is what fzfa does. 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 fzf command-line binary as its UI. Doesn’t participate in Emacs’s completion machinery (no completing-read integration, no embark, 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 requirement fzfa imposes and also don’t care about how integrated it is with other Emacs packages.

Prior discussions

Architecture

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.

Acknowledgements

About

Async completing-read using fzf

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors