diff options
| author | Stefan Kangas | 2020-02-05 13:12:01 +0100 |
|---|---|---|
| committer | Stefan Kangas | 2020-02-05 13:18:12 +0100 |
| commit | aea12d4903136c057bb14d3fd7683bf7a4e1eff6 (patch) | |
| tree | 183e578c697d5b95191df9384f714c999f84a7e7 /lisp | |
| parent | 196da3017bc9b2fc6fecff0c0ce560e6c46b8a72 (diff) | |
| download | emacs-aea12d4903136c057bb14d3fd7683bf7a4e1eff6.tar.gz emacs-aea12d4903136c057bb14d3fd7683bf7a4e1eff6.zip | |
Add new filter commands to Package Menu (Bug#38424)
* lisp/emacs-lisp/package.el (package-menu-filter-by-version)
(package-menu-filter-by-status, package-menu-filter-by-archive):
New filter commands.
(package-menu--filter-by): New helper function.
(package-menu-filter-by-keyword, package-menu-filter-by-name): Use
the above helper function.
(package-menu-mode-menu):
(package-menu-mode-map): Update menu to include new filter commands.
* doc/emacs/package.texi (Package Menu): Document the new commands and
re-arrange the sort order of commands to be closer to the one in
describe-major-mode.
* etc/NEWS: Announce the new commands.
* lisp/emacs-lisp/package.el (package-menu--display): New function
extracted from....
(package-menu--generate): ...here.
* test/lisp/emacs-lisp/package-tests.el (with-package-menu-test):
New macro.
(package-test-update-listing, package-test-list-filter-by-name)
(package-test-list-filter-clear): Use above macro.
(package-test-list-filter-by-archive)
(package-test-list-filter-by-keyword)
(package-test-list-filter-by-status)
(package-test-list-filter-by-version-=)
(package-test-list-filter-by-version-<)
(package-test-list-filter-by-version->): New tests.
(package-test-filter-by-version): New helper function.
Diffstat (limited to 'lisp')
| -rw-r--r-- | lisp/emacs-lisp/package.el | 219 |
1 files changed, 175 insertions, 44 deletions
diff --git a/lisp/emacs-lisp/package.el b/lisp/emacs-lisp/package.el index a9508c1bdc5..f14ef7919ea 100644 --- a/lisp/emacs-lisp/package.el +++ b/lisp/emacs-lisp/package.el | |||
| @@ -2679,15 +2679,18 @@ either a full name or nil, and EMAIL is a valid email address." | |||
| 2679 | (define-key map "i" 'package-menu-mark-install) | 2679 | (define-key map "i" 'package-menu-mark-install) |
| 2680 | (define-key map "U" 'package-menu-mark-upgrades) | 2680 | (define-key map "U" 'package-menu-mark-upgrades) |
| 2681 | (define-key map "r" 'revert-buffer) | 2681 | (define-key map "r" 'revert-buffer) |
| 2682 | (define-key map (kbd "/ k") 'package-menu-filter-by-keyword) | ||
| 2683 | (define-key map (kbd "/ n") 'package-menu-filter-by-name) | ||
| 2684 | (define-key map (kbd "/ /") 'package-menu-clear-filter) | ||
| 2685 | (define-key map "~" 'package-menu-mark-obsolete-for-deletion) | 2682 | (define-key map "~" 'package-menu-mark-obsolete-for-deletion) |
| 2686 | (define-key map "x" 'package-menu-execute) | 2683 | (define-key map "x" 'package-menu-execute) |
| 2687 | (define-key map "h" 'package-menu-quick-help) | 2684 | (define-key map "h" 'package-menu-quick-help) |
| 2688 | (define-key map "H" #'package-menu-hide-package) | 2685 | (define-key map "H" #'package-menu-hide-package) |
| 2689 | (define-key map "?" 'package-menu-describe-package) | 2686 | (define-key map "?" 'package-menu-describe-package) |
| 2690 | (define-key map "(" #'package-menu-toggle-hiding) | 2687 | (define-key map "(" #'package-menu-toggle-hiding) |
| 2688 | (define-key map (kbd "/ /") 'package-menu-clear-filter) | ||
| 2689 | (define-key map (kbd "/ a") 'package-menu-filter-by-archive) | ||
| 2690 | (define-key map (kbd "/ k") 'package-menu-filter-by-keyword) | ||
| 2691 | (define-key map (kbd "/ n") 'package-menu-filter-by-name) | ||
| 2692 | (define-key map (kbd "/ s") 'package-menu-filter-by-status) | ||
| 2693 | (define-key map (kbd "/ v") 'package-menu-filter-by-version) | ||
| 2691 | map) | 2694 | map) |
| 2692 | "Local keymap for `package-menu-mode' buffers.") | 2695 | "Local keymap for `package-menu-mode' buffers.") |
| 2693 | 2696 | ||
| @@ -2714,8 +2717,11 @@ either a full name or nil, and EMAIL is a valid email address." | |||
| 2714 | 2717 | ||
| 2715 | "--" | 2718 | "--" |
| 2716 | ("Filter Packages" | 2719 | ("Filter Packages" |
| 2720 | ["Filter by Archive" package-menu-filter-by-archive :help "Filter packages by archive"] | ||
| 2717 | ["Filter by Keyword" package-menu-filter-by-keyword :help "Filter packages by keyword"] | 2721 | ["Filter by Keyword" package-menu-filter-by-keyword :help "Filter packages by keyword"] |
| 2718 | ["Filter by Name" package-menu-filter-by-name :help "Filter packages by name"] | 2722 | ["Filter by Name" package-menu-filter-by-name :help "Filter packages by name"] |
| 2723 | ["Filter by Status" package-menu-filter-by-status :help "Filter packages by status"] | ||
| 2724 | ["Filter by Version" package-menu-filter-by-version :help "Filter packages by version"] | ||
| 2719 | ["Clear Filter" package-menu-clear-filter :help "Clear package list filter"]) | 2725 | ["Clear Filter" package-menu-clear-filter :help "Clear package list filter"]) |
| 2720 | 2726 | ||
| 2721 | ["Hide by Regexp" package-menu-hide-package :help "Permanently hide all packages matching a regexp"] | 2727 | ["Hide by Regexp" package-menu-hide-package :help "Permanently hide all packages matching a regexp"] |
| @@ -3021,22 +3027,31 @@ When none are given, the package matches." | |||
| 3021 | found) | 3027 | found) |
| 3022 | t)) | 3028 | t)) |
| 3023 | 3029 | ||
| 3024 | (defun package-menu--generate (remember-pos packages &optional keywords) | 3030 | (defun package-menu--display (remember-pos suffix) |
| 3025 | "Populate the Package Menu. | 3031 | "Display the Package Menu. |
| 3026 | If REMEMBER-POS is non-nil, keep point on the same entry. | 3032 | If REMEMBER-POS is non-nil, keep point on the same entry. |
| 3033 | |||
| 3034 | If SUFFIX is non-nil, append that to \"Package\" for the first | ||
| 3035 | column in the header line." | ||
| 3036 | (setf (car (aref tabulated-list-format 0)) | ||
| 3037 | (if suffix | ||
| 3038 | (concat "Package[" suffix "]") | ||
| 3039 | "Package")) | ||
| 3040 | (tabulated-list-init-header) | ||
| 3041 | (tabulated-list-print remember-pos)) | ||
| 3042 | |||
| 3043 | (defun package-menu--generate (remember-pos &optional packages keywords) | ||
| 3044 | "Populate and display the Package Menu. | ||
| 3027 | PACKAGES should be t, which means to display all known packages, | 3045 | PACKAGES should be t, which means to display all known packages, |
| 3028 | or a list of package names (symbols) to display. | 3046 | or a list of package names (symbols) to display. |
| 3029 | 3047 | ||
| 3030 | With KEYWORDS given, only packages with those keywords are | 3048 | With KEYWORDS given, only packages with those keywords are |
| 3031 | shown." | 3049 | shown." |
| 3032 | (package-menu--refresh packages keywords) | 3050 | (package-menu--refresh packages keywords) |
| 3033 | (setf (car (aref tabulated-list-format 0)) | 3051 | (package-menu--display remember-pos |
| 3034 | (if keywords | 3052 | (when keywords |
| 3035 | (let ((filters (mapconcat #'identity keywords ","))) | 3053 | (let ((filters (mapconcat #'identity keywords ","))) |
| 3036 | (concat "Package[" filters "]")) | 3054 | (concat "Package[" filters "]"))))) |
| 3037 | "Package")) | ||
| 3038 | (tabulated-list-init-header) | ||
| 3039 | (tabulated-list-print remember-pos)) | ||
| 3040 | 3055 | ||
| 3041 | (defun package-menu--print-info (pkg) | 3056 | (defun package-menu--print-info (pkg) |
| 3042 | "Return a package entry suitable for `tabulated-list-entries'. | 3057 | "Return a package entry suitable for `tabulated-list-entries'. |
| @@ -3673,45 +3688,160 @@ shown." | |||
| 3673 | (select-window win) | 3688 | (select-window win) |
| 3674 | (switch-to-buffer buf)))) | 3689 | (switch-to-buffer buf)))) |
| 3675 | 3690 | ||
| 3691 | (defun package-menu--filter-by (predicate suffix) | ||
| 3692 | "Filter \"*Packages*\" buffer by PREDICATE and add SUFFIX to header. | ||
| 3693 | PREDICATE is a function which will be called with one argument, a | ||
| 3694 | `package-desc' object, and returns t if that object should be | ||
| 3695 | listed in the Package Menu. | ||
| 3696 | |||
| 3697 | SUFFIX is passed on to `package-menu--display' and is added to | ||
| 3698 | the header line of the first column." | ||
| 3699 | ;; Update `tabulated-list-entries' so that it contains all | ||
| 3700 | ;; packages before searching. | ||
| 3701 | (package-menu--refresh t nil) | ||
| 3702 | (let (found-entries) | ||
| 3703 | (dolist (entry tabulated-list-entries) | ||
| 3704 | (when (funcall predicate (car entry)) | ||
| 3705 | (push entry found-entries))) | ||
| 3706 | (if found-entries | ||
| 3707 | (progn | ||
| 3708 | (setq tabulated-list-entries found-entries) | ||
| 3709 | (package-menu--display t suffix)) | ||
| 3710 | (user-error "No packages found")))) | ||
| 3711 | |||
| 3712 | (defun package-menu-filter-by-archive (archive) | ||
| 3713 | "Filter the \"*Packages*\" buffer by ARCHIVE. | ||
| 3714 | Display only packages from package archive ARCHIVE. | ||
| 3715 | |||
| 3716 | When called interactively, prompt for ARCHIVE, which can be a | ||
| 3717 | comma-separated string. If ARCHIVE is empty, show all packages. | ||
| 3718 | |||
| 3719 | When called from Lisp, ARCHIVE can be a string or a list of | ||
| 3720 | strings. If ARCHIVE is nil or the empty string, show all | ||
| 3721 | packages." | ||
| 3722 | (interactive (list (completing-read-multiple | ||
| 3723 | "Filter by archive (comma separated): " | ||
| 3724 | (mapcar #'car package-archives)))) | ||
| 3725 | (package--ensure-package-menu-mode) | ||
| 3726 | (let ((re (if (listp archive) | ||
| 3727 | (regexp-opt archive) | ||
| 3728 | archive))) | ||
| 3729 | (package-menu--filter-by (lambda (pkg-desc) | ||
| 3730 | (let ((pkg-archive (package-desc-archive pkg-desc))) | ||
| 3731 | (and pkg-archive | ||
| 3732 | (string-match-p re pkg-archive)))) | ||
| 3733 | (concat "archive:" (if (listp archive) | ||
| 3734 | (string-join archive ",") | ||
| 3735 | archive))))) | ||
| 3736 | |||
| 3676 | (defun package-menu-filter-by-keyword (keyword) | 3737 | (defun package-menu-filter-by-keyword (keyword) |
| 3677 | "Filter the \"*Packages*\" buffer by KEYWORD. | 3738 | "Filter the \"*Packages*\" buffer by KEYWORD. |
| 3678 | Show only those items that relate to the specified KEYWORD. | 3739 | Display only packages with specified KEYWORD. |
| 3679 | 3740 | ||
| 3680 | KEYWORD can be a string or a list of strings. If it is a list, a | 3741 | When called interactively, prompt for KEYWORD, which can be a |
| 3681 | package will be displayed if it matches any of the keywords. | 3742 | comma-separated string. If KEYWORD is empty, show all packages. |
| 3682 | Interactively, it is a list of strings separated by commas. | 3743 | |
| 3683 | 3744 | When called from Lisp, KEYWORD can be a string or a list of | |
| 3684 | KEYWORD can also be used to filter by status or archive name by | 3745 | strings. If KEYWORD is nil or the empty string, show all |
| 3685 | using keywords like \"arc:gnu\" and \"status:available\". | 3746 | packages." |
| 3686 | Statuses available include \"incompat\", \"available\", | 3747 | (interactive (list (completing-read-multiple |
| 3687 | \"built-in\" and \"installed\"." | 3748 | "Keywords (comma separated): " |
| 3688 | (interactive | 3749 | (package-all-keywords)))) |
| 3689 | (list (completing-read-multiple | 3750 | (when (stringp keyword) |
| 3690 | "Keywords (comma separated): " (package-all-keywords)))) | 3751 | (setq keyword (list keyword))) |
| 3691 | (package--ensure-package-menu-mode) | 3752 | (package--ensure-package-menu-mode) |
| 3692 | (package-show-package-list t (if (stringp keyword) | 3753 | (if (not keyword) |
| 3693 | (list keyword) | 3754 | (package-menu--generate t t) |
| 3694 | keyword))) | 3755 | (package-menu--filter-by (lambda (pkg-desc) |
| 3756 | (package--has-keyword-p pkg-desc keyword)) | ||
| 3757 | (concat "keyword:" (string-join keyword ","))))) | ||
| 3695 | 3758 | ||
| 3696 | (defun package-menu-filter-by-name (name) | 3759 | (defun package-menu-filter-by-name (name) |
| 3697 | "Filter the \"*Packages*\" buffer by NAME. | 3760 | "Filter the \"*Packages*\" buffer by NAME regexp. |
| 3698 | Show only those items whose name matches the regular expression | 3761 | Display only packages with name that matches regexp NAME. |
| 3699 | NAME. If NAME is nil or the empty string, show all packages." | 3762 | |
| 3700 | (interactive (list (read-from-minibuffer "Filter by name (regexp): "))) | 3763 | When called interactively, prompt for NAME. |
| 3764 | |||
| 3765 | If NAME is nil or the empty string, show all packages." | ||
| 3766 | (interactive (list (read-regexp "Filter by name (regexp)"))) | ||
| 3701 | (package--ensure-package-menu-mode) | 3767 | (package--ensure-package-menu-mode) |
| 3702 | (if (or (not name) (string-empty-p name)) | 3768 | (if (or (not name) (string-empty-p name)) |
| 3703 | (package-show-package-list t nil) | 3769 | (package-menu--generate t t) |
| 3704 | ;; Update `tabulated-list-entries' so that it contains all | 3770 | (package-menu--filter-by (lambda (pkg-desc) |
| 3705 | ;; packages before searching. | 3771 | (string-match-p name (symbol-name |
| 3706 | (package-menu--refresh t nil) | 3772 | (package-desc-name pkg-desc)))) |
| 3707 | (let (matched) | 3773 | (format "name:%s" name)))) |
| 3708 | (dolist (entry tabulated-list-entries) | 3774 | |
| 3709 | (let* ((pkg-name (package-desc-name (car entry)))) | 3775 | (defun package-menu-filter-by-status (status) |
| 3710 | (when (string-match name (symbol-name pkg-name)) | 3776 | "Filter the \"*Packages*\" buffer by STATUS. |
| 3711 | (push pkg-name matched)))) | 3777 | Display only packages with specified STATUS. |
| 3712 | (if matched | 3778 | |
| 3713 | (package-show-package-list matched nil) | 3779 | When called interactively, prompt for STATUS, which can be a |
| 3714 | (user-error "No packages found"))))) | 3780 | comma-separated string. If STATUS is empty, show all packages. |
| 3781 | |||
| 3782 | When called from Lisp, STATUS can be a string or a list of | ||
| 3783 | strings. If STATUS is nil or the empty string, show all | ||
| 3784 | packages." | ||
| 3785 | (interactive (list (completing-read "Filter by status: " | ||
| 3786 | '("avail-obso" | ||
| 3787 | "available" | ||
| 3788 | "built-in" | ||
| 3789 | "dependency" | ||
| 3790 | "disabled" | ||
| 3791 | "external" | ||
| 3792 | "held" | ||
| 3793 | "incompat" | ||
| 3794 | "installed" | ||
| 3795 | "new" | ||
| 3796 | "unsigned")))) | ||
| 3797 | (package--ensure-package-menu-mode) | ||
| 3798 | (if (or (not status) (string-empty-p status)) | ||
| 3799 | (package-menu--generate t t) | ||
| 3800 | (package-menu--filter-by (lambda (pkg-desc) | ||
| 3801 | (string-match-p status (package-desc-status pkg-desc))) | ||
| 3802 | (format "status:%s" status)))) | ||
| 3803 | |||
| 3804 | (defun package-menu-filter-by-version (version predicate) | ||
| 3805 | "Filter the \"*Packages*\" buffer by VERSION and PREDICATE. | ||
| 3806 | Display only packages with a matching version. | ||
| 3807 | |||
| 3808 | When called interactively, prompt for one of the qualifiers `<', | ||
| 3809 | `>' or `=', and a package version. Show only packages that has a | ||
| 3810 | lower (`<'), equal (`=') or higher (`>') version than the | ||
| 3811 | specified one. | ||
| 3812 | |||
| 3813 | When called from Lisp, VERSION should be a version string and | ||
| 3814 | PREDICATE should be the symbol `=', `<' or `>'. | ||
| 3815 | |||
| 3816 | If VERSION is nil or the empty string, show all packages." | ||
| 3817 | (interactive (let ((choice (intern | ||
| 3818 | (char-to-string | ||
| 3819 | (read-char-choice | ||
| 3820 | "Filter by version? [Type =, <, > or q] " | ||
| 3821 | '(?< ?> ?= ?q)))))) | ||
| 3822 | (if (eq choice 'q) | ||
| 3823 | '(quit nil) | ||
| 3824 | (list (read-from-minibuffer | ||
| 3825 | (concat "Filter by version (" | ||
| 3826 | (pcase choice | ||
| 3827 | ('= "= equal to") | ||
| 3828 | ('< "< less than") | ||
| 3829 | ('> "> greater than")) | ||
| 3830 | "): ")) | ||
| 3831 | choice)))) | ||
| 3832 | (unless (equal predicate 'quit) | ||
| 3833 | (if (or (not version) (string-empty-p version)) | ||
| 3834 | (package-menu--generate t t) | ||
| 3835 | (package-menu--filter-by | ||
| 3836 | (let ((fun (pcase predicate | ||
| 3837 | ('= 'version-list-=) | ||
| 3838 | ('< 'version-list-<) | ||
| 3839 | ('> '(lambda (a b) (not (version-list-<= a b)))) | ||
| 3840 | (_ (error "Unknown predicate: %s" predicate)))) | ||
| 3841 | (ver (version-to-list version))) | ||
| 3842 | (lambda (pkg-desc) | ||
| 3843 | (funcall fun (package-desc-version pkg-desc) ver))) | ||
| 3844 | (format "versions:%s%s" predicate version))))) | ||
| 3715 | 3845 | ||
| 3716 | (defun package-menu-clear-filter () | 3846 | (defun package-menu-clear-filter () |
| 3717 | "Clear any filter currently applied to the \"*Packages*\" buffer." | 3847 | "Clear any filter currently applied to the \"*Packages*\" buffer." |
| @@ -3760,6 +3890,7 @@ The return value is a string (or nil in case we can't find it)." | |||
| 3760 | (or (lm-header "package-version") | 3890 | (or (lm-header "package-version") |
| 3761 | (lm-header "version"))))))))) | 3891 | (lm-header "version"))))))))) |
| 3762 | 3892 | ||
| 3893 | |||
| 3763 | ;;;; Quickstart: precompute activation actions for faster start up. | 3894 | ;;;; Quickstart: precompute activation actions for faster start up. |
| 3764 | 3895 | ||
| 3765 | ;; Activating packages via `package-initialize' is costly: for N installed | 3896 | ;; Activating packages via `package-initialize' is costly: for N installed |