;;; ivy-prescient.el --- prescient.el + Ivy -*- lexical-binding: t -*- ;; Copyright (C) 2018 Radon Rosborough ;; Author: Radon Rosborough ;; Homepage: https://github.com/raxod502/prescient.el ;; Keywords: extensions ;; Package-Version: 20210425.1720 ;; Package-Commit: 4a0f5405798cfcb98ea005078ef2e2d490e922c4 ;; Created: 1 May 2018 ;; Package-Requires: ((emacs "25.1") (prescient "5.1") (ivy "0.11.0")) ;; SPDX-License-Identifier: MIT ;; Version: 5.1 ;;; Commentary: ;; ivy-prescient.el provides an interface for using prescient.el to ;; sort and filter candidates in Ivy menus. To enable its ;; functionality, turn on `ivy-prescient-mode' in your init-file or ;; interactively. ;; For more information, see https://github.com/raxod502/prescient.el. ;;; Code: ;;;; Libraries (eval-when-compile (require 'cl-macs) (require 'subr-x)) (require 'map) (require 'ivy) (require 'prescient) ;;;; User options (defcustom ivy-prescient-sort-commands '(:not swiper swiper-isearch ivy-switch-buffer) "Control which commands have their candidates sorted by `ivy-prescient'. If nil, then sorting is disabled for all commands. If t, then sorting is enabled for all commands. If a list of commands, then only those commands have their candidates sorted. If a list starting with the symbol `:not', then all commands *except* the ones listed have their candidates sorted. Note that this variable overrides the sorting options of Ivy, unless `ivy-prescient-enable-sorting' is nil in which case it has no effect." :group 'prescient :type '(choice (boolean :tag "Unconditional") (repeat :tag "Whitelist" function) (cons (const :not) (repeat :tag "Blacklist" function)))) (defcustom ivy-prescient-retain-classic-highlighting nil "Whether to emulate the way Ivy highlights candidates as closely as possible. With the default value, nil, the entire match is highlighted with `ivy-minibuffer-match-face-1' while initials in an initialism are highlighted with `ivy-minibuffer-match-face-2' through `ivy-minibuffer-match-face-4'. With a non-nil value, substring matches are also highlighted using `ivy-minibuffer-match-face-2' through `ivy-minibuffer-match-face-4', meaning that the only use of `ivy-minibuffer-match-face-1' is in between the initials of an initialism. Note that a non-nil value for this variable emulates the highlighting behavior of `ivy--regex-ignore-order', not the default `ivy--regex-plus', since `ivy-prescient' allows out-of-order matching." :group 'prescient :type 'boolean) (defcustom ivy-prescient-enable-filtering t "Whether to enable filtering by `ivy-prescient'. If nil, then `ivy-prescient-mode' does not change the filtering behavior of Ivy from the default. See Ivy documentation for how to configure filtering yourself. Changing this variable will not take effect until `ivy-prescient-mode' has been reloaded." :group 'prescient :type 'boolean) (defcustom ivy-prescient-enable-sorting t "Whether to enable sorting by `ivy-prescient'. If nil, then `ivy-prescient-mode' does not change the sorting behavior of Ivy from the default. See Ivy documentation for how to configure sorting yourself. Changing this variable will not take effect until `ivy-prescient-mode' has been reloaded." :group 'prescient :type 'boolean) ;;;; Minor mode (defun ivy-prescient-re-builder (query) "Generate an Ivy-formatted regexp list for the given QUERY string. This is for use in `ivy-re-builders-alist'." (setq ivy--subexps 0) (save-match-data (or (mapcar (lambda (regexp) (setq ivy--subexps (max ivy--subexps (regexp-opt-depth regexp))) (cons regexp t)) (prescient-filter-regexps query (if ivy-prescient-retain-classic-highlighting 'all 'with-group))) ;; For some reason, Ivy doesn't seem to like to be given an empty ;; list of regexps. Instead, it wants an empty string. ""))) (defvar ivy-prescient--old-re-builder nil "Previous default value in `ivy-re-builders-alist'.") (defun ivy-prescient-sort-function (c1 c2) "Comparison function that uses prescient.el to sort candidates. This is for use in `ivy-sort-functions-alist'. C1 and C2 are arbitrary candidates to be compared; they may be strings or cons cells whose cars are strings." ;; For some reason, Ivy supports candidates that are lists, and just ;; takes their cars. I guess we have to support that too. (when (listp c1) (setq c1 (car c1))) (when (listp c2) (setq c2 (car c2))) (prescient-sort-compare c1 c2)) (defvar ivy-prescient--old-ivy-sort-functions-alist nil "Previous values from `ivy-sort-functions-alist'. When `ivy-prescient-mode' is disabled, all of the elements of this alist are used to update `ivy-sort-functions-alist'.") (defvar ivy-prescient--old-ivy-sort-matches-completion-in-region-function nil "Previous value for sorting `completion-in-region' results. This is the value that was associated to `ivy-completion-in-region' in `ivy-sort-matches-functions-alist'.") (defvar ivy-prescient--old-initial-inputs-alist nil "Previous value of `ivy-initial-inputs-alist'.") (defun ivy-prescient--wrap-action (action) "Wrap an action for use in `ivy-read'. ACTION is the original action, a function. Return a new function that also invokes `prescient-remember'." (if (or (bound-and-true-p ivy-marked-candidates) (not (eq 'ivy-prescient-sort-function (let ((sort (ivy-state-sort ivy-last)) (coll (ivy-state-collection ivy-last))) (cond ((functionp sort) sort) ((or sort (eq coll #'read-file-name-internal)) (ivy--sort-function coll))))))) action (let ((dir ivy--directory)) (lambda (x) (let ((cand x)) (when (listp cand) (setq cand (car x))) (when dir (setq cand (file-relative-name cand dir))) (prescient-remember cand)) (funcall action x))))) (defun ivy-prescient--remember-directory (success) "Remember the directory we just entered when SUCCESS." (when success (prescient-remember (file-name-as-directory (file-name-nondirectory (directory-file-name ivy--directory))))) success) (defun ivy-prescient--enable-sort-commands (args) "Enable sorting of `ivy-prescient-sort-commands'. If the `:caller' in ARGS should be sorted according to `ivy-prescient-sort-commands', then `:sort' is enabled even if wasn't in the call to `ivy-read'." (when (or (and (symbolp ivy-prescient-sort-commands) ivy-prescient-sort-commands) (and (listp ivy-prescient-sort-commands) (if (eq (car ivy-prescient-sort-commands) :not) (not (memq (plist-get args :caller) (cdr ivy-prescient-sort-commands))) (memq (plist-get args :caller) ivy-prescient-sort-commands)))) ;; Put it at the end so it doesn't override a value that's already ;; there. (I.e., you can explicitly pass `:sort nil' to disable ;; sorting.) (setq args (append args '(:sort t)))) args) ;;;###autoload (define-minor-mode ivy-prescient-mode "Minor mode to use prescient.el in Ivy menus." :global t :group 'prescient (if ivy-prescient-mode (progn (ivy-prescient-mode -1) (setq ivy-prescient-mode t) (when ivy-prescient-enable-filtering (cl-shiftf ivy-prescient--old-re-builder (alist-get t ivy-re-builders-alist) #'ivy-prescient-re-builder) (cl-shiftf ivy-prescient--old-initial-inputs-alist ivy-initial-inputs-alist nil)) (when ivy-prescient-enable-sorting ;; Not sure if `map-apply' (note that `map-do' is not ;; available before Emacs 26) handles mutation of alist ;; during iteration. Use `map-keys' plus `dolist' to be ;; safe. (dolist (caller (map-keys ivy-sort-functions-alist)) (when (memq (alist-get caller ivy-sort-functions-alist) '(ivy-string< ivy-sort-file-function-default)) ;; Use `ignore' to silence byte-compiler. We only use ;; the setter, not the getter. (ignore (cl-shiftf (alist-get caller ivy-prescient--old-ivy-sort-functions-alist) (alist-get caller ivy-sort-functions-alist) #'ivy-prescient-sort-function)))) (cl-shiftf ivy-prescient--old-ivy-sort-matches-completion-in-region-function (alist-get #'ivy-completion-in-region ivy-sort-matches-functions-alist) nil) (advice-add #'ivy-read :filter-args #'ivy-prescient--enable-sort-commands) (advice-add #'ivy--directory-enter :filter-return #'ivy-prescient--remember-directory) (advice-add #'ivy--get-action :filter-return #'ivy-prescient--wrap-action))) (when (equal (alist-get t ivy-re-builders-alist) #'ivy-prescient-re-builder) (setf (alist-get t ivy-re-builders-alist) ivy-prescient--old-re-builder)) (map-apply (lambda (caller function) (when (equal (alist-get caller ivy-sort-functions-alist) #'ivy-prescient-sort-function) (setf (alist-get caller ivy-sort-functions-alist) function))) ivy-prescient--old-ivy-sort-functions-alist) (setq ivy-prescient--old-ivy-sort-functions-alist nil) (unless (alist-get #'ivy-completion-in-region ivy-sort-matches-functions-alist) (setf (alist-get #'ivy-completion-in-region ivy-sort-matches-functions-alist) ivy-prescient--old-ivy-sort-matches-completion-in-region-function)) (dolist (pair (reverse ivy-prescient--old-initial-inputs-alist)) (unless (alist-get (car pair) ivy-initial-inputs-alist) (setf (alist-get (car pair) ivy-initial-inputs-alist) (cdr pair)))) (advice-remove #'ivy-read #'ivy-prescient--enable-sort-commands) (advice-remove #'ivy--directory-enter #'ivy-prescient--remember-directory) (advice-remove #'ivy--get-action #'ivy-prescient--wrap-action))) ;;;; Closing remarks (provide 'ivy-prescient) ;;; ivy-prescient.el ends here ;; Local Variables: ;; outline-regexp: ";;;;* " ;; checkdoc-verb-check-experimental-flag: nil ;; End: