diff options
| author | Juri Linkov | 2018-01-21 23:45:43 +0200 |
|---|---|---|
| committer | Juri Linkov | 2018-01-21 23:45:43 +0200 |
| commit | afba4ccb8b8c6347a44efd0b9f4d6fb85756f85b (patch) | |
| tree | 42b97dca576f2020dc18a33ccabcc7ff1ddf2c93 | |
| parent | 9ae0e4aa1aee3d7ff2546e34aa83536f72f8c06a (diff) | |
| download | emacs-afba4ccb8b8c6347a44efd0b9f4d6fb85756f85b.tar.gz emacs-afba4ccb8b8c6347a44efd0b9f4d6fb85756f85b.zip | |
New function read-answer (bug#30073)
* lisp/emacs-lisp/map-ynp.el (read-answer): New function.
(read-answer-short): New defcustom.
* lisp/dired.el (dired-delete-file): Use read-answer.
(dired--yes-no-all-quit-help): Remove function.
(dired-delete-help): Remove defconst.
* lisp/subr.el (assoc-delete-all): New function.
| -rw-r--r-- | etc/NEWS | 3 | ||||
| -rw-r--r-- | lisp/dired.el | 41 | ||||
| -rw-r--r-- | lisp/emacs-lisp/map-ynp.el | 122 | ||||
| -rw-r--r-- | lisp/subr.el | 15 | ||||
| -rw-r--r-- | test/lisp/dired-aux-tests.el | 2 | ||||
| -rw-r--r-- | test/lisp/dired-tests.el | 22 |
6 files changed, 160 insertions, 45 deletions
| @@ -240,6 +240,9 @@ file name extensions. | |||
| 240 | ** The ecomplete sorting has changed to a decay-based algorithm. This | 240 | ** The ecomplete sorting has changed to a decay-based algorithm. This |
| 241 | can be controlled by the new `ecomplete-sort-predicate' variable. | 241 | can be controlled by the new `ecomplete-sort-predicate' variable. |
| 242 | 242 | ||
| 243 | ** The new function 'read-answer' accepts either long or short answers | ||
| 244 | depending on the new customizable variable 'read-answer-short'. | ||
| 245 | |||
| 243 | 246 | ||
| 244 | * Changes in Emacs 27.1 on Non-Free Operating Systems | 247 | * Changes in Emacs 27.1 on Non-Free Operating Systems |
| 245 | 248 | ||
diff --git a/lisp/dired.el b/lisp/dired.el index b853d64c563..eebf8362cfc 100644 --- a/lisp/dired.el +++ b/lisp/dired.el | |||
| @@ -2997,37 +2997,6 @@ Any other value means to ask for each directory." | |||
| 2997 | ;; Match anything but `.' and `..'. | 2997 | ;; Match anything but `.' and `..'. |
| 2998 | (defvar dired-re-no-dot "^\\([^.]\\|\\.\\([^.]\\|\\..\\)\\).*") | 2998 | (defvar dired-re-no-dot "^\\([^.]\\|\\.\\([^.]\\|\\..\\)\\).*") |
| 2999 | 2999 | ||
| 3000 | (defconst dired-delete-help | ||
| 3001 | "Type: | ||
| 3002 | `yes' to delete recursively the current directory, | ||
| 3003 | `no' to skip to next, | ||
| 3004 | `all' to delete all remaining directories with no more questions, | ||
| 3005 | `quit' to exit, | ||
| 3006 | `help' to show this help message.") | ||
| 3007 | |||
| 3008 | (defun dired--yes-no-all-quit-help (prompt &optional help-msg) | ||
| 3009 | "Ask a question with valid answers: yes, no, all, quit, help. | ||
| 3010 | PROMPT must end with '? ', for instance, 'Delete it? '. | ||
| 3011 | If optional arg HELP-MSG is non-nil, then is a message to show when | ||
| 3012 | the user answers 'help'. Otherwise, default to `dired-delete-help'." | ||
| 3013 | (let ((valid-answers (list "yes" "no" "all" "quit")) | ||
| 3014 | (answer "") | ||
| 3015 | (input-fn (lambda () | ||
| 3016 | (read-string | ||
| 3017 | (format "%s [yes, no, all, quit, help] " prompt))))) | ||
| 3018 | (setq answer (funcall input-fn)) | ||
| 3019 | (when (string= answer "help") | ||
| 3020 | (with-help-window "*Help*" | ||
| 3021 | (with-current-buffer "*Help*" | ||
| 3022 | (insert (or help-msg dired-delete-help))))) | ||
| 3023 | (while (not (member answer valid-answers)) | ||
| 3024 | (unless (string= answer "help") | ||
| 3025 | (beep) | ||
| 3026 | (message "Please answer `yes' or `no' or `all' or `quit'") | ||
| 3027 | (sleep-for 2)) | ||
| 3028 | (setq answer (funcall input-fn))) | ||
| 3029 | answer)) | ||
| 3030 | |||
| 3031 | ;; Delete file, possibly delete a directory and all its files. | 3000 | ;; Delete file, possibly delete a directory and all its files. |
| 3032 | ;; This function is useful outside of dired. One could change its name | 3001 | ;; This function is useful outside of dired. One could change its name |
| 3033 | ;; to e.g. recursive-delete-file and put it somewhere else. | 3002 | ;; to e.g. recursive-delete-file and put it somewhere else. |
| @@ -3057,11 +3026,17 @@ TRASH non-nil means to trash the file instead of deleting, provided | |||
| 3057 | "trash" | 3026 | "trash" |
| 3058 | "delete") | 3027 | "delete") |
| 3059 | (dired-make-relative file)))) | 3028 | (dired-make-relative file)))) |
| 3060 | (pcase (dired--yes-no-all-quit-help prompt) ; Prompt user. | 3029 | (pcase (read-answer |
| 3030 | prompt | ||
| 3031 | '(("yes" ?y "delete recursively the current directory") | ||
| 3032 | ("no" ?n "skip to next") | ||
| 3033 | ("all" ?! "delete all remaining directories with no more questions") | ||
| 3034 | ("quit" ?q "exit"))) | ||
| 3061 | ('"all" (setq recursive 'always dired-recursive-deletes recursive)) | 3035 | ('"all" (setq recursive 'always dired-recursive-deletes recursive)) |
| 3062 | ('"yes" (if (eq recursive 'top) (setq recursive 'always))) | 3036 | ('"yes" (if (eq recursive 'top) (setq recursive 'always))) |
| 3063 | ('"no" (setq recursive nil)) | 3037 | ('"no" (setq recursive nil)) |
| 3064 | ('"quit" (keyboard-quit))))) | 3038 | ('"quit" (keyboard-quit)) |
| 3039 | (_ (keyboard-quit))))) ; catch all unknown answers | ||
| 3065 | (setq recursive nil)) ; Empty dir or recursive is nil. | 3040 | (setq recursive nil)) ; Empty dir or recursive is nil. |
| 3066 | (delete-directory file recursive trash)))) | 3041 | (delete-directory file recursive trash)))) |
| 3067 | 3042 | ||
diff --git a/lisp/emacs-lisp/map-ynp.el b/lisp/emacs-lisp/map-ynp.el index dd80524a152..61c04ff7b3e 100644 --- a/lisp/emacs-lisp/map-ynp.el +++ b/lisp/emacs-lisp/map-ynp.el | |||
| @@ -252,4 +252,126 @@ C-g to quit (cancel the whole command); | |||
| 252 | ;; Return the number of actions that were taken. | 252 | ;; Return the number of actions that were taken. |
| 253 | actions)) | 253 | actions)) |
| 254 | 254 | ||
| 255 | |||
| 256 | ;; read-answer is a general-purpose question-asker that supports | ||
| 257 | ;; either long or short answers. | ||
| 258 | |||
| 259 | ;; For backward compatibility check if short y/n answers are preferred. | ||
| 260 | (defcustom read-answer-short (eq (symbol-function 'yes-or-no-p) 'y-or-n-p) | ||
| 261 | "If non-nil, accept short answers to the question." | ||
| 262 | :type 'boolean | ||
| 263 | :version "27.1" | ||
| 264 | :group 'minibuffer) | ||
| 265 | |||
| 266 | (defconst read-answer-map--memoize (make-hash-table :weakness 'key :test 'equal)) | ||
| 267 | |||
| 268 | (defun read-answer (question answers) | ||
| 269 | "Read an answer either as a complete word or its character abbreviation. | ||
| 270 | Ask user a question and accept an answer from the list of possible answers. | ||
| 271 | |||
| 272 | QUESTION should end in a space; this function adds a list of answers to it. | ||
| 273 | |||
| 274 | ANSWERS is an alist with elements in the following format: | ||
| 275 | (LONG-ANSWER SHORT-ANSWER HELP-MESSAGE) | ||
| 276 | where | ||
| 277 | LONG-ANSWER is a complete answer, | ||
| 278 | SHORT-ANSWER is an abbreviated one-character answer, | ||
| 279 | HELP-MESSAGE is a string describing the meaning of the answer. | ||
| 280 | |||
| 281 | Example: | ||
| 282 | \\='((\"yes\" ?y \"perform the action\") | ||
| 283 | (\"no\" ?n \"skip to the next\") | ||
| 284 | (\"all\" ?! \"accept all remaining without more questions\") | ||
| 285 | (\"help\" ?h \"show help\") | ||
| 286 | (\"quit\" ?q \"exit\")) | ||
| 287 | |||
| 288 | When `read-answer-short' is non-nil, accept short answers. | ||
| 289 | |||
| 290 | Return a long answer even in case of accepting short ones. | ||
| 291 | |||
| 292 | When `use-dialog-box' is t, pop up a dialog window to get user input." | ||
| 293 | (custom-reevaluate-setting 'read-answer-short) | ||
| 294 | (let* ((short read-answer-short) | ||
| 295 | (answers-with-help | ||
| 296 | (if (assoc "help" answers) | ||
| 297 | answers | ||
| 298 | (append answers '(("help" ?? "show this help message"))))) | ||
| 299 | (answers-without-help | ||
| 300 | (assoc-delete-all "help" (copy-alist answers-with-help))) | ||
| 301 | (prompt | ||
| 302 | (format "%s(%s) " question | ||
| 303 | (mapconcat (lambda (a) | ||
| 304 | (if short | ||
| 305 | (format "%c" (nth 1 a)) | ||
| 306 | (nth 0 a))) | ||
| 307 | answers-with-help ", "))) | ||
| 308 | (message | ||
| 309 | (format "Please answer %s." | ||
| 310 | (mapconcat (lambda (a) | ||
| 311 | (format "`%s'" (if short | ||
| 312 | (string (nth 1 a)) | ||
| 313 | (nth 0 a)))) | ||
| 314 | answers-with-help " or "))) | ||
| 315 | (short-answer-map | ||
| 316 | (when short | ||
| 317 | (or (gethash answers read-answer-map--memoize) | ||
| 318 | (puthash answers | ||
| 319 | (let ((map (make-sparse-keymap))) | ||
| 320 | (set-keymap-parent map minibuffer-local-map) | ||
| 321 | (dolist (a answers-with-help) | ||
| 322 | (define-key map (vector (nth 1 a)) | ||
| 323 | (lambda () | ||
| 324 | (interactive) | ||
| 325 | (delete-minibuffer-contents) | ||
| 326 | (insert (nth 0 a)) | ||
| 327 | (exit-minibuffer)))) | ||
| 328 | (define-key map [remap self-insert-command] | ||
| 329 | (lambda () | ||
| 330 | (interactive) | ||
| 331 | (delete-minibuffer-contents) | ||
| 332 | (beep) | ||
| 333 | (message message) | ||
| 334 | (sleep-for 2))) | ||
| 335 | map) | ||
| 336 | read-answer-map--memoize)))) | ||
| 337 | answer) | ||
| 338 | (while (not (assoc (setq answer (downcase | ||
| 339 | (cond | ||
| 340 | ((and (display-popup-menus-p) | ||
| 341 | last-input-event ; not during startup | ||
| 342 | (listp last-nonmenu-event) | ||
| 343 | use-dialog-box) | ||
| 344 | (x-popup-dialog | ||
| 345 | t | ||
| 346 | (cons question | ||
| 347 | (mapcar (lambda (a) | ||
| 348 | (cons (capitalize (nth 0 a)) | ||
| 349 | (nth 0 a))) | ||
| 350 | answers-with-help)))) | ||
| 351 | (short | ||
| 352 | (read-from-minibuffer | ||
| 353 | prompt nil short-answer-map nil | ||
| 354 | 'yes-or-no-p-history)) | ||
| 355 | (t | ||
| 356 | (read-from-minibuffer | ||
| 357 | prompt nil nil nil | ||
| 358 | 'yes-or-no-p-history))))) | ||
| 359 | answers-without-help)) | ||
| 360 | (if (string= answer "help") | ||
| 361 | (with-help-window "*Help*" | ||
| 362 | (with-current-buffer "*Help*" | ||
| 363 | (insert "Type:\n" | ||
| 364 | (mapconcat | ||
| 365 | (lambda (a) | ||
| 366 | (format "`%s'%s to %s" | ||
| 367 | (if short (string (nth 1 a)) (nth 0 a)) | ||
| 368 | (if short (format " (%s)" (nth 0 a)) "") | ||
| 369 | (nth 2 a))) | ||
| 370 | answers-with-help ",\n") | ||
| 371 | ".\n"))) | ||
| 372 | (beep) | ||
| 373 | (message message) | ||
| 374 | (sleep-for 2))) | ||
| 375 | answer)) | ||
| 376 | |||
| 255 | ;;; map-ynp.el ends here | 377 | ;;; map-ynp.el ends here |
diff --git a/lisp/subr.el b/lisp/subr.el index 46cf5a34ccc..092850a44d9 100644 --- a/lisp/subr.el +++ b/lisp/subr.el | |||
| @@ -705,6 +705,21 @@ Non-strings in LIST are ignored." | |||
| 705 | (setq list (cdr list))) | 705 | (setq list (cdr list))) |
| 706 | list) | 706 | list) |
| 707 | 707 | ||
| 708 | (defun assoc-delete-all (key alist) | ||
| 709 | "Delete from ALIST all elements whose car is `equal' to KEY. | ||
| 710 | Return the modified alist. | ||
| 711 | Elements of ALIST that are not conses are ignored." | ||
| 712 | (while (and (consp (car alist)) | ||
| 713 | (equal (car (car alist)) key)) | ||
| 714 | (setq alist (cdr alist))) | ||
| 715 | (let ((tail alist) tail-cdr) | ||
| 716 | (while (setq tail-cdr (cdr tail)) | ||
| 717 | (if (and (consp (car tail-cdr)) | ||
| 718 | (equal (car (car tail-cdr)) key)) | ||
| 719 | (setcdr tail (cdr tail-cdr)) | ||
| 720 | (setq tail tail-cdr)))) | ||
| 721 | alist) | ||
| 722 | |||
| 708 | (defun assq-delete-all (key alist) | 723 | (defun assq-delete-all (key alist) |
| 709 | "Delete from ALIST all elements whose car is `eq' to KEY. | 724 | "Delete from ALIST all elements whose car is `eq' to KEY. |
| 710 | Return the modified alist. | 725 | Return the modified alist. |
diff --git a/test/lisp/dired-aux-tests.el b/test/lisp/dired-aux-tests.el index 89cb7b6111d..ab6d1cb0564 100644 --- a/test/lisp/dired-aux-tests.el +++ b/test/lisp/dired-aux-tests.el | |||
| @@ -59,7 +59,7 @@ | |||
| 59 | (unwind-protect | 59 | (unwind-protect |
| 60 | (if ,yes-or-no | 60 | (if ,yes-or-no |
| 61 | (cl-letf (((symbol-function 'yes-or-no-p) | 61 | (cl-letf (((symbol-function 'yes-or-no-p) |
| 62 | (lambda (prompt) (eq ,yes-or-no 'yes)))) | 62 | (lambda (_prompt) (eq ,yes-or-no 'yes)))) |
| 63 | ,@body) | 63 | ,@body) |
| 64 | ,@body) | 64 | ,@body) |
| 65 | ;; clean up | 65 | ;; clean up |
diff --git a/test/lisp/dired-tests.el b/test/lisp/dired-tests.el index c0242137b3a..bb0e1bc3880 100644 --- a/test/lisp/dired-tests.el +++ b/test/lisp/dired-tests.el | |||
| @@ -384,9 +384,9 @@ | |||
| 384 | (dired-test-with-temp-dirs | 384 | (dired-test-with-temp-dirs |
| 385 | 'just-empty-dirs | 385 | 'just-empty-dirs |
| 386 | (let (asked) | 386 | (let (asked) |
| 387 | (advice-add 'dired--yes-no-all-quit-help | 387 | (advice-add 'read-answer |
| 388 | :override | 388 | :override |
| 389 | (lambda (_) (setq asked t) "") | 389 | (lambda (_q _a) (setq asked t) "") |
| 390 | '((name . dired-test-bug27940-advice))) | 390 | '((name . dired-test-bug27940-advice))) |
| 391 | (dired default-directory) | 391 | (dired default-directory) |
| 392 | (dired-toggle-marks) | 392 | (dired-toggle-marks) |
| @@ -395,44 +395,44 @@ | |||
| 395 | (progn | 395 | (progn |
| 396 | (should-not asked) | 396 | (should-not asked) |
| 397 | (should-not (dired-get-marked-files))) ; All dirs deleted. | 397 | (should-not (dired-get-marked-files))) ; All dirs deleted. |
| 398 | (advice-remove 'dired--yes-no-all-quit-help 'dired-test-bug27940-advice)))) | 398 | (advice-remove 'read-answer 'dired-test-bug27940-advice)))) |
| 399 | ;; Answer yes | 399 | ;; Answer yes |
| 400 | (dired-test-with-temp-dirs | 400 | (dired-test-with-temp-dirs |
| 401 | nil | 401 | nil |
| 402 | (advice-add 'dired--yes-no-all-quit-help :override (lambda (_) "yes") | 402 | (advice-add 'read-answer :override (lambda (_q _a) "yes") |
| 403 | '((name . dired-test-bug27940-advice))) | 403 | '((name . dired-test-bug27940-advice))) |
| 404 | (dired default-directory) | 404 | (dired default-directory) |
| 405 | (dired-toggle-marks) | 405 | (dired-toggle-marks) |
| 406 | (dired-do-delete nil) | 406 | (dired-do-delete nil) |
| 407 | (unwind-protect | 407 | (unwind-protect |
| 408 | (should-not (dired-get-marked-files)) ; All dirs deleted. | 408 | (should-not (dired-get-marked-files)) ; All dirs deleted. |
| 409 | (advice-remove 'dired--yes-no-all-quit-help 'dired-test-bug27940-advice))) | 409 | (advice-remove 'read-answer 'dired-test-bug27940-advice))) |
| 410 | ;; Answer no | 410 | ;; Answer no |
| 411 | (dired-test-with-temp-dirs | 411 | (dired-test-with-temp-dirs |
| 412 | nil | 412 | nil |
| 413 | (advice-add 'dired--yes-no-all-quit-help :override (lambda (_) "no") | 413 | (advice-add 'read-answer :override (lambda (_q _a) "no") |
| 414 | '((name . dired-test-bug27940-advice))) | 414 | '((name . dired-test-bug27940-advice))) |
| 415 | (dired default-directory) | 415 | (dired default-directory) |
| 416 | (dired-toggle-marks) | 416 | (dired-toggle-marks) |
| 417 | (dired-do-delete nil) | 417 | (dired-do-delete nil) |
| 418 | (unwind-protect | 418 | (unwind-protect |
| 419 | (should (= 5 (length (dired-get-marked-files)))) ; Just the empty dirs deleted. | 419 | (should (= 5 (length (dired-get-marked-files)))) ; Just the empty dirs deleted. |
| 420 | (advice-remove 'dired--yes-no-all-quit-help 'dired-test-bug27940-advice))) | 420 | (advice-remove 'read-answer 'dired-test-bug27940-advice))) |
| 421 | ;; Answer all | 421 | ;; Answer all |
| 422 | (dired-test-with-temp-dirs | 422 | (dired-test-with-temp-dirs |
| 423 | nil | 423 | nil |
| 424 | (advice-add 'dired--yes-no-all-quit-help :override (lambda (_) "all") | 424 | (advice-add 'read-answer :override (lambda (_q _a) "all") |
| 425 | '((name . dired-test-bug27940-advice))) | 425 | '((name . dired-test-bug27940-advice))) |
| 426 | (dired default-directory) | 426 | (dired default-directory) |
| 427 | (dired-toggle-marks) | 427 | (dired-toggle-marks) |
| 428 | (dired-do-delete nil) | 428 | (dired-do-delete nil) |
| 429 | (unwind-protect | 429 | (unwind-protect |
| 430 | (should-not (dired-get-marked-files)) ; All dirs deleted. | 430 | (should-not (dired-get-marked-files)) ; All dirs deleted. |
| 431 | (advice-remove 'dired--yes-no-all-quit-help 'dired-test-bug27940-advice))) | 431 | (advice-remove 'read-answer 'dired-test-bug27940-advice))) |
| 432 | ;; Answer quit | 432 | ;; Answer quit |
| 433 | (dired-test-with-temp-dirs | 433 | (dired-test-with-temp-dirs |
| 434 | nil | 434 | nil |
| 435 | (advice-add 'dired--yes-no-all-quit-help :override (lambda (_) "quit") | 435 | (advice-add 'read-answer :override (lambda (_q _a) "quit") |
| 436 | '((name . dired-test-bug27940-advice))) | 436 | '((name . dired-test-bug27940-advice))) |
| 437 | (dired default-directory) | 437 | (dired default-directory) |
| 438 | (dired-toggle-marks) | 438 | (dired-toggle-marks) |
| @@ -440,7 +440,7 @@ | |||
| 440 | (dired-do-delete nil)) | 440 | (dired-do-delete nil)) |
| 441 | (unwind-protect | 441 | (unwind-protect |
| 442 | (should (= 6 (length (dired-get-marked-files)))) ; All empty dirs but zeta-empty-dir deleted. | 442 | (should (= 6 (length (dired-get-marked-files)))) ; All empty dirs but zeta-empty-dir deleted. |
| 443 | (advice-remove 'dired--yes-no-all-quit-help 'dired-test-bug27940-advice)))) | 443 | (advice-remove 'read-answer 'dired-test-bug27940-advice)))) |
| 444 | 444 | ||
| 445 | 445 | ||
| 446 | (provide 'dired-tests) | 446 | (provide 'dired-tests) |