Subject: bug#36981: 26.2; request: add searching by package name to list-packages

From: Federico Tedin <[email-masked]>
Date: Mon, 30 Sep 2019 23:26:49 +0200

Hey Stefan, thanks again for the input.

> Perhaps just:
>
> * test/lisp/emacs-lisp/package-tests.el (package-test-list-filter-by-name)
> (package-test-list-clear-filter): New tests.
>
>> +@item / s
>> +Filter the package list by name (@code{package-menu-filter-by-name}).
>
> That should be "/ n" as below.

Good catch!

>> ++++
>> +*** New function 'package-menu-filter-by-name'.
>> +Allows users to filter packages by name on the packages list.  By
>> +default, it is bound to '/ n'.  To clear the filter, use '/ /'.
>> +
>> ++++
>> +*** Function 'package-menu-fiter-by-keyword' has been renamed
>> +from 'package-menu-filter'.  Its keybinding has also been changed to
>> +'/ k' (from 'f').  To clear the filter, '/ /' must now be used instead
>> +of 'q'.
>> +
>> ++++
>> +*** New function 'package-menu-clear-filter'
>> +Allows users to clear a filter applied with 'package-menu-filter-by-name' or
>> +'package-menu-filter-by-keyword'.
>
> Perhaps this would be better as just one item where all changes are
> explained together?
>
>> @@ -2699,7 +2701,9 @@ package-menu-mode-map
>>      ["Unmark" package-menu-mark-unmark :help "Clear any marks on a package and move to the next line"]
>>
>>      "--"
>> -    ["Filter Package List" package-menu-filter :help "Filter package selection (q to go back)"]
>> +    ["Filter Packages by Keyword" package-menu-filter-by-keyword :help "Filter packages by keyword"]
>> +    ["Filter Packages by Name" package-menu-filter-by-name :help "Filter packages by name"]
>> +    ["Clear Packages Filter" package-menu-clear-filter :help "Clear package list filter"]
>
> This is fine by me, but perhaps it would be better to add a submenu
> for filtering?

Good idea, I've changed the menu entries so that they're part of a
submenu now. I'm attaching the new patch.

Thanks!

>From a6cb17c6d99113d6f6a6b2a3164a9101c8fecb51 Mon Sep 17 00:00:00 2001
From: Federico Tedin <[email-masked]>
Date: Thu, 26 Sep 2019 19:18:58 +0200
Subject: [PATCH 1/1] Filter packages by name in list-packages. (Bug#36981)

* lisp/emacs-lisp/package.el (package-menu-filter-by-name): New
function to filter packages by name.
(package-menu-clear-filter): New function to clear applied filters.
(package-menu-filter-by-keyword): Rename function from
package-menu-filter.
(package-menu--generate): Don't change 'q' binding anymore.
(package-menu-mode-map): Bind '/ n' to package-menu-filter-by-name, '/
k' to package-menu-filter-by-keyword and '/ /' to
package-menu-clear-filter.
(package-menu-mode-menu): Update menu entries for the three functions.
* test/lisp/emacs-lisp/package-tests.el (package-test-list-filter-by-name)
(package-test-list-clear-filter): New tests.
* doc/emacs/package.texi: Document usage of
package-menu-filter-by-name, package-menu-clear-filter and update
reference to package-menu-filter-by-keyword.
* etc/NEWS: Announce changes.
---
 doc/emacs/package.texi                | 19 ++++++++---
 etc/NEWS                              | 10 ++++++
 lisp/emacs-lisp/package.el            | 45 ++++++++++++++++++++-------
 test/lisp/emacs-lisp/package-tests.el | 24 +++++++++++++-
 4 files changed, 81 insertions(+), 17 deletions(-)

diff --git a/doc/emacs/package.texi b/doc/emacs/package.texi
index 2c09ca8902..d97648af1b 100644
--- a/doc/emacs/package.texi
+++ b/doc/emacs/package.texi
@@ -136,11 +136,20 @@ Package Menu
 the list of available packages from the package archive again, and
 recomputes the package list.
 
-@item f
-Filter the package list (@code{package-menu-filter}).  This prompts
-for a keyword (e.g., @samp{games}), then shows only the packages
-that relate to that keyword.  To restore the full package list,
-type @kbd{q}.
+@item / k
+Filter the package list by keyword
+(@code{package-menu-filter-by-keyword}).  This prompts for a keyword
+(e.g., @samp{games}), then shows only the packages that relate to that
+keyword.
+
+@item / n
+Filter the package list by name (@code{package-menu-filter-by-name}).
+This prompts for a string, then shows only the packages whose names
+match a regexp with that value.
+
+@item / /
+Clear filter currently applied to the package list
+(@code{package-menu-clear-filter}).
 
 @item H
 Permanently hide packages that match a regexp
diff --git a/etc/NEWS b/etc/NEWS
index cb8b6fcac1..015322c111 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -983,6 +983,16 @@ early init file.
 
 *** New function 'package-activate-all'.
 
++++
+*** New functions for filtering packages list
+A new function has been added which allows users to filter the
+packages list by name: 'package-menu-filter-by-name'.  By default, it
+is bound to '/ n'. Additionally, the function
+'package-menu-fiter-by-keyword' has been renamed from
+'package-menu-filter'.  Its keybinding has also been changed to '/ k'
+(from 'f').  To clear any of the two filters, the user can now call
+the 'package-menu-clear-filter' function, bound to '/ /' by default.
+
 ---
 *** Imenu support has been added to 'package-menu-mode'.
 
diff --git a/lisp/emacs-lisp/package.el b/lisp/emacs-lisp/package.el
index ab1fb8b90f..28e971044e 100644
--- a/lisp/emacs-lisp/package.el
+++ b/lisp/emacs-lisp/package.el
@@ -2667,7 +2667,9 @@ package-menu-mode-map
     (define-key map "i" 'package-menu-mark-install)
     (define-key map "U" 'package-menu-mark-upgrades)
     (define-key map "r" 'package-menu-refresh)
-    (define-key map "f" 'package-menu-filter)
+    (define-key map (kbd "/ k") 'package-menu-filter-by-keyword)
+    (define-key map (kbd "/ n") 'package-menu-filter-by-name)
+    (define-key map (kbd "/ /") 'package-menu-clear-filter)
     (define-key map "~" 'package-menu-mark-obsolete-for-deletion)
     (define-key map "x" 'package-menu-execute)
     (define-key map "h" 'package-menu-quick-help)
@@ -2699,7 +2701,11 @@ package-menu-mode-map
     ["Unmark" package-menu-mark-unmark :help "Clear any marks on a package and move to the next line"]
 
     "--"
-    ["Filter Package List" package-menu-filter :help "Filter package selection (q to go back)"]
+    ("Filter Packages"
+     ["Filter by Keyword" package-menu-filter-by-keyword :help "Filter packages by keyword"]
+     ["Filter by Name" package-menu-filter-by-name :help "Filter packages by name"]
+     ["Clear Filter" package-menu-clear-filter :help "Clear package list filter"])
+
     ["Hide by Regexp" package-menu-hide-package :help "Permanently hide all packages matching a regexp"]
     ["Display Older Versions" package-menu-toggle-hiding
      :style toggle :selected (not package-menu--hide-packages)
@@ -3010,9 +3016,6 @@ package-menu--generate
             (let ((filters (mapconcat #'identity keywords ",")))
               (concat "Package[" filters "]"))
           "Package"))
-  (if keywords
-      (define-key package-menu-mode-map "q" 'package-show-package-list)
-    (define-key package-menu-mode-map "q" 'quit-window))
   (tabulated-list-init-header)
   (tabulated-list-print remember-pos))
 
@@ -3640,9 +3643,7 @@ package-show-package-list
         (select-window win)
       (switch-to-buffer buf))))
 
-;; package-menu--generate rebinds "q" on the fly, so we have to
-;; hard-code the binding in the doc-string here.
-(defun package-menu-filter (keyword)
+(defun package-menu-filter-by-keyword (keyword)
   "Filter the *Packages* buffer.
 Show only those items that relate to the specified KEYWORD.
 
@@ -3653,9 +3654,7 @@ package-menu-filter
 KEYWORD can also be used to filter by status or archive name by
 using keywords like \"arc:gnu\" and \"status:available\".
 Statuses available include \"incompat\", \"available\",
-\"built-in\" and \"installed\".
-
-To restore the full package list, type `q'."
+\"built-in\" and \"installed\"."
   (interactive
    (list (completing-read-multiple
           "Keywords (comma separated): " (package-all-keywords))))
@@ -3663,6 +3662,30 @@ package-menu-filter
                                    (list keyword)
                                  keyword)))
 
+(defun package-menu-filter-by-name (name)
+  "Filter the *Packages* buffer by NAME.
+Show only those items whose name matches the regular expression
+NAME. If NAME is nil or the empty string, show all packages."
+  (interactive (list (read-from-minibuffer "Filter by name (regexp): ")))
+  (if (or (not name) (string-empty-p name))
+      (package-show-package-list t nil)
+    ;; Update `tabulated-list-entries' so that it contains all
+    ;; packages before searching.
+    (package-menu--refresh t nil)
+    (let (matched)
+      (dolist (entry tabulated-list-entries)
+        (let* ((pkg-name (package-desc-name (car entry))))
+          (when (string-match name (symbol-name pkg-name))
+            (push pkg-name matched))))
+      (if matched
+          (package-show-package-list matched nil)
+        (user-error "No packages found")))))
+
+(defun package-menu-clear-filter ()
+  "Clear any filter currently applied to the *Packages* buffer."
+  (interactive)
+  (package-menu--generate t t))
+
 (defun package-list-packages-no-fetch ()
   "Display a list of packages.
 Does not fetch the updated list of packages before displaying.
diff --git a/test/lisp/emacs-lisp/package-tests.el b/test/lisp/emacs-lisp/package-tests.el
index c757bccf67..a2ee9f06b1 100644
--- a/test/lisp/emacs-lisp/package-tests.el
+++ b/test/lisp/emacs-lisp/package-tests.el
@@ -28,7 +28,7 @@
 
 ;; Run this in a clean Emacs session using:
 ;;
-;;     $ emacs -Q --batch -L . -l package-test.el -l ert -f ert-run-tests-batch-and-exit
+;;     $ emacs -Q --batch -L . -l package-tests.el -l ert -f ert-run-tests-batch-and-exit
 
 ;;; Code:
 
@@ -360,6 +360,28 @@ package-test--compatible-p
       (should-not (re-search-forward "^\\s-+simple-single\\s-+1.3\\s-+\\(available\\|new\\)" nil t))
       (kill-buffer buf))))
 
+(ert-deftest package-test-list-filter-by-name ()
+  "Ensure package list is filtered correctly by package name."
+  (with-package-test ()
+    (let ((buf (package-list-packages)))
+      (package-menu-filter-by-name "tetris")
+      (goto-char (point-min))
+      (should (re-search-forward "^\\s-+tetris" nil t))
+      (should (= (count-lines (point-min) (point-max)) 1))
+      (kill-buffer buf))))
+
+(ert-deftest package-test-list-clear-filter ()
+  "Ensure package list filter is cleared correctly."
+  (with-package-test ()
+    (let ((buf (package-list-packages)))
+      (let ((num-packages (count-lines (point-min) (point-max))))
+        (should (> num-packages 1))
+        (package-menu-filter-by-name "tetris")
+        (should (= (count-lines (point-min) (point-max)) 1))
+        (package-menu-clear-filter)
+        (should (= (count-lines (point-min) (point-max)) num-packages)))
+      (kill-buffer buf))))
+
 (ert-deftest package-test-update-archives ()
   "Test updating package archives."
   (with-package-test ()
-- 
2.17.1