From a3d310929239a08768b1b0f6ee78673e52e21c86 Mon Sep 17 00:00:00 2001 From: Zhu Zihao Date: Fri, 19 Jul 2019 21:45:43 +0800 Subject: [PATCH] Add `ivy-fuz.el` Help fuz integrate with ivy. --- README.org | 14 ++++- fuz-extra.el | 28 +++++----- ivy-fuz.el | 153 ++++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 172 insertions(+), 23 deletions(-) diff --git a/README.org b/README.org index e92b8a0..d8dac00 100644 --- a/README.org +++ b/README.org @@ -38,7 +38,19 @@ ln -sv target/release/libfuz_core.so fuz-core.so (require 'helm-fuz) (helm-fuz-mode)) #+END_SRC -*** Ivy (WIP) +*** Ivy + + Add these config to your =.emacs.d= + + #+BEGIN_SRC emacs-lisp + (setq ivy-sort-functions-alist '(t . ivy-fuz-sort-fn)) + (setq ivy-re-builders-alist '(t . ivy-fuz-fuzzy-regex)) + (setq ivy-highlight-functions-alist '(ivy-fuz-fuzzy-regex . ivy-fuz-highlight-fn)) + + (with-eval-after-load 'ivy + (require 'ivy-fuz)) + #+END_SRC + *** Company (WIP) ** Using fuz's score/match function in your Elisp *** TODO Add documentation here diff --git a/fuz-extra.el b/fuz-extra.el index 602bec1..73f515a 100644 --- a/fuz-extra.el +++ b/fuz-extra.el @@ -31,6 +31,7 @@ ;;; Code: (require 'cl-lib) +(require 'inline) (require 'fuz) (eval-when-compile @@ -63,27 +64,24 @@ Sign: (-> (-> Str Str (Listof Long)) (Listof Str) Str (Option (Listof Long)))" (cl-return nil)))) (cons total-score (sort (cl-delete-duplicates all-indices :test #'=) #'<)))) -(defsubst fuz-sort-with-key! (list comp-fn key) +(define-inline fuz-sort-with-key! (list comp-fn key) "Sort LIST with COMP-FN, transfrom elem in LIST with KEY before comparison." - (sort list (lambda (e1 e2) - (funcall comp-fn - (funcall key e1) - (funcall key e2))))) - -(defun fuz-memo-function (fn test) + (inline-letevals (key) + (inline-quote + (sort ,list (lambda (e1 e2) + (funcall ,comp-fn + (funcall ,key e1) + (funcall ,key e2))))))) + +(defsubst fuz-memo-function (fn test) "Memoize the FN. Sign: (All (I O) (-> (-> I O) (U 'eq 'eql 'equal) (-> I O))) -TEST can be one of `eq' `equal' `eql', which used to compare the input -of FN and decide whether to get cached value or not." - (let ((cache (make-hash-table :test test)) - (not-found-sym (make-symbol "not-found"))) +TEST can be one of `eq', `eql', `equal', which used as cache hash's test-fn." + (let ((cache (make-hash-table :test test))) (lambda (input) - (let ((val (gethash input cache not-found-sym))) - (if (eq val not-found-sym) - (puthash input (funcall fn input) cache) - val))))) + (gethash input cache (puthash input (funcall fn input) cache))))) (provide 'fuz-extra) diff --git a/ivy-fuz.el b/ivy-fuz.el index d5e994b..e6f9ba3 100644 --- a/ivy-fuz.el +++ b/ivy-fuz.el @@ -1,13 +1,152 @@ -;;; -*- lexical-binding: t; -*- +;;; ivy-fuz.el --- Integrate Ivy and Fuz -*- lexical-binding: t -*- -(require 'cl-lib) -(require 'fuz) +;; Copyright (C) 2019 Zhu Zihao + +;; Author: Zhu Zihao +;; URL: +;; Version: 0.0.1 +;; Package-Requires: ((emacs "25.1") (ivy "20190717")) +;; Keywords: lisp + +;; This file is NOT part of GNU Emacs. + +;; This file is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation; either version 3, or (at your option) +;; any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; For a full copy of the GNU General Public License +;; see . + +;;; Commentary: + +;; + +;;; Code: + +(require 'inline) (require 'ivy) +(require 'fuz) +(require 'fuz-extra) + +(eval-when-compile + (require 'pcase)) + +;;; Customize + +(defgroup ivy-fuz () + "Sort `ivy' candidates by fuz" + :group 'ivy + :prefix "ivy-fuz-") + +(defcustom ivy-fuz-sorting-method 'skim + "The fuzzy sorting method in use. -(defalias 'ivy-fuz-regex-fuzzy 'ivy--regex-fuzzy) +The value should be `skim' or `clangd', skim's scoring function is a little +slower but return better result than clangd's." + :type '(choice + (const :tag "Skim" skim) + (const :tag "Clangd" clangd)) + :group 'ivy-fuz) -;; (defun ivy-fuz--sort-advice (orig-fun name candidates) -;; (if (not (= )))) +(defcustom ivy-fuz-sort-limit 15000 + "" + :type '(choice + (const :tag "Unlimited" nil) + integer) + :group 'ivy-fuz) + +(defvar ivy-fuz--sort-timer (float-time)) +(defvar ivy-fuz--score-cache (make-hash-table :test #'equal)) + +;;; Utils + +(define-inline ivy-fuz--fuzzy-score (pattern str) + "Calc the fuzzy match score of STR with PATTERN. + +Sign: (-> Str Str Long)" + (inline-quote + (or (pcase-exhaustive ivy-fuz-sorting-method + (`clangd (fuz-calc-score-clangd ,pattern ,str)) + (`skim (fuz-calc-score-skim ,pattern ,str))) + most-negative-fixnum))) + +(define-inline ivy-fuz--fuzzy-indices (pattern str) + "Return all char positions where STR fuzzy matched with PATTERN. + +Sign: (-> Str Str (Option (Listof Long)))" + (inline-quote + (pcase-exhaustive ivy-fuz-sorting-method + (`clangd (fuz-find-indices-clangd ,pattern ,str)) + (`skim (fuz-find-indices-skim ,pattern ,str))))) + +(defun ivy-fuz--get-score-data (pattern cand) + "Return (LENGTH SCORE) by matching CAND with PATTERN. + +Sign: (-> Str Str (List Long Long))" + (let ((len (length cand))) + ;; FIXME: Short pattern may have higher score matching longer pattern + ;; than exactly matching itself + ;; e.g. "ielm" will prefer [iel]m-[m]enu than [ielm] + (if (string= cand pattern) + (list len most-positive-fixnum) + (list len (ivy-fuz--fuzzy-score pattern cand))))) + +;;;###autoload +(defalias 'ivy-fuz-fuzzy-regex #'ivy--regex-fuzzy) + +;;;###autoload +(defun ivy-fuz-sort-fn (pattern cands) + (condition-case nil + (let* ((bolp (string-prefix-p "^" pattern)) + (fuzzy-regex + (concat "\\`" + (and bolp (regexp-quote (substring pattern 1 2))) + (mapconcat + (lambda (x) + (setq x (char-to-string x)) + (concat "[^" x "]*" (regexp-quote x))) + (if bolp (substring pattern 2) pattern) + ""))) + (realpat (if bolp (substring pattern 1) pattern)) + (memo-fn (fuz-memo-function + (lambda (cand) (ivy-fuz--get-score-data realpat cand)) + #'equal))) + (let ((counter 0) + cands-to-sort) + (while (and cands + (< counter ivy-fuz-sort-limit)) + (when (string-match-p fuzzy-regex (car cands)) + (push (pop cands) cands-to-sort) + (cl-incf counter))) + + (setq cands (fuz-sort-with-key! cands #'< #'length)) + (nconc (fuz-sort-with-key! cands-to-sort + (pcase-lambda (`(,len1 ,scr1) `(,len2 ,scr2)) + (if (= scr1 scr2) + (< len1 len2) + (> scr1 scr2))) + memo-fn) + cands))) + (error cands))) + +;;;###autoload +(defun ivy-fuz-highlight-fn (str) + "" + (fuz-highlighter (ivy-fuz--fuzzy-indices (ivy--remove-prefix "^" ivy-text) + str) + (ivy--minibuffer-face 0) + str)) (provide 'ivy-fuz) -;;; ivy-fuz.el ends here + +;; Local Variables: +;; coding: utf-8 +;; End: + +;; ivy-fuz.el ends here.