branch: externals/ivy
commit 22ec74344697efd2d1d1c624338c21764dc862ce
Author: Basil L. Contovounesios <[email protected]>
Commit: Basil L. Contovounesios <[email protected]>

    Make counsel-M-x collection customizable
    
    * ivy.el (ivy--flx-available-p): Remove, replacing all uses...
    (ivy--features, ivy--feature-p): ...with these new definitions
    instead.  These are like 'features' and 'require', respectively,
    except they hit the filesystem at most once, caching the
    result (#1246).
    
    * counsel.el (counsel-M-x-transformer): Don't show key binding when
    suggest-key-bindings is nil, as per read-extended-command-1.  Don't
    assume key-description result is mutable.
    (counsel-M-x-collection): New user option (#2870).
    (counsel--M-x-make-predicate): Rename...
    (counsel--M-x-predicate): ...to this.  Accept all completion table
    types.  Treat unknown read-extended-command-predicate value as a
    match, for more graceful degradation.
    (counsel--M-x-externs): Remove, breaking functionality out...
    (counsel--M-x-collection, counsel--amx-collection)
    (counsel--smex-collection): ...into these new functions.  Obey
    counsel-M-x-collection.  Use ivy--feature-p to avoid hitting
    filesystem up to twice (#1246, #3019).  Return amx-cache as-is now
    that counsel--M-x-predicate supports it.
    (counsel--M-x-externs-predicate): Remove, replacing with
    counsel--M-x-predicate across the board for consistency.
    (counsel--M-x-prompt): Take prefix as an argument and refactor.
    (counsel-M-x-action): Bind prefix-arg around command-execute instead
    of setting it before, as per execute-extended-command.  Obey
    ivy-current-prefix-arg (#1639).  Break amx/smex ranking out...
    (counsel--M-x-extern-rank): ...into this new function obeying
    counsel-M-x-collection.
    (counsel-M-x): Revert #893 which set this-command to last-command,
    as this is misleading during minibuffer-setup-hook (#891, #2874).
    
    * ivy-test.el (counsel--M-x-prompt): New test.
    
    Closes #891.
    Fixes #1246.
    Fixes #1639.
    Fixes #2870.
    Fixes #2874.
---
 counsel.el  | 204 ++++++++++++++++++++++++++++++++++--------------------------
 ivy-test.el |  18 ++++++
 ivy.el      |  21 ++++---
 3 files changed, 146 insertions(+), 97 deletions(-)

diff --git a/counsel.el b/counsel.el
index b94ee1180a..67d0b49412 100644
--- a/counsel.el
+++ b/counsel.el
@@ -846,6 +846,7 @@ With prefix arg MODE a query for the symbol help mode is 
offered."
 ;;;; `counsel-M-x'
 
 (defface counsel-key-binding
+  ;; Default Emacs 28 `help-key-binding' doesn't look great in parentheses.
   '((t :inherit font-lock-keyword-face))
   "Face used by `counsel-M-x' for key bindings."
   :group 'ivy-faces)
@@ -874,7 +875,7 @@ With prefix arg MODE a query for the symbol help mode is 
offered."
     (concat cmd
             (when (and (symbolp alias) counsel-alias-expand)
               (format " (%s)" alias))
-            (when key
+            (when (and key suggest-key-bindings)
               ;; Prefer `<f2>' over `C-x 6' where applicable
               (let ((i (cl-search [?\C-x ?6] key)))
                 (when i
@@ -884,73 +885,109 @@ With prefix arg MODE a query for the symbol help mode is 
offered."
                                  (lookup-key map dup))
                       (setq key dup)))))
               (setq key (key-description key))
-              (put-text-property 0 (length key) 'face 'counsel-key-binding key)
+              (setq key (propertize key 'face 'counsel-key-binding))
               (format " (%s)" key)))))
 
-(defvar amx-initialized)
-(defvar amx-cache)
-(declare-function amx-initialize "ext:amx")
-(declare-function amx-detect-new-commands "ext:amx")
-(declare-function amx-update "ext:amx")
-(declare-function amx-rank "ext:amx")
-(defvar smex-initialized-p)
-(defvar smex-ido-cache)
-(declare-function smex-initialize "ext:smex")
-(declare-function smex-detect-new-commands "ext:smex")
-(declare-function smex-update "ext:smex")
-(declare-function smex-rank "ext:smex")
-
-(defun counsel--M-x-externs ()
-  "Return `counsel-M-x' candidates from external packages.
-The return value is a list of strings.  The currently supported
-packages are, in order of precedence, `amx' and `smex'."
-  (cond ((require 'amx nil t)
-         (unless amx-initialized
-           (amx-initialize))
-         (when (amx-detect-new-commands)
-           (amx-update))
-         (mapcar (lambda (entry)
-                   (symbol-name (car entry)))
-                 amx-cache))
-        ((require 'smex nil t)
-         (unless smex-initialized-p
-           (smex-initialize))
-         (when (smex-detect-new-commands)
-           (smex-update))
-         smex-ido-cache)))
-
-(defun counsel--M-x-externs-predicate (cand)
-  "Return non-nil if `counsel-M-x' should complete CAND.
-CAND is a string returned by `counsel--M-x-externs'."
-  (not (function-get (intern cand) 'no-counsel-M-x)))
-
-(defun counsel--M-x-make-predicate ()
+(defcustom counsel-M-x-collection 'auto
+  "Where to source `counsel-M-x' completion candidates from.
+`obarray' - Use the default M-x collection built into Emacs.
+`amx'     - Source candidates from the external `amx' package.
+`smex'    - Source candidates from the external `smex' package.
+`auto'    - Automatically detect one of the previous options,
+            falling back to `obarray'.  This is the default.
+The value can alternatively be a function of no arguments
+that returns a completion table suitable for `ivy-read'."
+  :package-version '(counsel . "0.16.0")
+  :type '(choice (const :tag "Built-in" obarray)
+                 (const :tag "Amx package" amx)
+                 (const :tag "Smex package" smex)
+                 (const :tag "Auto-detect" auto)
+                 (function :tag "Custom function")))
+
+(defun counsel--M-x-collection ()
+  "Return a completion table obeying `counsel-M-x-collection'."
+  (let ((src counsel-M-x-collection))
+    (cond ((eq src 'obarray) obarray)
+          ((eq src 'auto)
+           (cond ((ivy--feature-p 'amx)
+                  (counsel--amx-collection))
+                 ((ivy--feature-p 'smex)
+                  (counsel--smex-collection))
+                 (obarray)))
+          ((eq src 'amx)
+           (unless (ivy--feature-p 'amx)
+             (user-error "Package `amx' not installed"))
+           (counsel--amx-collection))
+          ((eq src 'smex)
+           (unless (ivy--feature-p 'smex)
+             (user-error "Package `smex' not installed"))
+           (counsel--smex-collection))
+          ((functionp src) (funcall src))
+          ((user-error "Unknown `counsel-M-x-collection': %S" src)))))
+
+(defun counsel--M-x-extern-rank (cmd)
+  "Tell external `counsel-M-x-collection' that CMD was selected."
+  (declare-function amx-rank "ext:amx")
+  (declare-function smex-rank "ext:smex")
+  (let ((src counsel-M-x-collection))
+    (cond ((and (memq src '(auto amx))
+                (bound-and-true-p amx-initialized))
+           (amx-rank cmd))
+          ((and (memq src '(auto smex))
+                (bound-and-true-p smex-initialized-p))
+           (smex-rank cmd)))))
+
+(defun counsel--amx-collection ()
+  "Return `counsel-M-x' candidates from the `amx' package."
+  (declare-function amx-detect-new-commands "ext:amx")
+  (declare-function amx-initialize "ext:amx")
+  (declare-function amx-update "ext:amx")
+  (defvar amx-cache)
+  (defvar amx-initialized)
+  (unless amx-initialized
+    (amx-initialize))
+  (when (amx-detect-new-commands)
+    (amx-update))
+  amx-cache)
+
+(defun counsel--smex-collection ()
+  "Return `counsel-M-x' candidates from the `smex' package."
+  (declare-function smex-detect-new-commands "ext:smex")
+  (declare-function smex-initialize "ext:smex")
+  (declare-function smex-update "ext:smex")
+  (defvar smex-ido-cache)
+  (defvar smex-initialized-p)
+  (unless smex-initialized-p
+    (smex-initialize))
+  (when (smex-detect-new-commands)
+    (smex-update))
+  smex-ido-cache)
+
+(defun counsel--M-x-predicate ()
   "Return a predicate for `counsel-M-x' in the current buffer."
-  (defvar read-extended-command-predicate)
   (let ((buf (current-buffer)))
-    (lambda (sym)
-      (and (commandp sym)
-           (not (function-get sym 'byte-obsolete-info))
-           (not (function-get sym 'no-counsel-M-x))
-           (cond ((not (bound-and-true-p read-extended-command-predicate)))
-                 ((functionp read-extended-command-predicate)
-                  (condition-case-unless-debug err
-                      (funcall read-extended-command-predicate sym buf)
-                    (error (message "read-extended-command-predicate: %s: %s"
-                                    sym (error-message-string err))))))))))
-
-(defun counsel--M-x-prompt ()
-  "String for `M-x' plus the string representation of `current-prefix-arg'."
-  (concat (cond ((null current-prefix-arg)
-                 nil)
-                ((eq current-prefix-arg '-)
-                 "- ")
-                ((integerp current-prefix-arg)
-                 (format "%d " current-prefix-arg))
-                ((= (car current-prefix-arg) 4)
-                 "C-u ")
-                (t
-                 (format "%d " (car current-prefix-arg))))
+    ;; Should work with all completion table types.
+    (lambda (key &optional _val)
+      (when (consp key) (setq key (car key)))
+      (when (stringp key) (setq key (intern key)))
+      (and (commandp key)
+           (not (function-get key 'byte-obsolete-info))
+           (not (function-get key 'no-counsel-M-x))
+           ;; New in Emacs 28.
+           (let ((pred (bound-and-true-p read-extended-command-predicate)))
+             (or (not (functionp pred))
+                 (condition-case-unless-debug err
+                     (funcall pred key buf)
+                   (error (message "read-extended-command-predicate: %s: %s"
+                                   key (error-message-string err))))))))))
+
+(defun counsel--M-x-prompt (arg)
+  "Prompt for `counsel-M-x' preceded by a printed form of prefix ARG."
+  (concat (cond ((null arg) ())
+                ((eq (car-safe arg) 4) "C-u ")
+                ((or (eq arg '-)
+                     (integerp (or (car-safe arg) arg)))
+                 (format "%s " (or (car-safe arg) arg))))
           "M-x "))
 
 (defvar counsel-M-x-history nil
@@ -960,38 +997,29 @@ CAND is a string returned by `counsel--M-x-externs'."
   "Execute CMD."
   (setq cmd (intern
              (subst-char-in-string ?\s ?- (string-remove-prefix "^" cmd))))
-  (cond ((bound-and-true-p amx-initialized)
-         (amx-rank cmd))
-        ((bound-and-true-p smex-initialized-p)
-         (smex-rank cmd)))
-  (setq prefix-arg current-prefix-arg)
+  (counsel--M-x-extern-rank cmd)
+  ;; As per `execute-extended-command'.
   (setq this-command cmd)
   (setq real-this-command cmd)
-  (command-execute cmd 'record))
+  (let ((prefix-arg (or ivy-current-prefix-arg current-prefix-arg)))
+    (command-execute cmd 'record)))
 
 ;;;###autoload
 (defun counsel-M-x (&optional initial-input)
   "Ivy version of `execute-extended-command'.
 Optional INITIAL-INPUT is the initial input in the minibuffer.
-This function integrates with either the `amx' or `smex' package
-when available, in that order of precedence."
+This function integrates with either the `amx' or `smex' package when
+available, in that order of precedence; see `counsel-M-x-collection'."
   (interactive)
-  ;; When `counsel-M-x' returns, `last-command' would be set to
-  ;; `counsel-M-x' because :action hasn't been invoked yet.
-  ;; Instead, preserve the old value of `this-command'.
-  (setq this-command last-command)
-  (setq real-this-command real-last-command)
-  (let ((externs (counsel--M-x-externs)))
-    (ivy-read (counsel--M-x-prompt) (or externs obarray)
-              :predicate (if externs
-                             #'counsel--M-x-externs-predicate
-                           (counsel--M-x-make-predicate))
-              :require-match t
-              :history 'counsel-M-x-history
-              :action #'counsel-M-x-action
-              :keymap counsel-describe-map
-              :initial-input initial-input
-              :caller 'counsel-M-x)))
+  (ivy-read (counsel--M-x-prompt current-prefix-arg)
+            (counsel--M-x-collection)
+            :predicate (counsel--M-x-predicate)
+            :require-match t
+            :history 'counsel-M-x-history
+            :action #'counsel-M-x-action
+            :keymap counsel-describe-map
+            :initial-input initial-input
+            :caller 'counsel-M-x))
 
 (ivy-configure 'counsel-M-x
   :initial-input "^"
diff --git a/ivy-test.el b/ivy-test.el
index 12bfeb0112..c8a984b4bd 100644
--- a/ivy-test.el
+++ b/ivy-test.el
@@ -519,6 +519,24 @@ Since `execute-kbd-macro' doesn't pick up a let-bound 
`default-directory'.")
                   '(("foo")) t)
                  "^(?!.*foo)")))
 
+(ert-deftest counsel--M-x-prompt ()
+  "Test `counsel--M-x-prompt' behavior."
+  (should (equal (counsel--M-x-prompt ()) "M-x "))
+  (should (equal (counsel--M-x-prompt t) "M-x "))
+  (should (equal (counsel--M-x-prompt '(())) "M-x "))
+  (should (equal (counsel--M-x-prompt '(t)) "M-x "))
+  (should (equal (counsel--M-x-prompt -1) "-1 M-x "))
+  (should (equal (counsel--M-x-prompt '(-1)) "-1 M-x "))
+  (should (equal (counsel--M-x-prompt 0) "0 M-x "))
+  (should (equal (counsel--M-x-prompt '(0)) "0 M-x "))
+  (should (equal (counsel--M-x-prompt 1) "1 M-x "))
+  (should (equal (counsel--M-x-prompt '(1)) "1 M-x "))
+  (should (equal (counsel--M-x-prompt 4) "4 M-x "))
+  (should (equal (counsel--M-x-prompt '(4)) "C-u M-x "))
+  (should (equal (counsel--M-x-prompt 16) "16 M-x "))
+  (should (equal (counsel--M-x-prompt '(16)) "16 M-x "))
+  (should (equal (counsel--M-x-prompt '-) "- M-x ")))
+
 (defmacro ivy--string-buffer (text &rest body)
   "Test helper that wraps TEXT in a temp buffer while running BODY."
   `(with-temp-buffer
diff --git a/ivy.el b/ivy.el
index 329732b29b..efe565a965 100644
--- a/ivy.el
+++ b/ivy.el
@@ -3876,12 +3876,15 @@ The alist VAL is a sorting function with the signature 
of
   (let ((default-directory ivy--directory))
     (sort (copy-sequence candidates) #'file-newer-than-file-p)))
 
-(defvar ivy--flx-available-p)
-(defun ivy--flx-available-p ()
-  "Try to load package `flx' once; return non-nil on success."
-  (if (boundp 'ivy--flx-available-p)
-      ivy--flx-available-p
-    (setq ivy--flx-available-p (require 'flx nil t))))
+(defvar ivy--features ()
+  "Alist mapping features to their `require' result.")
+
+(defun ivy--feature-p (feature)
+  "Try to load FEATURE once; return non-nil on success."
+  (cdr (or (assq feature ivy--features)
+           (let ((entry (cons feature (require feature nil t))))
+             (push entry ivy--features)
+             entry))))
 
 (defun ivy--sort (name candidates)
   "Re-sort candidates by NAME.
@@ -3890,7 +3893,7 @@ All CANDIDATES are assumed to match NAME."
     (cond ((setq fun (ivy-alist-setting ivy-sort-matches-functions-alist))
            (funcall fun name candidates))
           ((and (eq ivy--regex-function #'ivy--regex-fuzzy)
-                (ivy--flx-available-p))
+                (ivy--feature-p 'flx))
            (ivy--flx-sort name candidates))
           (t
            candidates))))
@@ -3990,7 +3993,7 @@ CANDS are the current candidates."
                      ((and (not empty)
                            (not (eq caller 'swiper))
                            (not (and (eq ivy--regex-function 
#'ivy--regex-fuzzy)
-                                     (ivy--flx-available-p)
+                                     (ivy--feature-p 'flx)
                                      ;; Limit to configured number of 
candidates
                                      (null (nthcdr ivy-flx-limit cands))))
                            ;; If there was a preselected candidate, don't try 
to
@@ -4263,7 +4266,7 @@ with the extended highlighting of 
`ivy-format-function-line'."
 (defun ivy--highlight-fuzzy (str)
   "Highlight STR, using the fuzzy method."
   (if (and (eq (ivy-alist-setting ivy-re-builders-alist) #'ivy--regex-fuzzy)
-           (ivy--flx-available-p))
+           (ivy--feature-p 'flx))
       (let ((flx-name (string-remove-prefix "^" ivy-text)))
         (ivy--flx-propertize
          (cons (flx-score str flx-name ivy--flx-cache) str)))

Reply via email to