浏览代码

Add "ido-cr+-nil-def-alternate-behavior-list" custom option

This replaces the internal variable "ido-cr+-no-default-action", which
now has no effect and is marked obsolete.

Fixes #127.
Ryan C. Thompson 8 年之前
父节点
当前提交
650377b544
共有 2 个文件被更改,包括 185 次插入139 次删除
  1. 115 80
      ido-completing-read+.el
  2. 70 59
      tests/test-ido-completing-read+.el

+ 115 - 80
ido-completing-read+.el

@@ -189,30 +189,10 @@ other times, since those two functions are in `pre-command-hook'
 and `post-command-hook' respectively. In particular, this will
 and `post-command-hook' respectively. In particular, this will
 generally be nil while running an idle timer.")
 generally be nil while running an idle timer.")
 
 
-(defvar ido-cr+-no-default-action 'prepend-empty-string
-  "Controls the behavior of ido-cr+ when DEF is nil and REQUIRE-MATCH is non-nil.
-
-Possible values:
-
-- `prepend-empty-string': The empty string will be added to the
-  front of COLLECTION, making it the default. This is the
-  standard behavior since it mimics the semantics of
-  `completing-read-default'.
-
-- `append-empty-string': The empty string will be added to the
-  end of COLLECTION, thus keeping the original default while
-  making the empty string available as a completion.
-
-- `nil': No action will be taken.
-
-- Any other value: The value will be interpreted as a 1-argument
-  function, which will receive the current collection as its
-  argument and return the collection with any necessary
-  modifications applied.
-
-This is not meant to be set permanently, but rather let-bound
-before calling `ido-completing-read+' under controlled
-circumstances.")
+(make-obsolete-variable
+ 'ido-cr+-no-default-action
+ " This variable no longer has any effect. Customize `ido-cr+-nil-def-alternate-behavior-list' instead."
+ "4.2")
 
 
 (defvar ido-cr+-orig-completing-read-args nil
 (defvar ido-cr+-orig-completing-read-args nil
   "Original arguments passed to `ido-completing-read+'.
   "Original arguments passed to `ido-completing-read+'.
@@ -322,6 +302,43 @@ precedence over whitelisting."
   :type '(repeat (choice (symbol :tag "Function or command name")
   :type '(repeat (choice (symbol :tag "Function or command name")
                          (string :tag "Regexp"))))
                          (string :tag "Regexp"))))
 
 
+(defcustom ido-cr+-nil-def-alternate-behavior-list
+  '(
+    ;; https://github.com/DarwinAwardWinner/ido-ubiquitous/issues/127#issuecomment-318954232
+    "ebal-"
+    )
+  "Functions & commands with alternate behavior when DEF is nil.
+
+This variable has the same format as
+`ido-cr+-function-blacklist'. When `ido-completing-read+` is
+called through `completing-read' by/with any command, function,
+or collection matched by entries in this list, it will behave
+differently when DEF is nil. Instead of using the empty string as
+the default value, it will use the first element of COLLECTION.
+
+This is needed for optimal compatibility with commands written
+under the assumption that REQUIRE-MATCH means that a match is
+required."
+  :group 'ido-completing-read-plus
+  :type '(repeat (choice (symbol :tag "Function or command name")
+                         (string :tag "Regexp"))))
+
+(defvaralias 'ido-cr+-nil-def-wall-of-shame 'ido-cr+-nil-def-alternate-behavior-list
+  "Functions and commands whose authors need to read the docstring for `completing-read'.
+
+Many functions that call `completing-read' are written with the
+assumption that the setting the REQUIRE-MATCH argument of
+`completing-read' to t means it is required to return a match.
+While that would make logical sense, it's wrong. the docstring
+for `completing-read' describes the correct behavior.
+
+> If the input is null, ‘completing-read’ returns DEF, or the
+> first element of the list of default values, or an empty string
+> if DEF is nil, regardless of the value of REQUIRE-MATCH.
+
+This can be avoided by passing an element of COLLECTION as DEF
+instead of leaving it as nil.")
+
 ;;;###autoload
 ;;;###autoload
 (defcustom ido-cr+-replace-completely nil
 (defcustom ido-cr+-replace-completely nil
   "If non-nil, replace `ido-completeing-read' completely with ido-cr+.
   "If non-nil, replace `ido-completeing-read' completely with ido-cr+.
@@ -363,7 +380,8 @@ https://github.com/DarwinAwardWinner/ido-ubiquitous/issues"
   "Return non-nil if FUN matches an entry in FUN-LIST.
   "Return non-nil if FUN matches an entry in FUN-LIST.
 
 
 This is used to check for matches to `ido-cr+-function-blacklist'
 This is used to check for matches to `ido-cr+-function-blacklist'
-and `ido-cr+-function-whitelist'.
+and `ido-cr+-function-whitelist'. Read those docstrings to see
+how the matching is done.
 
 
 This is declared as macro only in order to extract the variable
 This is declared as macro only in order to extract the variable
 name used for the second argument so it can be used in a debug
 name used for the second argument so it can be used in a debug
@@ -453,7 +471,9 @@ completion for them."
                     (functionp collection))
                     (functionp collection))
            collection))
            collection))
         ;; If the whitelist is empty, everything is whitelisted
         ;; If the whitelist is empty, everything is whitelisted
-        (whitelisted (not ido-cr+-function-whitelist)))
+        (whitelisted (not ido-cr+-function-whitelist))
+        ;; If non-nil, we need alternate nil DEF handling
+        (alt-nil-def nil))
     (condition-case sig
     (condition-case sig
         (progn
         (progn
           ;; Check a bunch of fallback conditions
           ;; Check a bunch of fallback conditions
@@ -480,7 +500,18 @@ completion for them."
                (if (symbolp collection)
                (if (symbolp collection)
                    (format "Collection function `%S' is whitelisted" collection)
                    (format "Collection function `%S' is whitelisted" collection)
                  "Collection function is whitelisted"))
                  "Collection function is whitelisted"))
-              (setq whitelisted t)))
+              (setq whitelisted t))
+            ;; nil DEF list
+            (when (and
+                   require-match (null def)
+                   (ido-cr+-function-is-in-list
+                    collection
+                    ido-cr+-nil-def-alternate-behavior-list))
+              (ido-cr+--debug-message
+               (if (symbolp collection)
+                   (format "Using alternate nil DEF handling for collection function `%S'" collection)
+                 "Using alternate nil DEF handling for collection function"))
+              (setq alt-nil-def t)))
 
 
           ;; Expand all currently-known completions.
           ;; Expand all currently-known completions.
           (setq collection
           (setq collection
@@ -504,22 +535,28 @@ completion for them."
           ;; If called from `completing-read', check for
           ;; If called from `completing-read', check for
           ;; black/white-listed commands/callers
           ;; black/white-listed commands/callers
           (when (ido-cr+--called-from-completing-read)
           (when (ido-cr+--called-from-completing-read)
-            ;; Check calling command
-            (when (ido-cr+-function-is-blacklisted this-command)
-              (signal 'ido-cr+-fallback
-                      (list "calling command `%S' is blacklisted" this-command)))
-            (when (and (not whitelisted)
-                       (ido-cr+-function-is-whitelisted this-command))
-              (ido-cr+--debug-message "Command `%S' is whitelisted" this-command)
-              (setq whitelisted t))
-            ;; Also need to check `ido-cr+-current-command'
-            (when (ido-cr+-function-is-blacklisted ido-cr+-current-command)
-              (signal 'ido-cr+-fallback
-                      (list "calling command `%S' is blacklisted" ido-cr+-current-command)))
-            (when (and (not whitelisted)
-                       (ido-cr+-function-is-whitelisted ido-cr+-current-command))
-              (ido-cr+--debug-message "Command `%S' is whitelisted" ido-cr+-current-command)
-              (setq whitelisted t))
+            ;; Check calling command and `ido-cr+-current-command'
+            (cl-loop
+             for cmd in (list this-command ido-cr+-current-command)
+
+             if (ido-cr+-function-is-blacklisted cmd)
+             do (signal 'ido-cr+-fallback
+                        (list "calling command `%S' is blacklisted" cmd))
+
+             if (and (not whitelisted)
+                     (ido-cr+-function-is-whitelisted cmd))
+             do (progn
+                  (ido-cr+--debug-message "Command `%S' is whitelisted" cmd)
+                  (setq whitelisted t))
+
+             if (and
+                 require-match (null def) (not alt-nil-def)
+                 (ido-cr+-function-is-in-list
+                  cmd ido-cr+-nil-def-alternate-behavior-list))
+             do (progn
+                  (ido-cr+--debug-message
+                   "Using alternate nil DEF handling for command `%S'" cmd)
+                  (setq alt-nil-def t)))
 
 
             ;; Check every function in the call stack starting after
             ;; Check every function in the call stack starting after
             ;; `completing-read' until to the first
             ;; `completing-read' until to the first
@@ -534,11 +571,13 @@ completion for them."
                      while (not (memq (indirect-function caller)
                      while (not (memq (indirect-function caller)
                                       '(internal--funcall-interactively
                                       '(internal--funcall-interactively
                                         (indirect-function 'call-interactively))))
                                         (indirect-function 'call-interactively))))
+
                      if (ido-cr+-function-is-blacklisted caller)
                      if (ido-cr+-function-is-blacklisted caller)
                      do (signal 'ido-cr+-fallback
                      do (signal 'ido-cr+-fallback
                                 (list (if (symbolp caller)
                                 (list (if (symbolp caller)
                                           (format "calling function `%S' is blacklisted" caller)
                                           (format "calling function `%S' is blacklisted" caller)
                                         "a calling function is blacklisted")))
                                         "a calling function is blacklisted")))
+
                      if (and (not whitelisted)
                      if (and (not whitelisted)
                              (ido-cr+-function-is-whitelisted caller))
                              (ido-cr+-function-is-whitelisted caller))
                      do (progn
                      do (progn
@@ -546,50 +585,47 @@ completion for them."
                            (if (symbolp caller)
                            (if (symbolp caller)
                                (format "Calling function `%S' is whitelisted" caller)
                                (format "Calling function `%S' is whitelisted" caller)
                              "A calling function is whitelisted"))
                              "A calling function is whitelisted"))
-                          (setq whitelisted t))))
+                          (setq whitelisted t))
+
+                     if (and require-match (null def) (not alt-nil-def)
+                             (ido-cr+-function-is-in-list
+                              caller ido-cr+-nil-def-alternate-behavior-list))
+                     do (progn
+                          (ido-cr+--debug-message
+                           (if (symbolp caller)
+                               (format "Using alternate nil DEF handling for calling function `%S'" caller)
+                             "Using alternate nil DEF handling for a calling function"))
+                          (setq alt-nil-def t))))
 
 
           (unless whitelisted
           (unless whitelisted
             (signal 'ido-cr+-fallback
             (signal 'ido-cr+-fallback
                     (list "no functions or commands matched the whitelist for this call")))
                     (list "no functions or commands matched the whitelist for this call")))
 
 
+          (when (and require-match (null def))
+            ;; Replace nil with "" for DEF if match is required, unless
+            ;; alternate nil DEF handling is enabled
+            (if alt-nil-def
+                (ido-cr+--debug-message
+                 "Leaving the default at nil because alternate nil DEF handling is enabled.")
+              (ido-cr+--debug-message
+               "Adding \"\" as the default completion since no default was provided.")
+              (setq def (list ""))))
+
           ;; In ido, the semantics of "default" are simply "put it at
           ;; In ido, the semantics of "default" are simply "put it at
-          ;; the front of the list". Furthermore, ido has certain
-          ;; issues with a non-nil DEF arg. Specifically, it can't
-          ;; handle list defaults or providing both DEF and
-          ;; INITIAL-INPUT. So, just pre-process the collection to put
-          ;; the default(s) at the front and then set DEF to nil in
-          ;; the call to ido to avoid these issues.
+          ;; the front of the list". Furthermore, ido can't handle a
+          ;; list of defaults, nor can it handle both DEF and
+          ;; INITIAL-INPUT being non-nil. So, just pre-process the
+          ;; collection to put the default(s) at the front and then
+          ;; set DEF to nil in the call to ido to avoid these issues.
           (unless (listp def)
           (unless (listp def)
             ;; Ensure DEF is a list
             ;; Ensure DEF is a list
             (setq def (list def)))
             (setq def (list def)))
           (when def
           (when def
             ;; Ensure DEF are strings
             ;; Ensure DEF are strings
             (setq def (mapcar (apply-partially #'format "%s") def))
             (setq def (mapcar (apply-partially #'format "%s") def))
-            (setq collection (append def (cl-set-difference collection def
-                                                            :test #'equal))
-                  def nil))
-
-          ;; If DEF was nil and REQUIRE-MATCH was non-nil, then we need to
-          ;; add the empty string as the first option, because RET on
-          ;; an empty input needs to return "". (Or possibly we need
-          ;; to take some other action based on the value of
-          ;; `ido-cr+-no-default-action'.)
-          (when (and require-match
-                     ido-cr+-no-default-action
-                     (not (ido-cr+-default-was-provided)))
-            (cl-case ido-cr+-no-default-action
-              (nil
-               ;; Take no action
-               t)
-              (prepend-empty-string
-               (ido-cr+--debug-message "Adding \"\" as the default completion since no default was provided.")
-               (setq collection (cons "" collection)))
-              (append-empty-string
-               (ido-cr+--debug-message "Adding \"\" as a completion option since no default was provided.")
-               (setq collection (append collection '(""))))
-              (otherwise
-               (ido-cr+--debug-message "Running custom action function since no default was provided.")
-               (setq collection (funcall ido-cr+-no-default-action collection)))))
+            ;; Prepend DEF to COLLECTION and remove duplicates
+            (setq collection (delete-dups (append def collection)))
+                  def nil)
 
 
           ;; Check for a specific bug
           ;; Check for a specific bug
           (when (and ido-enable-dot-prefix
           (when (and ido-enable-dot-prefix
@@ -602,11 +638,10 @@ completion for them."
           ;; version check after this bug is fixed:
           ;; version check after this bug is fixed:
           ;; https://debbugs.gnu.org/cgi/bugreport.cgi?bug=27807
           ;; https://debbugs.gnu.org/cgi/bugreport.cgi?bug=27807
           (when (consp initial-input)
           (when (consp initial-input)
-            (setq initial-input
-                  (cons (car initial-input)
-                        ;; `completing-read' uses 0-based index while
-                        ;; `read-from-minibuffer' uses 1-based index.
-                        (1+ (cdr initial-input)))))
+            ;; `completing-read' uses 0-based index while
+            ;; `read-from-minibuffer' uses 1-based index.
+            (cl-incf (cdr initial-input)))
+
           ;; Finally ready to do actual ido completion
           ;; Finally ready to do actual ido completion
           (prog1
           (prog1
               (let ((ido-cr+-minibuffer-depth (1+ (minibuffer-depth)))
               (let ((ido-cr+-minibuffer-depth (1+ (minibuffer-depth)))

+ 70 - 59
tests/test-ido-completing-read+.el

@@ -126,9 +126,8 @@ also accept a quoted list for the sake of convenience."
        ido-cr+-max-items
        ido-cr+-max-items
        ido-cr+-function-blacklist
        ido-cr+-function-blacklist
        ido-cr+-function-whitelist
        ido-cr+-function-whitelist
+       ido-cr+-nil-def-alternate-behavior-list
        ido-cr+-replace-completely
        ido-cr+-replace-completely
-       ;; Not a custom var; must specify new value
-       (ido-cr+-no-default-action 'prepend-empty-string)
        ido-confirm-unique-completion
        ido-confirm-unique-completion
        ido-enable-flex-matching)))
        ido-enable-flex-matching)))
 
 
@@ -318,62 +317,7 @@ also accept a quoted list for the sake of convenience."
            (ido-completing-read
            (ido-completing-read
             "Prompt: "
             "Prompt: "
             '("bluebird" "blues" "bluegrass" "blueberry" "yellow ""green") nil t))
             '("bluebird" "blues" "bluegrass" "blueberry" "yellow ""green") nil t))
-         :to-equal "b"))
-
-      (describe "with `ido-cr+-no-default-action'"
-
-        (describe "set to `prepend-empty-string'"
-          (before-each
-            (setq ido-cr+-no-default-action 'prepend-empty-string))
-          (it "should complete the empty string on RET if DEF is nil"
-            (expect
-             (with-simulated-input "RET"
-               (ido-completing-read+ "Prompt: " '("blue" "yellow" "green") nil t))
-             :to-equal ""))
-          (it "should complete DEF on RET if provided"
-            (expect
-             (with-simulated-input "RET"
-               (ido-completing-read+ "Prompt: " '("blue" "yellow" "green") nil t nil nil "green"))
-             :to-equal "green")))
-
-        ;; TODO: Remove this mode?
-        (xdescribe "set to `append-empty-string'"
-          (before-each
-            (setq ido-cr+-no-default-action 'append-empty-string))
-          (it "should complete the first option on RET if DEF is nil"
-            (expect
-             (with-simulated-input "RET"
-               (ido-completing-read+ "Prompt: " '("blue" "yellow" "green") nil t))
-             :to-equal "blue"))
-          (it "should allow exiting with an empty string if DEF is nil"
-            (expect
-             (with-simulated-input "C-j"
-               (ido-completing-read+ "Prompt: " '("blue" "yellow" "green") nil t))
-             :to-equal ""))
-          (it "should complete DEF on RET if provided"
-            (expect
-             (with-simulated-input "RET"
-               (ido-completing-read+ "Prompt: " '("blue" "yellow" "green") nil t nil nil "green"))
-             :to-equal "green")))
-
-        (describe "set to `nil'"
-          (before-each
-            (setq ido-cr+-no-default-action nil))
-          (it "should complete the first option on RET if DEF is nil"
-            (expect
-             (with-simulated-input "RET"
-               (ido-completing-read+ "Prompt: " '("blue" "yellow" "green") nil t))
-             :to-equal "blue"))
-          (it "should not allow exiting with an empty string if DEF is nil"
-            (expect-error
-             (with-simulated-input "C-j"
-               (ido-completing-read+ "Prompt: " '("blue" "yellow" "green") nil t))))
-          (it "should complete DEF on RET if provided"
-            (expect
-             (with-simulated-input "RET"
-               (ido-completing-read+ "Prompt: " '("blue" "yellow" "green") nil t nil nil "green"))
-             :to-equal "green")))
-        ))
+         :to-equal "b")))
 
 
     (describe "with manual fallback shortcuts"
     (describe "with manual fallback shortcuts"
       (it "should not fall back when C-b or C-f is used in the middle of the input"
       (it "should not fall back when C-b or C-f is used in the middle of the input"
@@ -710,7 +654,74 @@ also accept a quoted list for the sake of convenience."
           (expect
           (expect
            (with-simulated-input "g RET"
            (with-simulated-input "g RET"
              (ido-completing-read+ "Prompt: " 'whitelisted-collection))
              (ido-completing-read+ "Prompt: " 'whitelisted-collection))
-           :to-equal "g"))))))
+           :to-equal "g"))))
+
+    (describe "with `ido-cr+-nil-def-alternate-behavior-list'"
+      (before-all
+        (setf (symbol-function 'def-nil-command)
+              (lambda (arg)
+                (interactive
+                 (list
+                  (completing-read "Prompt: " '("blue" "yellow" "green") nil t)))
+                arg)
+              (symbol-function 'def-nil-function)
+              (lambda ()
+                (completing-read "Prompt: " '("blue" "yellow" "green") nil t))
+              (symbol-function 'cmd-that-calls-def-nil-function)
+              (lambda ()
+                (interactive)
+                (funcall 'def-nil-function))
+              (symbol-function 'def-nil-collection)
+              (lambda (string pred action)
+                (complete-with-action action '("blue" "yellow" "green") string pred))))
+      (after-all
+        (setf (symbol-function 'def-nil-command) nil
+              (symbol-function 'def-nil-function) nil
+              (symbol-function 'cmd-that-calls-def-nil-function) nil
+              (symbol-function 'def-nil-collection) nil))
+
+      (describe "when the specified functions are not in the list"
+        (before-each
+          (setq ido-cr+-nil-def-alternate-behavior-list nil))
+
+        (it "should use empty string default in a command"
+          (expect
+           (with-simulated-input "RET"
+             (call-interactively 'def-nil-command))
+           :to-equal ""))
+        (it "should use empty string default in a function"
+          (expect
+           (with-simulated-input "RET"
+             (call-interactively 'cmd-that-calls-def-nil-function))
+           :to-equal ""))
+        (it "should use empty string default for a collection"
+          (expect
+           (with-simulated-input "RET"
+             (ido-completing-read+ "Prompt: " 'def-nil-collection nil t))
+           :to-equal "")))
+
+      (describe "when the specified functions are in the list"
+        (before-each
+          (setq ido-cr+-nil-def-alternate-behavior-list
+                (append '(def-nil-command
+                          def-nil-function
+                          def-nil-collection)
+                        ido-cr+-nil-def-alternate-behavior-list)))
+        (it "should not use empty string default in a command"
+          (expect
+           (with-simulated-input "RET"
+             (call-interactively 'def-nil-command))
+           :to-equal "blue"))
+        (it "should not use empty string default in a function"
+          (expect
+           (with-simulated-input "RET"
+             (call-interactively 'cmd-that-calls-def-nil-function))
+           :to-equal "blue"))
+        (it "should not use empty string default for a collection"
+          (expect
+           (with-simulated-input "RET"
+             (ido-completing-read+ "Prompt: " 'def-nil-collection nil t))
+           :to-equal "blue"))))))
 
 
 ;; (defun ido-cr+-run-all-tests ()
 ;; (defun ido-cr+-run-all-tests ()
 ;;   (interactive)
 ;;   (interactive)