8000 GitHub - uppet/fz-index: fz-index — Sublime-style fuzzy file open for Emacs · GitHub
[go: up one dir, main page]

Skip to content
uppetPublic

About

fz-index — Sublime-style fuzzy file open for Emacs

Resources

Stars

4 stars

Watchers

0 watching

Forks

Latest commit

 

History

38 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

fz-index — Sublime-style fuzzy file open for Emacs

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, so emacs.c ranks src/emacs.c first. The built-in flex completion 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 c finds src/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.

How it differs from consult-find / projectile / fzf.el

  • consult-find and fzf.el shell 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.
  • projectile and 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.

Install

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 (Emacs 29+)

(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 :vc (Emacs 30+)

(use-package fz-index
  :vc (:url "https://github.com/uppet/fz-index")
  :bind ("C-c f" . fz-index-open-file))

MELPA

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.

elpaca / straight.el

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

Manual

(add-to-list 'load-path "/path/to/fz-index")
(require 'fz-index)
(global-set-key (kbd "C-c f") #'fz-index-open-file)

The native module

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

  1. 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).
  2. Failing that, the bundled fz-index.c is compiled with a local gcc/clang (needs only emacs-module.h and 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).

Security and trust model

Using fz-index loads native code into your Emacs process, so the trust question is worth stating plainly.

  1. Prebuilt binaries. The fz-index-core.so / .dylib / .dll downloaded from GitHub Releases is built by this project’s CI (GitHub Actions) from the committed fz-index.c, and verified against the SHA-256 in checksums.txt published 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.
  2. Local compilation. The fallback compiles the bundled fz-index.c with 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.

Usage

Choosing the base directory

  • M-x fz-index-open-set-base — set the base directory (like Sublime’s project folder); all later fz-index-open-file calls search from there. Without it, the current directory is the base
  • Alternatively, set fz-index-root-function to 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))))
        

Searching

  • 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 (or M-<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 (face fz-index-match)
  • Multi-word queries: space-separated words must all match, in any order (util hash c finds src/util/hash.c), like Sublime and fzf
  • Smart case: an all-lowercase query is case-insensitive; any uppercase letter makes it case-sensitive
  • TAB fills the minibuffer with the selected candidate, so you can refine the query from it
  • M-p / M-n browse 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

Opening

  • RET open, C-u RET open in another window, M-RET insert the relative path into the buffer you came from, C-g quit

The results buffer

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.

Preview

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

Frecency and history

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.

Using it through completing-read

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

Resetting

M-x fz-index-reset drops the cached index (rebuilt on next query) and clears the base set by fz-index-open-set-base.

How indexing works

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.

Performance (400k files, synthetic third_party-style tree)

operationtime (Linux)
cold index~0.1 s
per-keystroke24–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-tree

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

Limitations

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

About

fz-index — Sublime-style fuzzy file open for Emacs

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

0