Skip to content

dangduc/fzf-native

Repository files navigation

fzf-native

https://github.com/dangduc/fzf-native/actions/workflows/test.yaml/badge.svg https://github.com/dangduc/fzf-native/actions/workflows/cmake.yaml/badge.svg https://github.com/dangduc/fzf-native/actions/workflows/format.yaml/badge.svg

This is a package that provides fuzzy match scoring based on the fzf algorithm by junegunn. The main contribution is a wrapper over the C implementation of fzf from the project telescope-fzf-native.nvim. Elisp functions for scoring are exported through an Emacs dynamic module.

This package does one thing – For a given STR and QUERY compute and return a score and matching indices. If you’re looking for a fuzzy auto-completion engine, see the Use Cases section for how this package can be used in a completion-style.

;; Example of basic usage
(fzf-native-score "Hot-Topic" "hp")
;; (41 0 6)
;; Example of no match
(fzf-native-score "Hot-Topic" "2")
;; (0)
;; Example of slab re-use
(let ((slab (fzf-native-make-default-slab)))
  (fzf-native-score "Hello World" "er" slab)
  (fzf-native-score "Example of slab re-use" "xu" slab))
;; (24 1 19)

See test cases for more examples.

Supported Platforms

Linux, macOS (incl. Apple silicon), and Windows are supported. Pre-built shared libraries are in the bin/ directory.

Installation

MELPA

Not yet on MELPA.

Manually

Clone / download this repository and modify your load-path:

(add-to-list 'load-path (expand-file-name "/path/to/fzf-native/" user-emacs-directory))

use-package with :vc

; Configuration that uses pre-built dynamic module.
(use-package fzf-native
  :vc (:url "https://github.com/dangduc/fzf-native" :rev :newest)
  :config
  (fzf-native-load-dyn))

Straight Examples

; Configuration that builds dynamic module locally.
(use-package fzf-native
  :straight
  (:repo "dangduc/fzf-native"
   :host github
   :files (:defaults "*.c" "*.h" "*.txt"))
  :init
  (setq fzf-native-always-compile-module t)
  :config
  (fzf-native-load-own-build-dyn))
; Configuration that uses pre-built dynamic module.
(use-package fzf-native
  :straight
  (:repo "dangduc/fzf-native"
   :host github
   :files (:defaults "bin"))
  :config
  (fzf-native-load-dyn))

Use Cases

Fussy: fzf-native is used as one of several choose-your-own scoring backends in fussy, a package that provides a completing-style for intelligent matching and sorting. See fussy’s architecture overview for how fussy dispatches to fzf-native during all-completions.

fzfa: an Emacs completing-read wrapper that uses fzf-native’s async surface to stream candidates from long-running shell commands (find, rg, git ls-files, etc.) without blocking. See fzfa’s architecture overview for the Elisp-side pipeline.

Customization

The C module reads the following defcustoms via symbol-value at call time. Higher-level packages (fussy, fzfa) keep their own user-facing defcustoms and bridge their values onto these canonical names — fussy via setq-local (synchronous, same-buffer call pattern), fzfa via :around advice on the C entry points (timer-driven, cross-buffer). Set these directly only if you call fzf-native-score / fzf-native-score-all / fzf-native-async-* yourself; otherwise prefer the package-level knob.

VariableDefaultRead byBehavior
fzf-native-case-modesmartevery scoring call (sync + async)smart (lowercase = ignore, mixed = respect) / ignore / respect
fzf-native-batch-highlight25fzf-native-score / fzf-native-score-allnil = off; positive N = apply completions-common-part face to top N candidates via fzf_get_positions in C.
fzf-native-async-highlight200fzf-native-async-candidatesnil = off; t = all returned candidates; positive N = top N.
fzf-native-max-line-length256fzf-native-async-start (once at session start)nil = no limit; +N = drop lines longer than N; -N = truncate lines to N characters.
fzf-native-async-cache-size40fzf-native-async-start (once at session start)LRU result-cache entries. Each entry stores top-K results plus the full matched-candidate index for one query, enabling exact-fresh hits and prefix-refinement hits.
fzf-native-filter-only-min-pool10 000 000fzf-native-async-start (once at session start), fzf-native-score-all (per call)Pool-size threshold for filter-only mode. When the candidate pool reaches ≥ N, scoring swaps fzf_get_score for fzf_has_match (cheap boolean) and skips top-K sorting. nil~/~0 disables this arm.
fzf-native-filter-only-lengthnilevery scoring call (sync + async)Query-length threshold for filter-only mode. When set to a positive integer N and the query is at most N characters, take the cheap match path. nil~/~0 disables this arm.
fzf-native-filter-only-logicorevery scoring call (sync + async)How the two filter-only arms compose: or = either trigger is sufficient; and = every enabled arm must fire (a disabled arm is trivially satisfied).

Filter-only mode

Filter-only mode replaces the per-candidate fuzzy-DP score (fzf_get_score) with a cheap boolean match (fzf_has_match, ~5× faster) and skips the top-K counting sort. Useful when full scoring is wasted work: very large pools (pool-size arm) or very short queries (length arm) where score ranking carries little signal anyway.

Two independent triggers — pool size and query length — compose via fzf-native-filter-only-logic. Set either or both; nil~/~0 on a defcustom disables that arm.

Sync (fzf-native-score-all): when filter-only fires, candidates are returned in input order, no completion-score text property is attached. Highlights still apply to the top-N entries (top by input order in this mode).

Async (fzf-native-async-candidates): when filter-only fires, the match-set is still built exhaustively so the per-session result cache can refine on the next keystroke; only the cap-bound emit window (fzf-native-async-highlight) gets re-scored + sorted so the visible top stays ranked.

(fzf-native-filter-only-p QUERY-LENGTH POOL-SIZE)

C-defined predicate that returns non-nil when filter-only mode would fire at the given query length and pool size. Reads the same three defcustoms. Single source of truth for callers that want to take consistent code paths — e.g. fussy’s metadata adjustment skips its score-based sort when this returns non-nil for the current call.

Highlighting runs entirely inside the C module: after scoring, fzf_get_positions produces matched byte offsets, which are merged into runs and applied to the candidate string with put-text-property before strings are handed back to Emacs. No Elisp regex pass is involved.

Building the Native Libraries

mkdir build && cmake -B build -DCMAKE_C_FLAGS='-O3 -march=native' && cmake --build build

Debugging fzf-native with emacs/lldb (EXAMPLE)

Building Emacs

cd Code/emacs
brew install autoconf automake pkg-config ncurses gnutls libjpeg libgif libtiff libxpm libx11 libxt libxml2
autoreconf -isvf
./autogen.sh
./configure CFLAGS="-g -O0" LDFLAGS="-g" --with-ns
make bootstrap

Building fzf-native

$ pwd
~/.emacs.d/packages/emacs_31/elpaca/repos/fzf-native
$ rm -Rf build; mkdir build && cmake -B build -DCMAKE_C_FLAGS='-O0 -g -march=native' && cmake --build build

From LLDB

$ pwd
~/Code/emacs/src
$ lldb --local-lldbinit ./emacs

From Emacs

Refer to https://github.com/svaante/dape?tab=readme-ov-file#c-c-and-rust—lldb-dap on how to install LLDB-dap.

$ brew install llvm
($(brew --prefix --installed llvm)/bin)  # PREPEND (OSX already has a binary on $PATH) to $PATH.
(use-package dape
  :init
  ;; Enable repeat mode for more ergonomic `dape' use
  (use-package repeat
    :ensure nil
    :config
    (repeat-mode))
  :config
  (push
   '(lldb-dap
     modes (c-mode c-ts-mode c++-mode c++-ts-mode)
     command "lldb-dap"
     command-args ["--local-lldbinit"]
     ensure dape-ensure-command
     :type "lldb-dap"
     :cwd "/Users/james/Code/emacs/src"
     :program "/Users/james/Code/emacs/src/emacs")
   dape-configs)
  ;; Turn on global bindings for setting breakpoints with mouse
  (dape-breakpoint-global-mode)
  ;; Info buffers to the right
  (setq dape-buffer-window-arrangement 'right)
  ;; Info buffers like gud (gdb-mi)
  (setq dape-buffer-window-arrangement 'gud)
  (setq dape-info-hide-mode-line nil)
  ;; Pulse source line (performance hit)
  (add-hook 'dape-display-source-hook 'pulse-momentary-highlight-one-line)
  ;; Showing inlay hints
  (setq dape-inlay-hints t)
  ;; Save buffers on startup, useful for interpreted languages
  (add-hook 'dape-start-hook (lambda () (save-some-buffers t t)))
  ;; Kill compile buffer on build success
  (add-hook 'dape-compile-hook 'kill-buffer)
  ;; Projectile users
  (setq dape-cwd-fn 'projectile-project-root))

;; M-x dape in fzf-native-module.c
;; Set breakpoints with mouse.

Architecture

For a deep dive into how fzf-native works internally — the three scoring paths (sync single, sync batch, async streaming), the AsyncSession threading and lock model, the arena allocator, the score_abort same-filter rule, and the counting-sort top-K extraction — see architecture.org.

Credit

All credit for fzf.c goes to the telescope-fzf-native.nvim project. Much credit for Emacs module binding code goes to the hotfuzz project.

Releases

No releases published

Packages

 
 
 

Contributors