Skip to content

Commit aea12d4

Browse files
committed
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.
1 parent 196da30 commit aea12d4

File tree

4 files changed

+319
-94
lines changed

4 files changed

+319
-94
lines changed

doc/emacs/package.texi

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -151,27 +151,6 @@ Refresh the package list (@code{revert-buffer}). This fetches the
151151
list of available packages from the package archive again, and
152152
redisplays the package list.
153153

154-
@item / k
155-
@kindex / k @r{(Package Menu)}
156-
@findex package-menu-filter-by-keyword
157-
Filter the package list by keyword
158-
(@code{package-menu-filter-by-keyword}). This prompts for a keyword
159-
(e.g., @samp{games}), then shows only the packages that relate to that
160-
keyword.
161-
162-
@item / n
163-
@kindex / n @r{(Package Menu)}
164-
@findex package-menu-filter-by-name
165-
Filter the package list by name (@code{package-menu-filter-by-name}).
166-
This prompts for a string, then shows only the packages whose names
167-
match a regexp with that value.
168-
169-
@item / /
170-
@kindex / / @r{(Package Menu)}
171-
@findex package-menu-clear-filter
172-
Clear filter currently applied to the package list
173-
(@code{package-menu-clear-filter}).
174-
175154
@item H
176155
@kindex H @r{(Package Menu)}
177156
@findex package-menu-hide-package
@@ -183,6 +162,48 @@ Permanently hide packages that match a regexp
183162
@findex package-menu-toggle-hiding
184163
Toggle visibility of old versions of packages and also of versions
185164
from lower-priority archives (@code{package-menu-toggle-hiding}).
165+
166+
@item / a
167+
@kindex / a @r{(Package Menu)}
168+
@findex package-menu-filter-by-archive
169+
Filter package list by archive (@code{package-menu-filter-by-archive}).
170+
This prompts for a package archive (e.g., @samp{gnu}), then shows only
171+
packages from that archive.
172+
173+
@item / k
174+
@kindex / k @r{(Package Menu)}
175+
@findex package-menu-filter-by-keyword
176+
Filter package list by keyword (@code{package-menu-filter-by-keyword}).
177+
This prompts for a keyword (e.g., @samp{games}), then shows only
178+
packages with that keyword.
179+
180+
@item / n
181+
@kindex / n @r{(Package Menu)}
182+
@findex package-menu-filter-by-name
183+
Filter package list by name (@code{package-menu-filter-by-name}).
184+
This prompts for a regular expression, then shows only packages
185+
with names matching that regexp.
186+
187+
@item / s
188+
@kindex / s @r{(Package Menu)}
189+
@findex package-menu-filter-by-status
190+
Filter package list by status (@code{package-menu-filter-by-status}).
191+
This prompts for one or more statuses (e.g., @samp{available}), then
192+
shows only packages with matching status.
193+
194+
@item / v
195+
@kindex / v @r{(Package Menu)}
196+
@findex package-menu-filter-by-version
197+
Filter package list by version (@code{package-menu-filter-by-version}).
198+
This prompts first for one of the qualifiers @samp{<}, @samp{>} or
199+
@samp{=}, and then a package version, and shows packages that has a
200+
lower, equal or higher version than the one specified.
201+
202+
@item / /
203+
@kindex / / @r{(Package Menu)}
204+
@findex package-menu-filter-clear
205+
Clear filter currently applied to the package list
206+
(@code{package-menu-filter-clear}).
186207
@end table
187208

188209
@noindent

etc/NEWS

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,20 @@ like cell phones, tablets or cameras.
120120
*** Pcase 'map' pattern added keyword symbols abbreviation.
121121
A pattern like '(map :sym)' binds the map's value for ':sym' to 'sym',
122122
equivalent to '(map (:sym sym))'.
123+
** Package
124+
125+
+++
126+
*** New functions to filter the package list.
127+
The filter command key bindings are as follows:
128+
129+
key binding
130+
--- -------
131+
/ a package-menu-filter-by-archive
132+
/ k package-menu-filter-by-keyword
133+
/ n package-menu-filter-by-name
134+
/ s package-menu-filter-by-status
135+
/ v package-menu-filter-by-version
136+
/ / package-menu-filter-clear
123137

124138

125139
* New Modes and Packages in Emacs 28.1

lisp/emacs-lisp/package.el

Lines changed: 175 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2679,15 +2679,18 @@ either a full name or nil, and EMAIL is a valid email address."
26792679
(define-key map "i" 'package-menu-mark-install)
26802680
(define-key map "U" 'package-menu-mark-upgrades)
26812681
(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)
26852682
(define-key map "~" 'package-menu-mark-obsolete-for-deletion)
26862683
(define-key map "x" 'package-menu-execute)
26872684
(define-key map "h" 'package-menu-quick-help)
26882685
(define-key map "H" #'package-menu-hide-package)
26892686
(define-key map "?" 'package-menu-describe-package)
26902687
(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)
26912694
map)
26922695
"Local keymap for `package-menu-mode' buffers.")
26932696

@@ -2714,8 +2717,11 @@ either a full name or nil, and EMAIL is a valid email address."
27142717

27152718
"--"
27162719
("Filter Packages"
2720+
["Filter by Archive" package-menu-filter-by-archive :help "Filter packages by archive"]
27172721
["Filter by Keyword" package-menu-filter-by-keyword :help "Filter packages by keyword"]
27182722
["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"]
27192725
["Clear Filter" package-menu-clear-filter :help "Clear package list filter"])
27202726

27212727
["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."
30213027
found)
30223028
t))
30233029

3024-
(defun package-menu--generate (remember-pos packages &optional keywords)
3025-
"Populate the Package Menu.
3030+
(defun package-menu--display (remember-pos suffix)
3031+
"Display the Package Menu.
30263032
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.
30273045
PACKAGES should be t, which means to display all known packages,
30283046
or a list of package names (symbols) to display.
30293047
30303048
With KEYWORDS given, only packages with those keywords are
30313049
shown."
30323050
(package-menu--refresh packages keywords)
3033-
(setf (car (aref tabulated-list-format 0))
3034-
(if keywords
3035-
(let ((filters (mapconcat #'identity keywords ",")))
3036-
(concat "Package[" filters "]"))
3037-
"Package"))
3038-
(tabulated-list-init-header)
3039-
(tabulated-list-print remember-pos))
3051+
(package-menu--display remember-pos
3052+
(when keywords
3053+
(let ((filters (mapconcat #'identity keywords ",")))
3054+
(concat "Package[" filters "]")))))
30403055

30413056
(defun package-menu--print-info (pkg)
30423057
"Return a package entry suitable for `tabulated-list-entries'.
@@ -3673,45 +3688,160 @@ shown."
36733688
(select-window win)
36743689
(switch-to-buffer buf))))
36753690

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+
36763737
(defun package-menu-filter-by-keyword (keyword)
36773738
"Filter the \"*Packages*\" buffer by KEYWORD.
3678-
Show only those items that relate to the specified KEYWORD.
3679-
3680-
KEYWORD can be a string or a list of strings. If it is a list, a
3681-
package will be displayed if it matches any of the keywords.
3682-
Interactively, it is a list of strings separated by commas.
3683-
3684-
KEYWORD can also be used to filter by status or archive name by
3685-
using keywords like \"arc:gnu\" and \"status:available\".
3686-
Statuses available include \"incompat\", \"available\",
3687-
\"built-in\" and \"installed\"."
3688-
(interactive
3689-
(list (completing-read-multiple
3690-
"Keywords (comma separated): " (package-all-keywords))))
3739+
Display only packages with specified KEYWORD.
3740+
3741+
When called interactively, prompt for KEYWORD, which can be a
3742+
comma-separated string. If KEYWORD is empty, show all packages.
3743+
3744+
When called from Lisp, KEYWORD can be a string or a list of
3745+
strings. If KEYWORD is nil or the empty string, show all
3746+
packages."
3747+
(interactive (list (completing-read-multiple
3748+
"Keywords (comma separated): "
3749+
(package-all-keywords))))
3750+
(when (stringp keyword)
3751+
(setq keyword (list keyword)))
36913752
(package--ensure-package-menu-mode)
3692-
(package-show-package-list t (if (stringp keyword)
3693-
(list keyword)
3694-
keyword)))
3753+
(if (not keyword)
3754+
(package-menu--generate t t)
3755+
(package-menu--filter-by (lambda (pkg-desc)
3756+
(package--has-keyword-p pkg-desc keyword))
3757+
(concat "keyword:" (string-join keyword ",")))))
36953758

36963759
(defun package-menu-filter-by-name (name)
3697-
"Filter the \"*Packages*\" buffer by NAME.
3698-
Show only those items whose name matches the regular expression
3699-
NAME. If NAME is nil or the empty string, show all packages."
3700-
(interactive (list (read-from-minibuffer "Filter by name (regexp): ")))
3760+
"Filter the \"*Packages*\" buffer by NAME regexp.
3761+
Display only packages with name that matches regexp NAME.
3762+
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)")))
37013767
(package--ensure-package-menu-mode)
37023768
(if (or (not name) (string-empty-p name))
3703-
(package-show-package-list t nil)
3704-
;; Update `tabulated-list-entries' so that it contains all
3705-
;; packages before searching.
3706-
(package-menu--refresh t nil)
3707-
(let (matched)
3708-
(dolist (entry tabulated-list-entries)
3709-
(let* ((pkg-name (package-desc-name (car entry))))
3710-
(when (string-match name (symbol-name pkg-name))
3711-
(push pkg-name matched))))
3712-
(if matched
3713-
(package-show-package-list matched nil)
3714-
(user-error "No packages found")))))
3769+
(package-menu--generate t t)
3770+
(package-menu--filter-by (lambda (pkg-desc)
3771+
(string-match-p name (symbol-name
3772+
(package-desc-name pkg-desc))))
3773+
(format "name:%s" name))))
3774+
3775+
(defun package-menu-filter-by-status (status)
3776+
"Filter the \"*Packages*\" buffer by STATUS.
3777+
Display only packages with specified STATUS.
3778+
3779+
When called interactively, prompt for STATUS, which can be a
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)))))
37153845

37163846
(defun package-menu-clear-filter ()
37173847
"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)."
37603890
(or (lm-header "package-version")
37613891
(lm-header "version")))))))))
37623892

3893+
37633894
;;;; Quickstart: precompute activation actions for faster start up.
37643895

37653896
;; Activating packages via `package-initialize' is costly: for N installed

0 commit comments

Comments
 (0)