diff --git a/.travis.yml b/.travis.yml index cd1dec8..6361de7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,11 +9,14 @@ - EMACS_VERSION=emacs-25.1 - EMACS_VERSION=emacs-25.2 - EMACS_VERSION=emacs-25.3 - - EMACS_VERSION=emacs-git-snapshot + - EMACS_VERSION=emacs-26-pretest + # For some reason emac-git-snapshot hangs forever + # - EMACS_VERSION=emacs-git-snapshot - EMACS_VERSION=remacs-git-snapshot matrix: allow_failures: + - env: EMACS_VERSION=emacs-26-pretest - env: EMACS_VERSION=remacs-git-snapshot - env: EMACS_VERSION=emacs-git-snapshot @@ -39,4 +42,6 @@ script: - make compile - - make test + # Don't send redundant coverage info + - UNDERCOVER_CONFIG='((:send-report nil))' make test + - make test-with-flx diff --git a/Cask b/Cask index 1447226..c521b0e 100644 --- a/Cask +++ b/Cask @@ -1,5 +1,3 @@ -;; -*- mode: emacs-lisp -*- - (source gnu) (source melpa) @@ -10,8 +8,6 @@ (development (depends-on "flx-ido") - (depends-on "with-simulated-input" - :git "https://github.com/DarwinAwardWinner/with-simulated-input.git" - :files ("*.el")) - (depends-on "buttercup") + (depends-on "with-simulated-input" "2.2") + (depends-on "buttercup" "1.9") (depends-on "undercover")) diff --git a/ChangeLog b/ChangeLog index f96aff7..249ae48 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,20 @@ +2018-04-24 Ryan C. Thompson + + * ido-completing-read+.el (ido-cr+-update-dynamic-collection): + Prevent unintended re-sorting of the completion list after dynamic + updates. + +2018-04-21 Ryan C. Thompson + + * ido-completing-read+.el (ido-cr+-update-dynamic-collection): Fix + accidental creation of circular lists, which could lead to errors + or indefinite hangs (#151) + +2018-02-16 Ryan C. Thompson + + * ido-completing-read+.el: Add a temporary fix for a conflict with + flx-ido in Emacs 26. + 2017-08-19 Ryan C. Thompson * ido-completing-read+.el (ido-cr+-maybe-update-blacklist): Fix a diff --git a/Makefile b/Makefile index 794301a..4d42e9b 100644 --- a/Makefile +++ b/Makefile @@ -6,10 +6,17 @@ all: test -# We run clean-elc because undercover.el doesn't support elc files -test: - cask clean-elc - cask exec buttercup -L . +# We run clean-elc first because undercover.el doesn't support elc +# files. We run the tests first without loading flx-ido, and then with +# it. We only send the coverage report when running the full test +# suite. +test: clean + cask exec buttercup -L . tests + +test-with-flx: clean + cask exec buttercup -l tests/setup-undercover.el -L . tests tests-with-flx-ido + +all-tests: test test-with-flx compile: $(ELC_FILES) diff --git a/README.md b/README.md index 08650a5..3d63cc3 100644 --- a/README.md +++ b/README.md @@ -44,18 +44,18 @@ ## Ido itself ## First, enable `ido-mode` and `ido-everywhere`. - - (ido-mode 1) - (ido-everywhere 1) - +```elisp +(ido-mode 1) +(ido-everywhere 1) +``` ## ido-completing-read+ (this package) ## Install this package [from MELPA](http://melpa.org/#/ido-completing-read+) and then turn on `ido-ubiquitous-mode`: - - (require 'ido-completing-read+) - (ido-ubiquitous-mode 1) - +```elisp +(require 'ido-completing-read+) +(ido-ubiquitous-mode 1) +``` ## Smex ## Smex allows you to use ido for completion of commands in M-x, with @@ -63,25 +63,25 @@ list. First install the [smex](https://github.com/nonsequitur/smex) package, then follow the directions to load it and replace your normal M-x key-binding with smex: - - (require 'smex) ; Not needed if you use package.el - (smex-initialize) ; Can be omitted. This might cause a (minimal) delay - ; when Smex is auto-initialized on its first run. - (global-set-key (kbd "M-x") 'smex) - (global-set-key (kbd "M-X") 'smex-major-mode-commands) - ;; This is your old M-x. - (global-set-key (kbd "C-c C-c M-x") 'execute-extended-command) - +```elisp +(require 'smex) ; Not needed if you use package.el +(smex-initialize) ; Can be omitted. This might cause a (minimal) delay + ; when Smex is auto-initialized on its first run. +(global-set-key (kbd "M-x") 'smex) +(global-set-key (kbd "M-X") 'smex-major-mode-commands) +;; This is your old M-x. +(global-set-key (kbd "C-c C-c M-x") 'execute-extended-command) +``` (These directions are the same ones given in the smex README file.) ## ido-yes-or-no ## If you want to use ido for yes-or-no questions, even though it's massive overkill, install my [ido-yes-or-no package from MELPA](http://melpa.org/#/ido-yes-or-no), and then enable the mode: - - (require 'ido-yes-or-no) - (ido-yes-or-no-mode 1) - +```elisp +(require 'ido-yes-or-no) +(ido-yes-or-no-mode 1) +``` ## ido for `describe-face` and certain other commands ## Some commands, such as `describe-face`, use `completing-read-multiple` @@ -92,10 +92,10 @@ the [crm-custom](https://github.com/DarwinAwardWinner/crm-custom) package [from MELPA](http://melpa.org/#/crm-custom), then enable the mode: - - (require 'crm-custom) - (crm-custom-mode 1) - +```elisp +(require 'crm-custom) +(crm-custom-mode 1) +``` Make sure to read and understand the FAQ entry below about the empty entry at the beginning of the completion list before using this mode, or using it will likely be very confusing. @@ -117,10 +117,10 @@ called `icomplete-mode` that integrates with standard emacs completion and adds some ido-like behavior. It is built in to emacs, so no installation is necessary. Just load the file and enable the mode: - - (require 'icomplete) - (icomplete-mode 1) - +```elisp +(require 'icomplete) +(icomplete-mode 1) +``` # Frequently asked questions # ## How does ido-ubiquitous-mode decide when to replace `completing-read`?
Why don't some commands use ido completion? ## diff --git a/ido-completing-read+.el b/ido-completing-read+.el index c9f80d0..3d4a36f 100644 --- a/ido-completing-read+.el +++ b/ido-completing-read+.el @@ -5,7 +5,7 @@ ;; Filename: ido-completing-read+.el ;; Author: Ryan Thompson ;; Created: Sat Apr 4 13:41:20 2015 (-0700) -;; Version: 4.7 +;; Version: 4.10 ;; Package-Requires: ((emacs "24.4") (cl-lib "0.5") (s "0.1") (memoize "1.1")) ;; URL: https://github.com/DarwinAwardWinner/ido-completing-read-plus ;; Keywords: ido, completion, convenience @@ -77,7 +77,7 @@ ;; ;;; Code: -(defconst ido-completing-read+-version "4.7" +(defconst ido-completing-read+-version "4.10" "Currently running version of ido-completing-read+. Note that when you update ido-completing-read+, this variable may @@ -182,6 +182,9 @@ This allows ido-cr+ to update the set of completion candidates dynamically.") +(defvar ido-cr+-last-dynamic-update-text nil + "The value of `ido-text' last time a dynamic update occurred.") + (defvar ido-cr+-dynamic-update-idle-time 0.25 "Time to wait before updating dynamic completion list.") @@ -206,13 +209,13 @@ These are used for falling back to `completing-read-default'.") -(defvar ido-cr+-all-completions-memoized nil +(defvar ido-cr+-all-completions-memoized 'all-completions "Memoized version of `all-completions'. During completion with dynamic collection, this variable is set to a memoized copy of `all-completions'.") -(defvar ido-cr+-all-prefix-completions-memoized nil +(defvar ido-cr+-all-prefix-completions-memoized 'ido-cr+-all-prefix-completions "Memoized version of `ido-cr+-all-prefix-completions'. During completion with dynamic collection, this variable is set @@ -511,6 +514,7 @@ (when (and (not ido-cr+-assume-static-collection) (functionp collection)) collection)) + (ido-cr+-last-dynamic-update-text nil) ;; Only memoize if the collection is dynamic. (ido-cr+-all-prefix-completions-memoized (if ido-cr+-dynamic-collection @@ -828,15 +832,15 @@ (cl-loop for i from 0 upto (length string) append (funcall - (or ido-cr+-all-completions-memoized - 'all-completions) + ido-cr+-all-completions-memoized (s-left i string) collection predicate) into completion-list finally return (delete-dups completion-list))) - ;; Otherwise, just call `all-completions' on the empty string to - ;; get every possible completions for a static COLLECTION. + ;; If COLLECTION is not dynamic, then just call `all-completions' + ;; on the empty string, which will already return every possible + ;; completion. (t (all-completions "" collection predicate)))) @@ -873,79 +877,111 @@ (nreverse filtered-collection) filtered-collection))) +(defun ido-cr+-cyclicp (x) + "Returns non-nill if X is a list containing a circular reference." + (cl-loop + for tortoise on x + for hare on (cdr x) by #'cddr + thereis (eq tortoise hare))) + +(defun ido-cr+-maybe-chop (items elem) + "Like `ido-chop', but a no-op if ELEM is not in ITEMS. + +Normal `ido-chop' hangs infinitely in this case." + (cl-loop + with new-tail = () + for remaining on items + for next = (car remaining) + if (equal next elem) + return (nconc remaining new-tail) + else collect next into new-tail + finally return items)) + (defun ido-cr+-update-dynamic-collection () "Update the set of completions for a dynamic collection. This has no effect unless `ido-cr+-dynamic-collection' is non-nil." - (when (and (ido-cr+-active) - ido-cr+-dynamic-collection) - (let ((orig-ido-cur-list ido-cur-list)) - (condition-case-unless-debug err - (let* ((ido-text - (buffer-substring-no-properties (minibuffer-prompt-end) - ido-eoinput)) - (predicate (nth 2 ido-cr+-orig-completing-read-args)) - (first-match (car ido-matches)) - (strings-to-check - (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 - for string in strings-to-check - nconc - (funcall - (or ido-cr+-all-prefix-completions-memoized - 'ido-cr+-all-prefix-completions) - string ido-cr+-dynamic-collection predicate) - into result - finally return result))) - ;; Put the previous first match back at the front if possible - (when (and new-completions - first-match - (member first-match new-completions)) - (setq new-completions - (ido-chop new-completions first-match))) - (when (not (equal new-completions ido-cur-list)) - (when (and (bound-and-true-p flx-ido-mode) - (functionp 'flx-ido-reset)) - ;; Reset flx-ido since the set of completions has changed - (funcall 'flx-ido-reset)) - (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))) - (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) - ;; Rebuild the completion display unless ido is already planning - ;; to do it anyway - (unless ido-cr+-exhibit-pending - (ido-tidy) - (ido-exhibit)))) - (error - (display-warning 'ido-cr+ - (format - "Disabling dynamic update due to error: %S" - err)) - ;; Reset any variables that might have been modified during - ;; the failed update - (setq ido-cur-list orig-ido-cur-list) - ;; Prevent any further attempts at dynamic updating - (setq ido-cr+-dynamic-collection nil))))) + (when (and ido-cr+-dynamic-collection + (ido-cr+-active)) + ;; (cl-assert (not (ido-cr+-cyclicp ido-cur-list))) + (let ((orig-ido-cur-list ido-cur-list) + (ido-text + (buffer-substring-no-properties (minibuffer-prompt-end) + ido-eoinput))) + ;; If current `ido-text' is equal to or a prefix of the previous + ;; one, a dynamic update is not needed. + (when (or (null ido-cr+-last-dynamic-update-text) + (not (s-prefix? ido-text ido-cr+-last-dynamic-update-text))) + (ido-cr+--debug-message "Doing a dynamic update because `ido-text' changed from %S to %S" + ido-cr+-last-dynamic-update-text ido-text) + (setq ido-cr+-last-dynamic-update-text ido-text) + (condition-case-unless-debug err + (let* ((predicate (nth 2 ido-cr+-orig-completing-read-args)) + (first-match (car ido-matches)) + (strings-to-check + (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 + for string in strings-to-check + append + (funcall + ido-cr+-all-prefix-completions-memoized + string ido-cr+-dynamic-collection predicate) + into result + finally return result))) + ;; (cl-assert (not (ido-cr+-cyclicp new-completions))) + (if (equal new-completions ido-cur-list) + (ido-cr+--debug-message "Skipping dynamic update because the completion list did not change.") + (when (and (bound-and-true-p flx-ido-mode) + (functionp 'flx-ido-reset)) + ;; Reset flx-ido since the set of completions has changed + (funcall 'flx-ido-reset)) + (setq ido-cur-list (delete-dups (append ido-cur-list new-completions))) + (when ido-cr+-active-restrictions + (setq ido-cur-list (ido-cr+-apply-restrictions + ido-cur-list + ido-cr+-active-restrictions))) + (ido-cr+--debug-message + "Updated completion candidates for dynamic collection. `ido-cur-list' now has %s elements" + ido-text (length ido-cur-list)) + ;; Recompute matches with new completions + (let ((ido-rescan t)) + (ido-set-matches)) + (setq ido-rescan nil) + ;; Put the pre-update first match (if any) back in + ;; front + (when (and first-match + (not (equal first-match (car ido-matches))) + (member first-match ido-matches)) + (ido-cr+--debug-message "Restoring first match %S after dynamic update" first-match) + (setq ido-matches (ido-chop ido-matches first-match))) + ;; Rebuild the completion display unless ido is already planning + ;; to do it anyway + (unless ido-cr+-exhibit-pending + (ido-tidy) + (let ((ido-rescan nil)) + (ido-exhibit))))) + (error + (display-warning 'ido-cr+ + (format + "Disabling dynamic update due to error: %S" + err)) + ;; Reset any variables that might have been modified during + ;; the failed update + (setq ido-cur-list orig-ido-cur-list) + ;; Prevent any further attempts at dynamic updating + (setq ido-cr+-dynamic-collection nil)))))) ;; Always cancel an active timer when this function is called. (when ido-cr+-dynamic-update-timer (cancel-timer ido-cr+-dynamic-update-timer) @@ -959,6 +995,7 @@ (when ido-cr+-dynamic-update-timer (cancel-timer ido-cr+-dynamic-update-timer) (setq ido-cr+-dynamic-update-timer nil)) + (cl-assert (not (ido-cr+-cyclicp ido-cur-list))) (if (<= (length ido-matches) 1) ;; If we've narrowed it down to zero or one matches, update ;; immediately. @@ -987,6 +1024,8 @@ (apply oldfun args)) ;; Update `ido-eoinput' (setq ido-eoinput (point-max)) + ;; Clear this var to force an update + (setq ido-cr+-last-dynamic-update-text nil) ;; Now do update (ido-cr+-update-dynamic-collection)) ;; After maybe updating the dynamic collection, if there's still diff --git a/ido-ubiquitous.el b/ido-ubiquitous.el index cca2da9..c9647b6 100644 --- a/ido-ubiquitous.el +++ b/ido-ubiquitous.el @@ -4,11 +4,11 @@ ;; Author: Ryan C. Thompson ;; URL: https://github.com/DarwinAwardWinner/ido-ubiquitous -;; Version: 4.7 +;; Version: 4.10 ;; Created: 2011-09-01 ;; Keywords: convenience, completion, ido ;; EmacsWiki: InteractivelyDoThings -;; Package-Requires: ((ido-completing-read+ "4.7") (cl-lib "0.5")) +;; Package-Requires: ((ido-completing-read+ "4.10") (cl-lib "0.5")) ;; Filename: ido-ubiquitous.el ;; This file is NOT part of GNU Emacs. @@ -39,7 +39,7 @@ ;; ;;; Code: -(defconst ido-ubiquitous-version "4.7" +(defconst ido-ubiquitous-version "4.10" "Currently running version of ido-ubiquitous. Note that when you update ido-ubiquitous, this variable may not diff --git a/tests/setup-undercover.el b/tests/setup-undercover.el new file mode 100644 index 0000000..a4685c5 --- /dev/null +++ b/tests/setup-undercover.el @@ -0,0 +1,3 @@ +(require 'undercover) +(undercover "*.el" + (:exclude "test-*.el")) diff --git a/tests/test-ido-completing-read+.el b/tests/test-ido-completing-read+.el index fd77dfd..88ee30e 100644 --- a/tests/test-ido-completing-read+.el +++ b/tests/test-ido-completing-read+.el @@ -1,11 +1,6 @@ ;;; -*- lexical-binding: t -*- -(require 'undercover) -(undercover "*.el" - (:exclude "test-*.el")) - (require 'ido) -(require 'flx-ido) (require 'minibuf-eldef) (require 'ido-completing-read+) (require 'buttercup) @@ -113,6 +108,17 @@ (when (eq (car vars) 'quote) (setq vars (eval vars))) `(mapc #'unshadow-var ',vars)) + +(defmacro with-temp-info-buffer (&rest body) + "Create a temporary info buffer and exeluate BODY forms there." + (declare (indent 0)) + `(let ((temp-bufname (generate-new-buffer-name " *temp-info*"))) + (unwind-protect + (save-excursion + (info nil (generate-new-buffer-name " *temp-info*")) + ,@body) + (when (get-buffer temp-bufname) + (kill-buffer temp-bufname))))) (describe "Within the `ido-completing-read+' package" @@ -133,7 +139,6 @@ ido-confirm-unique-completion ido-enable-flex-matching ido-enable-dot-prefix - flx-ido-mode (minibuffer-electric-default-mode t))) ;; Suppress all messages during tests @@ -565,109 +570,7 @@ (with-simulated-input "eee C-SPC aaa C-u C-SPC ccc C-u C-SPC ggg RET" (ido-completing-read+ "Pick: " (collection-as-function collection) nil t nil nil (car collection))) - :to-equal "bbb-eee-ggg"))) - - (describe "with flx-ido-mode" - (before-each - (flx-ido-mode 1) - (flx-ido-reset)) - - (it "should allow selection of dynamically-added completions" - (expect - (with-simulated-input "hello-w RET" - (ido-completing-read+ "Say something: " my-dynamic-collection)) - :to-equal "hello-world") - (expect 'ido-cr+-update-dynamic-collection - :to-have-been-called)) - - (it "should allow ido flex-matching of dynamically-added completions" - (expect - (with-simulated-input "hello-ld RET" - (ido-completing-read+ "Say something: " my-dynamic-collection)) - :to-equal - "hello-world") - (expect 'ido-cr+-update-dynamic-collection - :to-have-been-called)) - - (it "should do a dynamic update when pressing TAB" - (expect - (with-simulated-input "h TAB -ld RET" - (ido-completing-read+ "Say something: " my-dynamic-collection)) - :to-equal - "hello-world") - (expect 'ido-cr+-update-dynamic-collection - :to-have-been-called)) - - (it "should do a dynamic update when idle" - (expect - (with-simulated-input - '("h" - (wsi-simulate-idle-time (1+ ido-cr+-dynamic-update-idle-time)) - "-ld RET") - (ido-completing-read+ "Say something: " my-dynamic-collection)) - :to-equal - "hello-world") - (expect 'ido-cr+-update-dynamic-collection - :to-have-been-called)) - - (it "should do a dynamic update when there is only one match remaining" - (expect - (with-simulated-input "hell-ld RET" - (ido-completing-read+ "Say something: " my-dynamic-collection)) - :to-equal - "hello-world") - (expect 'ido-cr+-update-dynamic-collection - :to-have-been-called)) - - (it "should not exit with a unique match if new matches are dynamically added" - (expect - (with-simulated-input '("hell TAB -ld RET") - (ido-completing-read+ "Say something: " my-dynamic-collection)) - :to-equal - "hello-world") - (expect 'ido-cr+-update-dynamic-collection - :to-have-been-called)) - - (it "should exit with a match that is still unique after dynamic updating" - (expect - (with-simulated-input '("helic TAB") - (ido-completing-read+ "Say something: " my-dynamic-collection)) - :to-equal - "helicopter") - (expect 'ido-cr+-update-dynamic-collection - :to-have-been-called)) - - (it "should respect `ido-restrict-to-matches' when doing dynamic updates" - (let ((collection - (list "aaa-ddd-ggg" "aaa-eee-ggg" "aaa-fff-ggg" - "bbb-ddd-ggg" "bbb-eee-ggg" "bbb-fff-ggg" - "ccc-ddd-ggg" "ccc-eee-ggg" "ccc-fff-ggg" - "aaa-ddd-hhh" "aaa-eee-hhh" "aaa-fff-hhh" - "bbb-ddd-hhh" "bbb-eee-hhh" "bbb-fff-hhh" - "ccc-ddd-hhh" "ccc-eee-hhh" "ccc-fff-hhh" - "aaa-ddd-iii" "aaa-eee-iii" "aaa-fff-iii" - "bbb-ddd-iii" "bbb-eee-iii" "bbb-fff-iii" - "ccc-ddd-iii" "ccc-eee-iii" "ccc-fff-iii"))) - ;; Test the internal function - (expect - (ido-cr+-apply-restrictions - collection - (list (cons nil "bbb") - (cons nil "eee"))) - :to-equal '("bbb-eee-ggg" "bbb-eee-hhh" "bbb-eee-iii")) - - ;; First verify it without a dynamic collection - (expect - (with-simulated-input "eee C-SPC bbb C-SPC ggg RET" - (ido-completing-read+ - "Pick: " collection nil t nil nil (car collection))) - :to-equal "bbb-eee-ggg") - ;; Now test the same with a dynamic collection - (expect - (with-simulated-input "eee C-SPC bbb C-SPC ggg RET" - (ido-completing-read+ - "Pick: " (collection-as-function collection) nil t nil nil (car collection))) - :to-equal "bbb-eee-ggg"))))) + :to-equal "bbb-eee-ggg")))) (describe "with unusual inputs" (it "should accept a COLLECTION of symbols" @@ -1011,10 +914,62 @@ (expect (with-simulated-input "RET" (ido-completing-read+ "Prompt: " 'def-nil-collection nil t)) - :to-equal "blue")))))) - -;; (defun ido-cr+-run-all-tests () -;; (interactive) -;; (ert "^ido-cr\\+-")) + :to-equal "blue")))) + + ;; Test is currently disabled pending additional information + (xit "should not hang or error when deleting characters in `org-refile' (issue #152)" + (expect + (progn + (ido-ubiquitous-mode 1) + (save-excursion + (with-temp-buffer + (org-mode) + (insert (s-trim " + * Heading 1 + ** Subheading 1.1 + ** Subheading 1.2 + ** Subheading 1.3 + * Heading 2 + * Heading 3 + ")) + (goto-char (point-max)) + ;; TODO Figure out what else needs to be set up to call + ;; `org-refile' + (with-simulated-input + "Heading DEL DEL DEL DEL DEL RET" + (command-execute 'org-refile))))) + :not :to-throw))) + + (describe "regressions should not occur for" + (it "issue #151: should not hang or error when cycling matches in `Info-menu'" + (expect + (progn + (ido-ubiquitous-mode 1) + (with-temp-info-buffer + (with-simulated-input + '("emacs" + (ido-next-match) + (wsi-simulate-idle-time 5) + (ido-next-match) + (wsi-simulate-idle-time 5) + (ido-next-match) + (wsi-simulate-idle-time 5) + (ido-next-match) + (wsi-simulate-idle-time 5) + "RET") + (command-execute 'Info-menu)))) + :not :to-throw)) + + (it "issue #153: should preserve the selected item when doing a deferred dynamic update" + (expect + (with-simulated-input + '("Emacs" + (ido-next-match) + (wsi-simulate-idle-time 5) + "RET") + (ido-completing-read+ + "Choose: " + (collection-as-function '("Emacs" "Emacs A" "Emacs B" "Emacs C")))) + :to-equal "Emacs A")))) ;;; test-ido-completing-read+.el ends here diff --git a/tests-with-flx-ido/test-ido-completing-read+-with-flx-ido.el b/tests-with-flx-ido/test-ido-completing-read+-with-flx-ido.el new file mode 100644 index 0000000..08ff6ee --- /dev/null +++ b/tests-with-flx-ido/test-ido-completing-read+-with-flx-ido.el @@ -0,0 +1,277 @@ +;;; -*- lexical-binding: t -*- + +;; This file contains tests specifically for ido-cr+ interoperating +;; with flx-ido. These tests were split out from the main test file, +;; which unfortunately means that they brought a lot of the +;; scaffolding from the main test file with them. This might get +;; cleaned up at a later date. Putting these in a separate file is +;; necessary so that the main test suite can be both with and without +;; flx-ido loaded. + +(require 'ido) +(require 'flx-ido) +(require 'minibuf-eldef) +(require 'ido-completing-read+) +(require 'buttercup) +(require 'cl-lib) +(require 'with-simulated-input) + +(defun collection-as-function (collection) + "Return a function equivalent to COLLECTION. + +The returned function will work equivalently to COLLECTION when +passed to `all-completions' and `try-completion'." + (completion-table-dynamic (lambda (string) (all-completions string collection)))) + +(defun shadow-var (var &optional temp-value) + "Shadow the value of VAR. + +This will push the current value of VAR to VAR's +`shadowed-values' property, and then set it to TEMP-VALUE. To +reverse this process, call `unshadow-var' on VAR. Vars can +be shadowed recursively, and must be unshadowed once for each +shadowing in order to restore the original value. You can think +of shadowing as dynamic binding with `let', but with manual +control over when bindings start and end. + +If VAR is a Custom variable (see `custom-variable-p'), it will be +set using `customize-set-variable', and if TEMP-VALUE is nil it +will be replaces with VAR's standard value. + + Other variables will be set with `set-default', and a TEMP-VALUE + of nil will not be treated specially. + +`shadow-var' only works on variables declared as special (i.e. +using `defvar' or similar). It will not work on lexically bound +variables." + (unless (special-variable-p var) + (error "Cannot shadow lexical var `%s'" var)) + (let* ((use-custom (custom-variable-p var)) + (setter (if use-custom 'customize-set-variable 'set-default)) + (temp-value (or temp-value + (and use-custom + (eval (car (get var 'standard-value))))))) + ;; Push the current value on the stack + (push (symbol-value var) (get var 'shadowed-values)) + (funcall setter var temp-value))) + +(defun var-shadowed-p (var) + "Return non-nil if VAR is shadowed by `shadow-var'." + ;; We don't actually want to return that list if it's non-nil. + (and (get var 'shadowed-values) t)) + +(defun unshadow-var (var) + "Reverse the last call to `shadow-var' on VAR." + (if (var-shadowed-p var) + (let* ((use-custom (custom-variable-p var)) + (setter (if use-custom 'customize-set-variable 'set-default)) + (value (pop (get var 'shadowed-values)))) + (funcall setter var value)) + (error "Var is not shadowed: %s" var))) + +(defun fully-unshadow-var (var) + "Reverse *all* calls to `shadow-var' on VAR." + (when (var-shadowed-p var) + (let* ((use-custom (custom-variable-p var)) + (setter (if use-custom 'customize-set-variable 'set-default)) + (value (car (last (get var 'shadowed-values))))) + (put var 'shadowed-values nil) + (funcall setter var value)))) + +(defun fully-unshadow-all-vars (&optional vars) + "Reverse *all* calls to `shadow-var' on VARS. + +If VARS is nil, unshadow *all* variables." + (if vars + (mapc #'fully-unshadow-var vars) + (mapatoms #'fully-unshadow-var)) + nil) + +(defmacro shadow-vars (varlist) + "Shadow a list of vars with new values. + +VARLIST describes the variables to be shadowed with the same +syntax as `let'. + +See `shadow-var'." + (declare (indent 0)) + (cl-loop + with var = nil + with value = nil + for binding in varlist + if (symbolp binding) + do (setq var binding + value nil) + else + do (setq var (car binding) + value (cadr binding)) + collect `(shadow-var ',var ,value) into exprs + finally return `(progn ,@exprs))) + +(defmacro unshadow-vars (vars) + "Un-shadow a list of VARS. + +This is a macro for consistency with `shadow-vars', but it will +also accept a quoted list for the sake of convenience." + (declare (indent 0)) + (when (eq (car vars) 'quote) + (setq vars (eval vars))) + `(mapc #'unshadow-var ',vars)) + +(describe "Within the `ido-completing-read+' package" + + ;; Reset all of these variables to their standard values before each + ;; test, saving the previous values for later restoration. + (before-each + (shadow-vars + ((ido-mode t) + (ido-ubiquitous-mode t) + (ido-cr+-debug-mode t) + ido-cr+-auto-update-blacklist + ido-cr+-fallback-function + ido-cr+-max-items + ido-cr+-function-blacklist + ido-cr+-function-whitelist + ido-cr+-nil-def-alternate-behavior-list + ido-cr+-replace-completely + ido-confirm-unique-completion + ido-enable-flex-matching + ido-enable-dot-prefix + flx-ido-mode + (minibuffer-electric-default-mode t))) + + ;; Suppress all messages during tests + (spy-on 'message)) + + ;; Restore the saved values after each test + (after-each + (fully-unshadow-all-vars)) + + (describe "the `ido-completing-read+' function" + + (describe "with dynamic collections" + (before-all + (setq my-dynamic-collection + (completion-table-dynamic + (lambda (text) + (cond + ;; Sub-completions for "hello" + ((s-prefix-p "hello" text) + '("hello" "hello-world" "hello-everyone" "hello-universe")) + ;; Sub-completions for "goodbye" + ((s-prefix-p "goodbye" text) + '("goodbye" "goodbye-world" "goodbye-everyone" "goodbye-universe")) + ;; General completions + (t + '("hello" "goodbye" "helicopter" "helium" "goodness" "goodwill"))))))) + (after-all + (setq my-dynamic-collection nil)) + (before-each + (setq ido-enable-flex-matching t + ido-confirm-unique-completion nil) + (spy-on 'ido-cr+-update-dynamic-collection + :and-call-through)) + + (describe "with flx-ido-mode" + (before-each + (flx-ido-mode 1) + (flx-ido-reset)) + + (it "should allow selection of dynamically-added completions" + (expect + (with-simulated-input "hello-w RET" + (ido-completing-read+ "Say something: " my-dynamic-collection)) + :to-equal "hello-world") + (expect 'ido-cr+-update-dynamic-collection + :to-have-been-called)) + + (it "should allow ido flex-matching of dynamically-added completions" + (expect + (with-simulated-input "hello-ld RET" + (ido-completing-read+ "Say something: " my-dynamic-collection)) + :to-equal + "hello-world") + (expect 'ido-cr+-update-dynamic-collection + :to-have-been-called)) + + (it "should do a dynamic update when pressing TAB" + (expect + (with-simulated-input "h TAB -ld RET" + (ido-completing-read+ "Say something: " my-dynamic-collection)) + :to-equal + "hello-world") + (expect 'ido-cr+-update-dynamic-collection + :to-have-been-called)) + + (it "should do a dynamic update when idle" + (expect + (with-simulated-input + '("h" + (wsi-simulate-idle-time (1+ ido-cr+-dynamic-update-idle-time)) + "-ld RET") + (ido-completing-read+ "Say something: " my-dynamic-collection)) + :to-equal + "hello-world") + (expect 'ido-cr+-update-dynamic-collection + :to-have-been-called)) + + (it "should do a dynamic update when there is only one match remaining" + (expect + (with-simulated-input "hell-ld RET" + (ido-completing-read+ "Say something: " my-dynamic-collection)) + :to-equal + "hello-world") + (expect 'ido-cr+-update-dynamic-collection + :to-have-been-called)) + + (it "should not exit with a unique match if new matches are dynamically added" + (expect + (with-simulated-input '("hell TAB -ld RET") + (ido-completing-read+ "Say something: " my-dynamic-collection)) + :to-equal + "hello-world") + (expect 'ido-cr+-update-dynamic-collection + :to-have-been-called)) + + (it "should exit with a match that is still unique after dynamic updating" + (expect + (with-simulated-input '("helic TAB") + (ido-completing-read+ "Say something: " my-dynamic-collection)) + :to-equal + "helicopter") + (expect 'ido-cr+-update-dynamic-collection + :to-have-been-called)) + + (it "should respect `ido-restrict-to-matches' when doing dynamic updates" + (let ((collection + (list "aaa-ddd-ggg" "aaa-eee-ggg" "aaa-fff-ggg" + "bbb-ddd-ggg" "bbb-eee-ggg" "bbb-fff-ggg" + "ccc-ddd-ggg" "ccc-eee-ggg" "ccc-fff-ggg" + "aaa-ddd-hhh" "aaa-eee-hhh" "aaa-fff-hhh" + "bbb-ddd-hhh" "bbb-eee-hhh" "bbb-fff-hhh" + "ccc-ddd-hhh" "ccc-eee-hhh" "ccc-fff-hhh" + "aaa-ddd-iii" "aaa-eee-iii" "aaa-fff-iii" + "bbb-ddd-iii" "bbb-eee-iii" "bbb-fff-iii" + "ccc-ddd-iii" "ccc-eee-iii" "ccc-fff-iii"))) + ;; Test the internal function + (expect + (ido-cr+-apply-restrictions + collection + (list (cons nil "bbb") + (cons nil "eee"))) + :to-equal '("bbb-eee-ggg" "bbb-eee-hhh" "bbb-eee-iii")) + + ;; First verify it without a dynamic collection + (expect + (with-simulated-input "eee C-SPC bbb C-SPC ggg RET" + (ido-completing-read+ + "Pick: " collection nil t nil nil (car collection))) + :to-equal "bbb-eee-ggg") + ;; Now test the same with a dynamic collection + (expect + (with-simulated-input "eee C-SPC bbb C-SPC ggg RET" + (ido-completing-read+ + "Pick: " (collection-as-function collection) nil t nil nil (car collection))) + :to-equal "bbb-eee-ggg"))))))) + +;;; test-ido-completing-read+.el ends here