fz-index ports Sublime Text’s Goto Anything to Emacs: open any file in a project by typing a few fragments of its path. The file you meant ranks first, and a live preview follows as you move through the matches.
Two things make it Sublime-style rather than yet another fuzzy finder:
- Ranking. Scoring uses a Smith-Waterman-style DP that evaluates every
alignment of the query against the path — the model reverse-engineered
from Sublime (
fts_fuzzy_match) — instead of a greedy left-to-right match. Consecutive runs and word/camelCase boundaries win, soemacs.crankssrc/emacs.cfirst. The built-inflexcompletion style and flx use greedy or heatmap heuristics and produce visibly different orderings. - Interaction. An explicit base directory (Sublime’s project folder);
an empty query lists your most-opened files, frecency-ranked; moving the
selection previews the file in the window you came from; space-separated
words match in any order (
util hash cfindssrc/util/hash.c); and there is a results buffer you can jump into, isearch and click.
The candidate index lives in a C dynamic module, outside the Emacs Lisp heap, and each keystroke pulls only the top matches into Lisp — so per-keystroke cost does not grow with project size, which is what keeps the above responsive on a 400k-file tree. No external program (find, fd, fzf) is required at any point, so the same setup works on Windows.
consult-findandfzf.elshell out to an external program (find/fd/fzf) and stream the whole file list through a process pipe; fz-index keeps the index in-process, so per-keystroke cost depends on neither process startup nor pipe throughput, and ranking follows the Sublime model above rather than the external tool’s own algorithm.projectileand friends index in Emacs Lisp, where hundreds of thousands of file names pressure the GC; fz-index holds paths in C memory and hands Lisp only the top 100 matches per keystroke.
The package consists of fz-index.el plus a small C module. The
module is installed automatically on first use — no manual build step
is needed in the common case.
(package-vc-install "https://github.com/uppet/fz-index")
(global-set-key (kbd "C-c f") #'fz-index-open-file)Update later with M-x package-vc-upgrade RET fz-index RET.
(use-package fz-index
:vc (:url "https://github.com/uppet/fz-index")
:bind ("C-c f" . fz-index-open-file))The recipe has been submitted and is pending review
(melpa/melpa#10252).
Once merged, M-x package-install RET fz-index RET becomes the
recommended way to install; until then, use package-vc above.
;; For users of elpaca.el
(elpaca (fz-index :host github :repo "uppet/fz-index"))
;; For users of straight.el
(straight-use-package
'(fz-index :type git :host github :repo "uppet/fz-index"))(add-to-list 'load-path "/path/to/fz-index")
(require 'fz-index)
(global-set-key (kbd "C-c f") #'fz-index-open-file)M-x fz-index-open-file needs the fz-index-core dynamic module. On first
use, fz-index-ensure-module installs it automatically
(fz-index-module-auto-install, default t):
- A prebuilt binary matching your platform is downloaded 8000 from GitHub Releases and verified against the published SHA-256 checksum (Linux/macOS/Windows, x86_64 and aarch64).
- Failing that, the bundled
fz-index.cis compiled with a local gcc/clang (needs onlyemacs-module.hand a C99 compiler).
The checksum file is fetched from the same GitHub release as the binary, so this protects the download against corruption (truncated or re-encoded bytes) but is not a defense against a compromised release. See *Security and trust model for the full picture and how to opt out.
The install runs on first use; to run it ahead of time, load the
library first and use M-x fz-index-ensure-module. Or build by
hand with make (make fz-index-core.dll cross-compiles for
Windows with gcc-mingw-w64-x86-64; the same source also builds
natively under MSYS2).
Using fz-index loads native code into your Emacs process, so the
trust question is worth stating plainly.
- Prebuilt binaries. The
fz-index-core.so/.dylib/.dlldownloaded from GitHub Releases is built by this project’s CI (GitHub Actions) from the committedfz-index.c, and verified against the SHA-256 inchecksums.txtpublished with the same release. The checksum protects against a corrupted or truncated download, but it is not a defense against a compromised release: anyone who could publish to the repository or its CI could ship a binary and its matching checksum together. - Local compilation. The fallback compiles the bundled
fz-index.cwith your system’s gcc/clang. This runs a compiler you already trust, on source you can read, and is the most auditable of the two paths.
To avoid the automatic download entirely, set
fz-index-module-auto-install to nil and build the module yourself
with make (or make fz-index-core.dll on Windows);
fz-index-ensure-module then only loads a module you placed next
to fz-index.el.
Releases are tagged v<fz-index-version>, built by the workflow in
.github/workflows/build.yml, and published at
https://github.com/uppet/fz-index/releases.
M-x fz-index-open-set-base— set the base directory (like Sublime’s project folder); all laterfz-index-open-filecalls search from there. Without it, the current directory is the base- Alternatively, set
fz-index-root-functionto a function that returns the base directory automatically wherever a search starts (returning nil falls back to the manual base / current directory), e.g. for project-aware roots:(setq fz-index-root-function (lambda () (when-let ((project (project-current))) (project-root project))))
M-x fz-index-open-file— fuzzy-find a file under the base directory; with empty input, your most-opened files under the base are shown- Type to narrow;
C-n/C-p(orM-<down>/M-<up>) to move (the results window scrolls to follow the selection), or click a candidate with the mouse to open it directly. The matched characters of each candidate are highlighted (facefz-index-match) - Multi-word queries: space-separated words must all match, in any
order (
util hash cfindssrc/util/hash.c), like Sublime and fzf - Smart case: an all-lowercase query is case-insensitive; any uppercase letter makes it case-sensitive
TABfills the minibuffer with the selected candidate, so you can refine the query from itM-p/M-nbrowse your previous queries (minibuffer history)- Re-querying on minibuffer input is debounced by
fz-index-query-delay(default 0.01s), so fast typing does not launch a query per keystroke; confirming always applies any pending update first
RETopen,C-u RETopen in another window,M-RETinsert the relative path into the buffer you came from,C-gquit
C-o jumps between the minibuffer and the results buffer; in the
results buffer, isearch (C-s) works, RET opens the file on the
current line, C-u RET opens it in another window, and mouse-1
opens the clicked candidate.
Moving the selection previews the file in the origin window,
debounced by fz-index-preview-delay so rapid navigation stays
free; C-l toggles preview (files larger than
fz-index-preview-max-size are skipped).
Opened files get a frecency bonus on later queries: log2-compressed
open counts with a half-life decay on the last-open time
(fz-index-frecency-half-life, default 7 days), so stale favorites
gradually lose their lead. Queries fetch several times the display
limit before ranking (fz-index-query-oversample), so a strong bonus
can pull a favorite back into view even when its raw score ranks
below the limit. History persists in fz-index-history-file, and
fz-index-read-file (the completing-read entry point) feeds the
same history.
M-x fz-index-read-file runs the same search through
completing-read, for users of vertico/icomplete/fido or the
default completion UI (on Emacs 31.1+, *Completions* appears and
updates live via the eager-display/eager-update metadata).
fz-index-completion-table exposes the engine for custom
integrations (see the docstring).
M-x fz-index-reset drops the cached index (rebuilt on next query)
and clears the base set by fz-index-open-set-base.
Indexing walks the directory tree with a pool of worker threads fed
by a shared work queue and honors .gitignore (including
per-directory files and .git/info/exclude). Symlinked directories
are not followed — a loop, or a link out of the tree, would
otherwise keep the scan growing forever (matching fd and rg) —
while symlinked files are indexed. On Windows the mingw build
cannot always tell links apart, so directory links and junctions may
still be followed there.
Indexes are persisted under user-emacs-directory/fz-index/ and
served stale-while-revalidate style: the first search of a session
starts from the cache instantly while a background rescan refreshes
it (fz-index-cache-enabled to disable). Live indexes older than
fz-index-max-age (default 1 hour) are likewise refreshed in the
background. Repeated keystrokes rescore only the previous match set
(incremental narrowing).
(fz-index-build ROOT &optional NOTIFY-PROCESS NO-GITIGNORE) is the
module entry point if you want to drive it yourself; see also
fz-index-save / fz-index-load for the on-disk format.
| operation | time (Linux) |
|---|---|
| cold index | ~0.1 s |
| per-keystroke | 24–38 ms |
Reproduce it yourself. bench.el times the index build and a set
of representative queries (single-word, multi-word, no-match, and an
incremental narrowing sequence) and prints the Emacs/OS identity and
file count, so results are comparable across machines. The synthetic
tree is generated deterministically with bench-gen-tree.c:
gcc -O2 -std=c99 bench-gen-tree.c -o bench-gen-tree
./bench-gen-tree /tmp/bench-tree 400000
emacs -Q --batch -L . -l bench.el /tmp/bench-treeThe numbers above are cold timings from a Linux machine; index build is I/O-bound and varies a lot by platform and filesystem. As an example, the same synthetic tree with the mingw build on Windows (8 threads, NTFS) indexes in ~28 s while per-keystroke queries stay in the tens of milliseconds.
- Remote (TRAMP) directories cannot be indexed; the module reads the local filesystem only and signals an error for remote roots.
- File names that are not valid UTF-8 are skipped (the module API cannot pass them to Lisp strings).