Ver código fonte

Support "ido-restrict-to-matches" for dynamic collections

Fixes #133.
Ryan C. Thompson 7 anos atrás
pai
commit
49e53dbbbd
2 arquivos alterados com 85 adições e 56 exclusões
  1. 67 55
      ido-completing-read+.el
  2. 18 1
      tests/test-ido-completing-read+.el

+ 67 - 55
ido-completing-read+.el

@@ -133,6 +133,7 @@ using it, so the initial value shouldn't matter.")))
 
 (define-ido-internal-var ido-context-switch-command)
 (define-ido-internal-var ido-cur-list)
+(define-ido-internal-var ido-cur-item)
 (define-ido-internal-var ido-require-match)
 
 ;;;###autoload
@@ -213,6 +214,13 @@ to a memoized copy of `all-completions'.")
 During completion with dynamic collection, this variable is set
 to a memoized copy of `ido-cr+-all-prefix-completions'.")
 
+(defvar ido-cr+-active-restrictions nil
+  "List of restrictions in place from `ido-restrict-to-matches'.
+
+Each element is a cons cell of (REMOVEP . TEXT), where REMOVEP is
+the prefix argument to `ido-restrict-to-matches' and TEXT is the
+pattern used to restrict.")
+
 (defgroup ido-completing-read-plus nil
   "Extra features and compatibility for `ido-completing-read'."
   :group 'ido)
@@ -493,6 +501,7 @@ completion for them."
             "")
            (t
             (signal 'wrong-type-argument (list 'stringp initial-input)))))
+         (ido-cr+-active-restrictions nil)
          ;; If collection is a function, save it for later, unless
          ;; instructed not to
          (ido-cr+-dynamic-collection
@@ -840,6 +849,39 @@ not a function, this is equivalent to
    (t
     (all-completions "" collection predicate))))
 
+(defun ido-cr+-apply-restrictions (collection restrictions)
+  "Filter COLLECTION through RESTRICTIONS in sequence.
+
+COLLECTION is a list of strings. RESTRICTIONS is a list of cons
+cells, with the cdr being the restriction text and the car being
+nil to include matches for that text and t to exclude matches for
+that text. The return value is a list of strings that satisfy all
+the restrictions, in the same order as they appeared in
+COLLECTION.
+
+RESTRICTIONS are applied one by one in order, which is important
+because in theory the order can make a difference to the final
+result."
+  (cl-loop
+   with filtered-collection = collection
+   with need-reverse = nil
+   for (removep . text) in restrictions
+   for restriction-matches =
+   (let ((ido-text text)
+         (ido-cur-item (or ido-cur-item 'list)))
+     (ido-set-matches-1 collection t))
+   for filtered-collection =
+   (if removep
+       (seq-difference filtered-collection restriction-matches)
+     (setq need-reverse (not need-reverse))
+     restriction-matches)
+   ;; Each run of `ido-set-matches-1' reverses the order, so reverse
+   ;; it one more time if it had an odd number of reverses
+   finally return
+   (if need-reverse
+       (nreverse filtered-collection)
+     filtered-collection)))
+
 (defun ido-cr+-update-dynamic-collection ()
   "Update the set of completions for a dynamic collection.
 
@@ -851,22 +893,21 @@ This has no effect unless `ido-cr+-dynamic-collection' is non-nil."
                                             ido-eoinput))
            (predicate (nth 2 ido-cr+-orig-completing-read-args))
            (first-match (car ido-matches))
-           (remembered-new-string nil)
            (strings-to-check
-            ;; If `ido-text' is a prefix of `first-match', then we
-            ;; only need to check the latter, because that will
-            ;; implicitly check the former as well.
             (cond
+             ;; If no match, then we only check `ido-text'
              ((null first-match)
               (list ido-text))
+             ;; If `ido-text' is a prefix of `first-match', then we
+             ;; only need to check `first-match'
              ((and first-match
                    (s-prefix? ido-text first-match))
               (list first-match))
+             ;; Otherwise we need to check both
              (t
               (list ido-text first-match))))
            (new-completions
             (cl-loop
-             with checked-strings = '()
              for string in strings-to-check
              nconc
              (funcall
@@ -874,22 +915,18 @@ This has no effect unless `ido-cr+-dynamic-collection' is non-nil."
                   'ido-cr+-all-prefix-completions)
               string ido-cr+-dynamic-collection predicate)
              into result
-             collect string into checked-strings
              finally return result)))
       (when new-completions
-        ;; Merge new completions into `ido-cur-list'
-        (setq
-         ido-cur-list
-         (delete-dups (nconc ido-cur-list new-completions)))
-        ;; Ensure that the currently-selected match is still at the head
-        ;; of the list
-        (let ((current-match (car ido-matches)))
-          (when (and current-match (member current-match ido-cur-list))
-            (setq ido-cur-list (ido-chop ido-cur-list current-match))))
+        (setq ido-cur-list (delete-dups new-completions))
+        (when ido-cr+-active-restrictions
+          (setq ido-cur-list (ido-cr+-apply-restrictions
+                              ido-cur-list
+                              ido-cr+-active-restrictions)))
+        (when (and first-match (member first-match ido-cur-list))
+          (setq ido-cur-list (ido-chop ido-cur-list first-match)))
         (ido-cr+--debug-message
          "Updated completion candidates for dynamic collection because `ido-text' changed to %S. `ido-cur-list' now has %s elements"
          ido-text (length ido-cur-list))
-
         ;; Recompute matches with new completions
         (setq ido-rescan t)
         (ido-set-matches)
@@ -897,48 +934,11 @@ This has no effect unless `ido-cr+-dynamic-collection' is non-nil."
         ;; to do it anyway
         (unless ido-cr+-exhibit-pending
           (ido-tidy)
-          (ido-exhibit)))
-      ;; Add `ido-text' and/or `first-match' to the list of remembered
-      ;; previous update texts. This is used to avoid re-computing
-      ;; completions on previously-seen string prefixes (since those
-      ;; completions have already been added to `ido-cur-list')
-      (cl-loop
-       for new-text in strings-to-check
-       do
-       (cond
-        ;; Common case optimization: if eitehr new element or first
-        ;; element of list is a prefix of the other, just keep the
-        ;; longer one.
-        ((s-prefix? new-text (car ido-cr+-previous-dynamic-update-texts))
-         nil)
-        ((s-prefix? (car ido-cr+-previous-dynamic-update-texts) new-text)
-         (setf (car ido-cr+-previous-dynamic-update-texts) new-text))
-        ;; General case: just prepend it to the list
-        (t
-         (setq remembered-new-string t)
-         (push new-text ido-cr+-previous-dynamic-update-texts))))
-      ;; Remove duplicates and trim the list down to the last 5
-      ;; remembered texts
-      (when remembered-new-string
-        (setq
-         ido-cr+-previous-dynamic-update-texts
-         ;; Elisp doesn't seem to have a "take first N elements"
-         ;; function that returns the entire list if it's shorter than
-         ;; N instead of signaling an error
-         (cl-loop
-          with result = '()
-          with n-taken = 0
-          for item in ido-cr+-previous-dynamic-update-texts
-          if (not (member item result))
-          collect item into result and
-          sum 1 into n-taken
-          if (>= n-taken 5)
-          return result
-          finally return result))))))
+          (ido-exhibit)))))
   ;; Always cancel an active timer when this function is called.
   (when ido-cr+-dynamic-update-timer
     (cancel-timer ido-cr+-dynamic-update-timer)
-    (setq ido-cr+-dynamic-update-timer nil))
+    (setq ido-cr+-dynamic-update-timer nil)))
 
 (defun ido-cr+-schedule-dynamic-collection-update ()
   "Schedule a dynamic collection update for now or in the future."
@@ -983,6 +983,18 @@ This has no effect unless `ido-cr+-dynamic-collection' is non-nil."
   (apply oldfun args))
 (advice-add 'ido-complete :around 'ido-complete@ido-cr+-update-dynamic-collection)
 
+;; When using `ido-restrict-to-matches', we also need to add an
+;; equivalent predicate to the dynamic collection so that
+;; dynamically-added completions are also properly restricted.
+(defun ido-restrict-to-matches@ido-cr+-record-restriction
+    (&optional removep)
+  "Record the restriction criterion for ido-cr+"
+  (ido-cr+--debug-message "Appending restriction %S to `ido-cr+-active-restrictions'"
+                          (cons removep ido-text))
+  (add-to-list 'ido-cr+-active-restrictions (cons removep ido-text) t))
+(advice-add 'ido-restrict-to-matches :before
+            'ido-restrict-to-matches@ido-cr+-record-restriction)
+
 ;; Interoperation with minibuffer-electric-default-mode: only show the
 ;; default when the input is empty and the empty string is the
 ;; selected choice

+ 18 - 1
tests/test-ido-completing-read+.el

@@ -434,7 +434,24 @@ also accept a quoted list for the sake of convenience."
          :to-equal
          "helicopter")
         (expect 'ido-cr+-update-dynamic-collection
-                :to-have-been-called)))
+                :to-have-been-called))
+      (it "should respect `ido-restrict-to-matches' when doing dynamic updates"
+        ;; First verify it without a dynamic collection
+        (expect
+         (with-simulated-input "aa C-SPC b RET"
+           (completing-read
+            "Pick: "
+            '(aaa aab aac bba bbb bbc cca ccb ccc)
+            nil t nil nil "aaa"))
+         :to-equal "aab")
+        ;; Now test the same with a dynamic collection
+        (expect
+         (with-simulated-input "aa C-SPC b RET"
+           (completing-read
+            "Pick: "
+            (collection-as-function '(aaa aab aac bba bbb bbc cca ccb ccc))
+            nil t nil nil "aaa"))
+         :to-equal "aab")))
 
     (describe "with unusual inputs"
       (it "should accept a COLLECTION of symbols"