diff options
| author | Juri Linkov | 2018-01-21 23:45:43 +0200 |
|---|---|---|
| committer | Noam Postavsky | 2018-08-04 11:37:39 -0400 |
| commit | cc233365a925dcf9fa7270630819f2e6e75280da (patch) | |
| tree | 7d3ecdf237cfe9a014d35034bc32cd1ec55ebfff | |
| parent | f0b8e64fb7720a9376bde80cc59fe37b0df83b9d (diff) | |
| download | emacs-cc233365a925dcf9fa7270630819f2e6e75280da.tar.gz emacs-cc233365a925dcf9fa7270630819f2e6e75280da.zip | |
New function read-answer (Bug#31782)
* lisp/emacs-lisp/map-ynp.el (read-answer-short): New defcustom.
(read-answer): New function.
* lisp/subr.el (assoc-delete-all): New function.
* etc/NEWS: Announce them.
* lisp/dired.el (dired-delete-file): Use read-answer.
(dired--yes-no-all-quit-help): Remove function.
(dired-delete-help): Remove defconst.
(backported from master, "New function read-answer (bug#30073)" and
"Respect non-saved value of `read-short-answer' (Bug#31782)")
| -rw-r--r-- | etc/NEWS | 6 | ||||
| -rw-r--r-- | lisp/dired.el | 41 | ||||
| -rw-r--r-- | lisp/emacs-lisp/map-ynp.el | 128 | ||||
| -rw-r--r-- | lisp/subr.el | 15 | ||||
| -rw-r--r-- | test/lisp/dired-tests.el | 22 |
5 files changed, 168 insertions, 44 deletions
| @@ -110,6 +110,12 @@ be removed prior using the changed 'shadow-*' commands. | |||
| 110 | 110 | ||
| 111 | * Lisp Changes in Emacs 26.2 | 111 | * Lisp Changes in Emacs 26.2 |
| 112 | 112 | ||
| 113 | ** The new function 'read-answer' accepts either long or short answers | ||
| 114 | depending on the new customizable variable 'read-answer-short'. | ||
| 115 | |||
| 116 | ** New function 'assoc-delete-all'. | ||
| 117 | Like 'assq-delete-all', but uses 'equal' for comparison. | ||
| 118 | |||
| 113 | 119 | ||
| 114 | * Changes in Emacs 26.2 on Non-Free Operating Systems | 120 | * Changes in Emacs 26.2 on Non-Free Operating Systems |
| 115 | 121 | ||
diff --git a/lisp/dired.el b/lisp/dired.el index c421e51ffd1..2520ed2a109 100644 --- a/lisp/dired.el +++ b/lisp/dired.el | |||
| @@ -2995,37 +2995,6 @@ Any other value means to ask for each directory." | |||
| 2995 | ;; Match anything but `.' and `..'. | 2995 | ;; Match anything but `.' and `..'. |
| 2996 | (defvar dired-re-no-dot "^\\([^.]\\|\\.\\([^.]\\|\\..\\)\\).*") | 2996 | (defvar dired-re-no-dot "^\\([^.]\\|\\.\\([^.]\\|\\..\\)\\).*") |
| 2997 | 2997 | ||
| 2998 | (defconst dired-delete-help | ||
| 2999 | "Type: | ||
| 3000 | `yes' to delete recursively the current directory, | ||
| 3001 | `no' to skip to next, | ||
| 3002 | `all' to delete all remaining directories with no more questions, | ||
| 3003 | `quit' to exit, | ||
| 3004 | `help' to show this help message.") | ||
| 3005 | |||
| 3006 | (defun dired--yes-no-all-quit-help (prompt &optional help-msg) | ||
| 3007 | "Ask a question with valid answers: yes, no, all, quit, help. | ||
| 3008 | PROMPT must end with '? ', for instance, 'Delete it? '. | ||
| 3009 | If optional arg HELP-MSG is non-nil, then is a message to show when | ||
| 3010 | the user answers 'help'. Otherwise, default to `dired-delete-help'." | ||
| 3011 | (let ((valid-answers (list "yes" "no" "all" "quit")) | ||
| 3012 | (answer "") | ||
| 3013 | (input-fn (lambda () | ||
| 3014 | (read-string | ||
| 3015 | (format "%s [yes, no, all, quit, help] " prompt))))) | ||
| 3016 | (setq answer (funcall input-fn)) | ||
| 3017 | (when (string= answer "help") | ||
| 3018 | (with-help-window "*Help*" | ||
| 3019 | (with-current-buffer "*Help*" | ||
| 3020 | (insert (or help-msg dired-delete-help))))) | ||
| 3021 | (while (not (member answer valid-answers)) | ||
| 3022 | (unless (string= answer "help") | ||
| 3023 | (beep) | ||
| 3024 | (message "Please answer `yes' or `no' or `all' or `quit'") | ||
| 3025 | (sleep-for 2)) | ||
| 3026 | (setq answer (funcall input-fn))) | ||
| 3027 | answer)) | ||
| 3028 | |||
| 3029 | ;; Delete file, possibly delete a directory and all its files. | 2998 | ;; Delete file, possibly delete a directory and all its files. |
| 3030 | ;; This function is useful outside of dired. One could change its name | 2999 | ;; This function is useful outside of dired. One could change its name |
| 3031 | ;; to e.g. recursive-delete-file and put it somewhere else. | 3000 | ;; to e.g. recursive-delete-file and put it somewhere else. |
| @@ -3055,11 +3024,17 @@ TRASH non-nil means to trash the file instead of deleting, provided | |||
| 3055 | "trash" | 3024 | "trash" |
| 3056 | "delete") | 3025 | "delete") |
| 3057 | (dired-make-relative file)))) | 3026 | (dired-make-relative file)))) |
| 3058 | (pcase (dired--yes-no-all-quit-help prompt) ; Prompt user. | 3027 | (pcase (read-answer |
| 3028 | prompt | ||
| 3029 | '(("yes" ?y "delete recursively the current directory") | ||
| 3030 | ("no" ?n "skip to next") | ||
| 3031 | ("all" ?! "delete all remaining directories with no more questions") | ||
| 3032 | ("quit" ?q "exit"))) | ||
| 3059 | ('"all" (setq recursive 'always dired-recursive-deletes recursive)) | 3033 | ('"all" (setq recursive 'always dired-recursive-deletes recursive)) |
| 3060 | ('"yes" (if (eq recursive 'top) (setq recursive 'always))) | 3034 | ('"yes" (if (eq recursive 'top) (setq recursive 'always))) |
| 3061 | ('"no" (setq recursive nil)) | 3035 | ('"no" (setq recursive nil)) |
| 3062 | ('"quit" (keyboard-quit))))) | 3036 | ('"quit" (keyboard-quit)) |
| 3037 | (_ (keyboard-quit))))) ; catch all unknown answers | ||
| 3063 | (setq recursive nil)) ; Empty dir or recursive is nil. | 3038 | (setq recursive nil)) ; Empty dir or recursive is nil. |
| 3064 | (delete-directory file recursive trash)))) | 3039 | (delete-directory file recursive trash)))) |
| 3065 | 3040 | ||
diff --git a/lisp/emacs-lisp/map-ynp.el b/lisp/emacs-lisp/map-ynp.el index 2a7eddedad7..8260af57278 100644 --- a/lisp/emacs-lisp/map-ynp.el +++ b/lisp/emacs-lisp/map-ynp.el | |||
| @@ -256,4 +256,132 @@ the current %s and exit." | |||
| 256 | ;; Return the number of actions that were taken. | 256 | ;; Return the number of actions that were taken. |
| 257 | actions)) | 257 | actions)) |
| 258 | 258 | ||
| 259 | |||
| 260 | ;; read-answer is a general-purpose question-asker that supports | ||
| 261 | ;; either long or short answers. | ||
| 262 | |||
| 263 | ;; For backward compatibility check if short y/n answers are preferred. | ||
| 264 | (defcustom read-answer-short 'auto | ||
| 265 | "If non-nil, `read-answer' accepts single-character answers. | ||
| 266 | If t, accept short (single key-press) answers to the question. | ||
| 267 | If nil, require long answers. If `auto', accept short answers if | ||
| 268 | the function cell of `yes-or-no-p' is set to `y-or-on-p'." | ||
| 269 | :type '(choice (const :tag "Accept short answers" t) | ||
| 270 | (const :tag "Require long answer" nil) | ||
| 271 | (const :tag "Guess preference" auto)) | ||
| 272 | :version "26.2" | ||
| 273 | :group 'minibuffer) | ||
| 274 | |||
| 275 | (defconst read-answer-map--memoize (make-hash-table :weakness 'key :test 'equal)) | ||
| 276 | |||
| 277 | (defun read-answer (question answers) | ||
| 278 | "Read an answer either as a complete word or its character abbreviation. | ||
| 279 | Ask user a question and accept an answer from the list of possible answers. | ||
| 280 | |||
| 281 | QUESTION should end in a space; this function adds a list of answers to it. | ||
| 282 | |||
| 283 | ANSWERS is an alist with elements in the following format: | ||
| 284 | (LONG-ANSWER SHORT-ANSWER HELP-MESSAGE) | ||
| 285 | where | ||
| 286 | LONG-ANSWER is a complete answer, | ||
| 287 | SHORT-ANSWER is an abbreviated one-character answer, | ||
| 288 | HELP-MESSAGE is a string describing the meaning of the answer. | ||
| 289 | |||
| 290 | Example: | ||
| 291 | \\='((\"yes\" ?y \"perform the action\") | ||
| 292 | (\"no\" ?n \"skip to the next\") | ||
| 293 | (\"all\" ?! \"accept all remaining without more questions\") | ||
| 294 | (\"help\" ?h \"show help\") | ||
| 295 | (\"quit\" ?q \"exit\")) | ||
| 296 | |||
| 297 | When `read-answer-short' is non-nil, accept short answers. | ||
| 298 | |||
| 299 | Return a long answer even in case of accepting short ones. | ||
| 300 | |||
| 301 | When `use-dialog-box' is t, pop up a dialog window to get user input." | ||
| 302 | (let* ((short (if (eq read-answer-short 'auto) | ||
| 303 | (eq (symbol-function 'yes-or-no-p) 'y-or-n-p) | ||
| 304 | read-answer-short)) | ||
| 305 | (answers-with-help | ||
| 306 | (if (assoc "help" answers) | ||
| 307 | answers | ||
| 308 | (append answers '(("help" ?? "show this help message"))))) | ||
| 309 | (answers-without-help | ||
| 310 | (assoc-delete-all "help" (copy-alist answers-with-help))) | ||
| 311 | (prompt | ||
| 312 | (format "%s(%s) " question | ||
| 313 | (mapconcat (lambda (a) | ||
| 314 | (if short | ||
| 315 | (format "%c" (nth 1 a)) | ||
| 316 | (nth 0 a))) | ||
| 317 | answers-with-help ", "))) | ||
| 318 | (message | ||
| 319 | (format "Please answer %s." | ||
| 320 | (mapconcat (lambda (a) | ||
| 321 | (format "`%s'" (if short | ||
| 322 | (string (nth 1 a)) | ||
| 323 | (nth 0 a)))) | ||
| 324 | answers-with-help " or "))) | ||
| 325 | (short-answer-map | ||
| 326 | (when short | ||
| 327 | (or (gethash answers read-answer-map--memoize) | ||
| 328 | (puthash answers | ||
| 329 | (let ((map (make-sparse-keymap))) | ||
| 330 | (set-keymap-parent map minibuffer-local-map) | ||
| 331 | (dolist (a answers-with-help) | ||
| 332 | (define-key map (vector (nth 1 a)) | ||
| 333 | (lambda () | ||
| 334 | (interactive) | ||
| 335 | (delete-minibuffer-contents) | ||
| 336 | (insert (nth 0 a)) | ||
| 337 | (exit-minibuffer)))) | ||
| 338 | (define-key map [remap self-insert-command] | ||
| 339 | (lambda () | ||
| 340 | (interactive) | ||
| 341 | (delete-minibuffer-contents) | ||
| 342 | (beep) | ||
| 343 | (message message) | ||
| 344 | (sleep-for 2))) | ||
| 345 | map) | ||
| 346 | read-answer-map--memoize)))) | ||
| 347 | answer) | ||
| 348 | (while (not (assoc (setq answer (downcase | ||
| 349 | (cond | ||
| 350 | ((and (display-popup-menus-p) | ||
| 351 | last-input-event ; not during startup | ||
| 352 | (listp last-nonmenu-event) | ||
| 353 | use-dialog-box) | ||
| 354 | (x-popup-dialog | ||
| 355 | t | ||
| 356 | (cons question | ||
| 357 | (mapcar (lambda (a) | ||
| 358 | (cons (capitalize (nth 0 a)) | ||
| 359 | (nth 0 a))) | ||
| 360 | answers-with-help)))) | ||
| 361 | (short | ||
| 362 | (read-from-minibuffer | ||
| 363 | prompt nil short-answer-map nil | ||
| 364 | 'yes-or-no-p-history)) | ||
| 365 | (t | ||
| 366 | (read-from-minibuffer | ||
| 367 | prompt nil nil nil | ||
| 368 | 'yes-or-no-p-history))))) | ||
| 369 | answers-without-help)) | ||
| 370 | (if (string= answer "help") | ||
| 371 | (with-help-window "*Help*" | ||
| 372 | (with-current-buffer "*Help*" | ||
| 373 | (insert "Type:\n" | ||
| 374 | (mapconcat | ||
| 375 | (lambda (a) | ||
| 376 | (format "`%s'%s to %s" | ||
| 377 | (if short (string (nth 1 a)) (nth 0 a)) | ||
| 378 | (if short (format " (%s)" (nth 0 a)) "") | ||
| 379 | (nth 2 a))) | ||
| 380 | answers-with-help ",\n") | ||
| 381 | ".\n"))) | ||
| 382 | (beep) | ||
| 383 | (message message) | ||
| 384 | (sleep-for 2))) | ||
| 385 | answer)) | ||
| 386 | |||
| 259 | ;;; map-ynp.el ends here | 387 | ;;; map-ynp.el ends here |
diff --git a/lisp/subr.el b/lisp/subr.el index f8ac70edefa..7582b6cdb85 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-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) |