This is a package to provide a completion-style
to Emacs that is able to
leverage flx as well as various other
fuzzy matching scoring packages to provide intelligent scoring and sorting.
This package is intended to be used with packages that leverage
completion-styles
, e.g. completing-read
and completion-at-point-functions
.
It is usable with icomplete
(as well as fido-mode
), selectrum
,
vertico
, corfu
, helm
and company-mode
’s company-capf
.
It is not currently usable with ido
which doesn’t support
completion-styles
and has its own sorting and filtering system. In
addition to those packages, other company-mode
backends will not hook into
this package. ivy
support can be somewhat baked in following
https://github.com/jojojames/fussy#ivy-integration but the
performance gains may not be as high as the other completion-read
APIs.
- Get the package, either from MELPA (soon to come):
M-x package-install RET fussy RET
Or clone / download this repository and modify your
load-path
:(add-to-list 'load-path (expand-file-name "/path/to/fussy/" user-emacs-directory))
(use-package fussy
:ensure t
:straight
(fussy :type git :host github :repo "jojojames/fussy")
:config
(push 'fussy completion-styles)
(setq
;; For example, project-find-file uses 'project-files which uses
;; substring completion by default. Set to nil to make sure it's using
;; flx.
completion-category-defaults nil
completion-category-overrides nil))
We default to flx for scoring matches but additional (listed below) scoring functions/backends can be used.
flx is a dependency of fussy
and the default
scoring algorithm.
flx
has a great scoring algorithm but is one of the slower implementations
compared to the other scoring backends written as native modules.
flx-rs is a native module written in Rust
that matches the original flx
scoring algorithm. It is about 10 times faster
than the original implementation written in Emacs Lisp. We can use this package
instead for extra performance with the same scoring strategy.
One downside of this package is that it doesn’t yet support using flx
’s file
cache so filename matching is currently slightly worse than the original Emacs
lisp implementation.
(use-package flx-rs
:ensure t
:straight
(flx-rs
:repo "jcs-elpa/flx-rs"
:fetcher github
:files (:defaults "bin"))
:config
(setq fussy-score-fn 'flx-rs-score)
(flx-rs-load-dyn))
Another option is to use the fuz library (also in Rust) for scoring.
This library has two fuzzy matching algorithms, skim
and clangd
.
Skim: Just like fzf v2, the algorithm is based on Smith-Waterman algorithm which is normally used in DNA sequence alignment
Clangd: The algorithm is based on clangd’s FuzzyMatch.cpp.
For more information: fuzzy-matcher
(use-package fuz
:ensure nil
:straight (fuz :type git :host github :repo "rustify-emacs/fuz.el")
:config
(setq fussy-score-fn 'fussy-fuz-score)
(unless (require 'fuz-core nil t)
(fuz-build-and-load-dymod)))
;; Same as fuz but with prebuilt binaries.
(use-package fuz-bin
:ensure t
:straight
(fuz-bin
:repo "jcs-elpa/fuz-bin"
:fetcher github
:files (:defaults "bin"))
:config
(setq fussy-score-fn 'fussy-fuz-bin-score)
(fuz-bin-load-dyn))
A mimetic poly-alloy of the Quicksilver scoring algorithm, essentially LiquidMetal.
Flex matching short abbreviations against longer strings is a boon in productivity for typists. Applications like Quicksilver, Alfred, LaunchBar, and Launchy have made this method of keyboard entry a popular one. It’s time to bring this same functionality to web controls. LiquidMetal makes scoring long strings against abbreviations easy.
For more information: liquidmetal
(use-package liquidmetal
:ensure t
:straight t
:config
(setq fussy-score-fn 'fussy-liquidmetal-score))
Fuzzy matching algorithm based on Sublime Text’s string search. Iterates through characters of a search string and calculates a score. This is another fuzzy implementation written in Rust.
For more information: fuzzy-rs
(use-package sublime-fuzzy
:ensure t
:straight
(sublime-fuzzy
:repo "jcs-elpa/sublime-fuzzy"
:fetcher github
:files (:defaults "bin"))
:config
(setq fussy-score-fn 'fussy-sublime-fuzzy-score)
(sublime-fuzzy-load-dyn))
This is a fuzzy Emacs completion style similar to the built-in flex style, but with a better scoring algorithm. Specifically, it is non-greedy and ranks completions that match at word; path component; or camelCase boundaries higher.
For more information: hotfuzz
Note, hotfuzz
has its own completion-style
that may be worth using over this one.
(use-package hotfuzz
:ensure t
:straight t
:config
(setq fussy-score-fn 'fussy-hotfuzz-score))
Before scoring and sorting candidates, we must somehow filter them from the completion table. The approaches below are several ways to do that, each with varying advantages and disadvantages.
For the choices below, we benchmark the functions by benchmarking the entire
fussy-all-completions
function with the below macro calling M-x
describe-symbol (30000 candidates)
in the scratch buffer.
(defmacro fussy--measure-time (&rest body)
"Measure the time it takes to evaluate BODY.
https://lists.gnu.org/archive/html/help-gnu-emacs/2008-06/msg00087.html"
`(let ((time (current-time)))
(let ((result ,@body))
(message "%.06f" (float-time (time-since time)))
result)))
This is the default filtering method and is 1:1 to the filtering done
when using the flex
completion-style
. Advantages are no additional
dependencies (e.g. orderless
) and likely bug-free/stable to use.
The only disadvantage is that it’s the slowest of the filtering methods.
;; Flex
(setq fussy-filter-fn 'fussy-filter-flex)
;; Type Letter a
;; 0.078952
;; Type Letter b
;; 0.052590
;; Type Letter c
;; 0.065808
;; Type Letter d
;; 0.061254
;; Type Letter e
;; 0.098000
;; Type Letter f
;; 0.053321
;; Type Letter g
;; 0.050180
This is another useable filtering method and leverages the all-completions
API
written in C to do its filtering. It seems to be the fastest of the filtering
methods from quick benchmarking as well as requiring no additional dependencies
(e.g. orderless
).
Implementation may be buggy though, so use with caution.
;; Fast
(setq fussy-filter-fn 'fussy-filter-fast)
;; Type Letter a
;; 0.030671
;; Type Letter b
;; 0.030247
;; Type Letter c
;; 0.036047
;; Type Letter d
;; 0.032071
;; Type Letter e
;; 0.034785
;; Type Letter f
;; 0.030392
;; Type Letter g
;; 0.033473
orderless can also be used for
filtering. It uses the all-completions
API like fussy-filter-fast
so is
also faster than the default filtering but has a dependency on orderless
.
;; Orderless
(setq fussy-filter-fn 'fussy-filter-orderless)
;; Type Letter a
;; 0.065390
;; Type Letter b
;; 0.036942
;; Type Letter c
;; 0.054091
;; Type Letter d
;; 0.048816
;; Type Letter e
;; 0.074258
;; Type Letter f
;; 0.040900
;; Type Letter g
;; 0.037928
To use orderless filtering:
(use-package orderless
:straight t
:ensure t
:commands (orderless-filter))
(setq fussy-filter-fn 'fussy-filter-orderless)
Fuzzy completion may or may not be too slow when completing with company-mode.
For this, we can advise company-capf
to use basic completions.
(defconst OG-COMPLETION-STYLES completion-styles
"Original `completion-styles' Emacs comes with.")
(defun company-capf-with-og-completion-styles (f &rest args)
"Set `completion-styles' to be the default Emacs `completion-styles'
while `company-capf' runs."
(let ((completion-styles OG-COMPLETION-STYLES))
(apply f args)))
(advice-add 'company-capf :around 'company-capf-with-og-completion-styles)
Another option is to only apply fuzzy matching later in the query.
(defun company-capf-smart-completion-styles (f &rest args)
"Change which `completion-style' to use based off `company-prefix'."
(let ((completion-styles
(if (length< company-prefix 3)
'(basic partial-completion emacs22)
'(fussy basic partial-completion emacs22))))
(apply f args)))
(advice-add 'company-capf :around 'company-capf-smart-completion-styles)
If you intend to run company-capf
with fussy
, then use the below
company-transformer
to sort matches based off fussy
scoring.
(setq company-transformers
'(fussy-company-sort-by-completion-score))
Eglot by default uses flex
in completion-category-defaults
.
Use this to override that.
(with-eval-after-load 'eglot
(add-to-list 'completion-category-overrides
'(eglot (styles fussy basic))))
Integration with helm is possible by
setting helm-completion-style
to emacs
instead of helm
.
(setq helm-completion-style 'emacs)
For more information: https://github.com/emacs-helm/helm/blob/master/helm-mode.el#L269
Since ivy
doesn’t support completion-styles
, we have to hack fussy
into it.
We can advise ivy--flx-sort
and replace it with our own sorting function.
(defun ivy--fussy-sort (name cands)
"Sort according to closeness to string NAME the string list CANDS."
(condition-case nil
(let* ((bolp (= (string-to-char name) ?^))
;; An optimized regex for fuzzy matching
;; "abc" → "^[^a]*a[^b]*b[^c]*c"
(fuzzy-regex (concat "\\`"
(and bolp (regexp-quote (substring name 1 2)))
(mapconcat
(lambda (x)
(setq x (char-to-string x))
(concat "[^" x "]*" (regexp-quote x)))
(if bolp (substring name 2) name)
"")))
;; Strip off the leading "^" for flx matching
(flx-name (if bolp (substring name 1) name))
cands-left
cands-to-sort)
;; Filter out non-matching candidates
(dolist (cand cands)
(when (string-match-p fuzzy-regex cand)
(push cand cands-left)))
;; pre-sort the candidates by length before partitioning
(setq cands-left (cl-sort cands-left #'< :key #'length))
;; partition the candidates into sorted and unsorted groups
(dotimes (_ (min (length cands-left) ivy-flx-limit))
(push (pop cands-left) cands-to-sort))
(nconc
;; Compute all of the flx scores in one pass and sort
(mapcar #'car
(sort (mapcar
(lambda (cand)
(cons cand
(car
(funcall
fussy-score-fn
cand flx-name
ivy--flx-cache))))
cands-to-sort)
(lambda (c1 c2)
;; Break ties by length
(if (/= (cdr c1) (cdr c2))
(> (cdr c1)
(cdr c2))
(< (length (car c1))
(length (car c2)))))))
;; Add the unsorted candidates
cands-left))
(error cands)))
(advice-add 'ivy--flx-sort :override 'ivy--fussy-sort)
For more information: abo-abo/swiper#848 (comment)
User is recommended to try the various scoring functions. See fussy-score-fn
.
For speed, flx-rs
or fuz/fuz-bin
will be the most performant but uses Rust.
flx-rs
will provide an algorithm that matches the original flx
algorithm.
Below is a sample config that uses flx-rs
for improved performance.
fuz-bin
or fuz
also seem to be slightly faster than flx-rs
and uses a different algorithm.
(use-package orderless
:straight t
:ensure t
:commands (orderless-filter))
(use-package flx-rs
:ensure t
:straight
(flx-rs
:repo "jcs-elpa/flx-rs"
:fetcher github
:files (:defaults "bin"))
:config
(setq fussy-score-fn 'flx-rs-score)
(flx-rs-load-dyn))
(use-package fussy
:ensure t
:straight
(fussy :type git :host github :repo "jojojames/fussy")
:config
(setq fussy-score-fn 'flx-rs-score)
(setq fussy-filter-fn 'fussy-filter-orderless)
(push 'fussy completion-styles)
(setq
;; For example, project-find-file uses 'project-files which uses
;; substring completion by default. Set to nil to make sure it's using
;; flx.
completion-category-defaults nil
completion-category-overrides nil)
;; `eglot' defaults to flex, so set an override to point to fussy instead.
(with-eval-after-load 'eglot
(add-to-list 'completion-category-overrides
'(eglot (styles fussy basic)))))
Documenting my configuration for the users that may want to copy. Unlike the
former configuration, this section will be kept up to date with my init.el
.
(use-package fuz-bin
:ensure t
:straight
(fuz-bin
:repo "jcs-elpa/fuz-bin"
:fetcher github
:files (:defaults "bin"))
:config
(fuz-bin-load-dyn))
(use-package fussy
:ensure t
:straight
(fussy :type git :host github :repo "jojojames/fussy")
:config
(setq fussy-filter-fn 'fussy-filter-fast)
(setq fussy-score-fn 'fussy-fuz-bin-score)
(push 'fussy completion-styles)
(setq
;; For example, project-find-file uses 'project-files which uses
;; substring completion by default. Set to nil to make sure it's using
;; flx.
completion-category-defaults nil
completion-category-overrides nil)
;; `eglot' defaults to flex, so set an override to point to flx instead.
(with-eval-after-load 'eglot
(add-to-list 'completion-category-overrides
'(eglot (styles fussy basic)))))
lewang/flx#54 company-mode/company-mode#47 abo-abo/swiper#207 abo-abo/swiper#2321 abo-abo/swiper#848 melpa/melpa#8029 emacs-helm/helm#2165