branch: externals/hotfuzz
commit 3076cb250d0cb7ac6c3ec746dc4ccfea09ccdb25
Author: Axel Forsman <[email protected]>
Commit: Axel Forsman <[email protected]>

    Use completion-regexp-list
    
    Delegate filtering to the built-in function all-completions by adding
    the regexp to completion-regexp-list, instead of doing it in Lisp with
    string-match-p. This brings a speedup of almost 10x, making the
    dynamic module only ~1000% faster.
---
 hotfuzz.el    | 125 ++++++++++++++++++++++++++--------------------------------
 test/tests.el |  19 +++++----
 2 files changed, 64 insertions(+), 80 deletions(-)

diff --git a/hotfuzz.el b/hotfuzz.el
index bab78bdd48..4b37af0d3b 100644
--- a/hotfuzz.el
+++ b/hotfuzz.el
@@ -1,8 +1,8 @@
-;;; hotfuzz.el --- Fuzzy completion style  -*- lexical-binding: t; -*-
+;;; hotfuzz.el --- Fuzzy completion style  -*- lexical-binding: t -*-
 
-;; Copyright 2021 Axel Forsman
+;; Copyright (C) 2021 Axel Forsman
 
-;; Author: Axel Forsman <[email protected]>
+;; Author: Axel Forsman <[email protected]>
 ;; Version: 0.1
 ;; Package-Requires: ((emacs "27.1"))
 ;; Keywords: matching
@@ -28,16 +28,14 @@
 
 (defgroup hotfuzz nil
   "Fuzzy completion style."
-  :group 'minibuffer
-  :link '(url-link :tag "GitHub" "https://github.com/axelf4/hotfuzz";))
+  :group 'minibuffer)
 
 (defcustom hotfuzz-max-highlighted-completions 25
   "The number of top-ranking completions that should be highlighted.
-Large values will decrease performance. Only applies when using the
-Emacs `completion-styles' interface."
+Large values will decrease performance."
   :type 'integer)
 
-;; Since we pre-allocate the vectors the common optimization where
+;; Since the vectors are pre-allocated the optimization where
 ;; symmetricity w.r.t. to insertions/deletions means it suffices to
 ;; allocate min(#needle, #haystack) for C/D when only calculating the
 ;; cost does not apply.
@@ -71,8 +69,8 @@ Emacs `completion-styles' interface."
 (defun hotfuzz--calc-bonus (haystack)
   "Precompute all potential bonuses for matching certain characters in 
HAYSTACK."
   (cl-loop for ch across haystack and i from 0 and lastch = ?/ then ch do
-           (aset hotfuzz--bonus i
-                 (aref (aref hotfuzz--bonus-prev-luts (aref 
hotfuzz--bonus-cur-lut ch)) lastch))))
+           (let ((lut (aref hotfuzz--bonus-prev-luts (aref 
hotfuzz--bonus-cur-lut ch))))
+             (aset hotfuzz--bonus i (aref lut lastch)))))
 
 ;; Aᵢ denotes the prefix a₀,...,aᵢ₋₁ of A
 (defun hotfuzz--match-row (a b i nc nd pc pd)
@@ -82,11 +80,11 @@ the minimum cost when aᵢ is deleted. The costs for row I 
are written
 into NC/ND, using the costs for row I-1 in PC/PD. The vectors NC/PC
 and ND/PD respectively may alias."
   (cl-loop
-   with m = (length b) and oldc
+   with m = (length b)
    and g = 100 and h = 5 ; Every k-symbol gap is penalized by g+hk
    ;; s threads the old value C[i-1][j-1] throughout the loop
-   for j below m and s = (if (zerop i) 0 (+ g (* h i))) then oldc do
-   (setq oldc (aref pc j))
+   for j below m and s = (if (zerop i) 0 (+ g (* h i))) then oldc
+   for oldc = (aref pc j) do
    ;; Either extend optimal conversion of (i) Aᵢ₋₁ to Bⱼ₋₁, by
    ;; matching bⱼ (C[i-1,j-1]-bonus); or (ii) Aᵢ₋₁ to Bⱼ, by deleting
    ;; aᵢ and opening a new gap (C[i-1,j]+g+h) or enlarging the
@@ -99,19 +97,17 @@ and ND/PD respectively may alias."
 
 (defun hotfuzz--cost (needle haystack)
   "Return the difference score of NEEDLE and the match HAYSTACK."
-  (let ((n (length haystack)) (m (length needle))
-        (c hotfuzz--c) (d hotfuzz--d))
+  (let ((n (length haystack)) (m (length needle)))
     (if (> n hotfuzz--max-haystack-len)
         10000
-      (fillarray c 10000)
-      (fillarray d 10000)
       (hotfuzz--calc-bonus haystack)
-      (dotimes (i n) (hotfuzz--match-row haystack needle i c d c d))
-      (aref c (1- m))))) ; Final cost
+      (let ((c (fillarray hotfuzz--c 10000)) (d (fillarray hotfuzz--d 10000)))
+        (dotimes (i n) (hotfuzz--match-row haystack needle i c d c d))
+        (aref c (1- m)))))) ; Final cost
 
 (defun hotfuzz-highlight (needle haystack)
   "Highlight the characters that NEEDLE matched in HAYSTACK.
-HAYSTACK has to be a match according to `hotfuzz-filter'."
+HAYSTACK has to be a match according to `hotfuzz-all-completions'."
   (let ((n (length haystack)) (m (length needle))
         (c hotfuzz--c) (d hotfuzz--d)
         (case-fold-search completion-ignore-case))
@@ -120,71 +116,62 @@ HAYSTACK has to be a match according to `hotfuzz-filter'."
       (fillarray d 10000)
       (hotfuzz--calc-bonus haystack)
       (cl-loop
-       with rows = (cl-loop
-                    with nc and nd and res
-                    for i below n and pc = c then nc and pd = d then nd do
-                    (setq nc (make-vector m 0) nd (make-vector m 0))
-                    (hotfuzz--match-row haystack needle i nc nd pc pd)
-                    (push (cons nc nd) res)
-                    finally return res)
+       with rows initially
+       (cl-loop for i below n and pc = c then nc and pd = d then nd
+                and nc = (make-vector m 0) and nd = (make-vector m 0) do
+                (hotfuzz--match-row haystack needle i nc nd pc pd)
+                (push (cons nc nd) rows))
        ;; Backtrack to find matching positions
        for j from (1- m) downto 0 and i downfrom (1- n) do
        (cl-destructuring-bind (c . d) (pop rows)
          (when (<= (aref d j) (aref c j))
-           (while (progn (cl-decf i)
+           (while (progn (setq i (1- i))
                          (> (aref d j) (aref (setq d (cdr (pop rows))) j))))))
        (add-face-text-property i (1+ i) 'completions-common-part nil 
haystack))))
   haystack)
 
-;;;###autoload
-(defun hotfuzz-filter (string candidates)
-  "Filter CANDIDATES that match STRING and sort by the match costs.
-CANDIDATES should be a list of strings."
-  (cond
-   ((string= string "") candidates)
-   ((require 'hotfuzz-module nil t)
-    (hotfuzz--filter-c string candidates completion-ignore-case))
-   ((let ((re (concat
-               "\\`"
-               (mapconcat
-                (lambda (ch) (format "[^%c]*%s" ch (regexp-quote 
(char-to-string ch))))
-                string "")))
-          (case-fold-search completion-ignore-case))
-      (if (> (length string) hotfuzz--max-needle-len)
-          (cl-loop for x in candidates if (string-match-p re x) collect x)
-        (cl-loop
-         for x in candidates if (string-match-p re x)
-         collect (cons (hotfuzz--cost string x) x) into xs
-         finally return (mapcar #'cdr (cl-sort xs #'car-less-than-car))))))))
-
 ;;; Completion style implementation
 
 ;;;###autoload
-(defun hotfuzz-all-completions (string table pred point)
+(defun hotfuzz-all-completions (string table &optional pred point)
   "Get hotfuzz-completions of STRING in TABLE.
 See `completion-all-completions' for the semantics of PRED and POINT.
 This function prematurely sorts the completions; mutating the returned
 list before passing it to `display-sort-function' or
 `cycle-sort-function' will lead to inaccuracies."
+  (unless point (setq point (length string)))
   (let* ((beforepoint (substring string 0 point))
          (afterpoint (substring string point))
          (bounds (completion-boundaries beforepoint table pred afterpoint))
          (prefix (substring beforepoint 0 (car bounds)))
          (needle (substring beforepoint (car bounds)))
-         completion-regexp-list
-         (all (hotfuzz-filter
-               needle
-               (if (and (listp table) (not (consp (car table)))
-                        (not (functionp table)) (not pred))
-                   table
-                 (all-completions prefix table pred)))))
+         (use-module-p (require 'hotfuzz-module nil t))
+         (case-fold-search completion-ignore-case)
+         (completion-regexp-list
+          (if use-module-p completion-regexp-list
+            (let ((re (mapconcat
+                       (lambda (ch) (let ((s (char-to-string ch)))
+                                      (concat "[^" s "]*" (regexp-quote s))))
+                       needle "")))
+              (cons (concat "\\`" re) completion-regexp-list))))
+         (all (if (and (string= prefix "") (or (stringp (car-safe table)) 
(null table))
+                       (not (or pred completion-regexp-list (string= needle 
""))))
+                  table
+                (all-completions prefix table pred))))
+    ;; `completion-pcm--all-completions' tests completion-regexp-list
+    ;; again with functional tables even though they should handle it.
+    (cond
+     ((or (null all) (string= needle "")))
+     (use-module-p (setq all (hotfuzz--filter-c needle all 
completion-ignore-case)))
+     ((> (length needle) hotfuzz--max-needle-len))
+     (t (cl-loop for x in-ref all do (setf x (cons (hotfuzz--cost needle x) x))
+                 finally (setq all (mapcar #'cdr (sort all 
#'car-less-than-car))))))
     (when all
       (unless (string= needle "")
-        ;; Highlighting all completions without deferred highlighting
-        ;; (bug#47711) would take too long.
-        (cl-loop
-         repeat hotfuzz-max-highlighted-completions and for x in-ref all do
-         (setf x (hotfuzz-highlight needle (copy-sequence x))))
+        ;; Without deferred highlighting (bug#47711) only highlight
+        ;; the top completions.
+        (cl-loop repeat hotfuzz-max-highlighted-completions and for x in-ref 
all
+                 do (setf x (hotfuzz-highlight needle (copy-sequence x))))
         (when (zerop hotfuzz-max-highlighted-completions)
           (setcar all (copy-sequence (car all))))
         (put-text-property 0 1 'completion-sorted t (car all)))
@@ -194,14 +181,12 @@ list before passing it to `display-sort-function' or
   "Adjust completion METADATA for hotfuzz sorting."
   (let ((existing-dsf (completion-metadata-get metadata 
'display-sort-function))
         (existing-csf (completion-metadata-get metadata 'cycle-sort-function)))
-    (cl-flet
-        ((compose-sort-fn
-          (existing-sort-fn)
-          (lambda (completions)
-            (if (or (null completions)
-                    (get-text-property 0 'completion-sorted (car completions)))
-                completions
-              (funcall existing-sort-fn completions)))))
+    (cl-flet ((compose-sort-fn (existing-sort-fn)
+                (lambda (completions)
+                  (if (or (null completions)
+                          (get-text-property 0 'completion-sorted (car 
completions)))
+                      completions
+                    (funcall existing-sort-fn completions)))))
       `(metadata
         (display-sort-function . ,(compose-sort-fn (or existing-dsf 
#'identity)))
         (cycle-sort-function . ,(compose-sort-fn (or existing-csf #'identity)))
diff --git a/test/tests.el b/test/tests.el
index 601ee63ee5..62fb3bf0cc 100644
--- a/test/tests.el
+++ b/test/tests.el
@@ -45,18 +45,18 @@
 (ert-deftest case-sensitivity-test ()
   (let ((xs '("aa" "aA " "Aa  " "AA   ")))
     (let ((completion-ignore-case nil))
-      (should (equal (hotfuzz-filter "a" xs) '("aa" "aA " "Aa  ")))
-      (should (equal (hotfuzz-filter "A" xs) '("Aa  " "AA   " "aA "))))
+      (should (equal (hotfuzz-all-completions "a" xs) '("aa" "aA " "Aa  ")))
+      (should (equal (hotfuzz-all-completions "A" xs) '("Aa  " "AA   " "aA 
"))))
     (let ((completion-ignore-case t))
-      (should (equal (hotfuzz-filter "a" xs) xs))
-      (should (equal (hotfuzz-filter "A" xs) xs)))))
+      (should (equal (hotfuzz-all-completions "a" xs) xs))
+      (should (equal (hotfuzz-all-completions "A" xs) xs)))))
 
 (ert-deftest long-candidates-test ()
   (let ((a (make-string 4096 ?x))
         (b (concat (make-string 2047 ?y) "x" (make-string 2048 ?y))))
     ;; Too long candidates should still be filtered with matches
     ;; lumped together at the end in their original order.
-    (should (equal (hotfuzz-filter "x" (list (make-string 4096 ?y) b a "x"))
+    (should (equal (hotfuzz-all-completions "x" (list (make-string 4096 ?y) b 
a "x"))
                    (list "x" b a)))))
 
 (ert-deftest filter-long-needle-test ()
@@ -64,7 +64,7 @@
          (a (concat needle "y")))
     ;; With a too long search string candidates should only be
     ;; filtered but not sorted.
-    (should (equal (hotfuzz-filter needle (list a "y" needle))
+    (should (equal (hotfuzz-all-completions needle (list a "y" needle))
                    (list a needle)))))
 
 (ert-deftest all-completions-test ()
@@ -87,13 +87,12 @@
       (completion-all-completions
        "/usr/s/man"
        (lambda (string _pred action)
-         (let ((prefix-len (length (file-name-directory string))))
+         (let ((dir (file-name-directory string)))
            (pcase action
              ('metadata '(metadata (category . file)))
              (`(boundaries . ,suffix)
-              `(boundaries ,prefix-len . ,(string-match-p "/" suffix)))
-             ('t (mapcar (lambda (x) (substring x prefix-len))
-                         (list "/usr/bin/" "/usr/share/" "/usr/local/"))))))
+              `(boundaries ,(length dir) . ,(string-match-p "/" suffix)))
+             ('t (all-completions "" '("bin/" "share/" "local/"))))))
        nil
        6) ; Point as in "/usr/s|/man"
       '("share/" . 5)))))

Reply via email to