]> git.eshelyaron.com Git - emacs.git/commitdiff
Add tabulated-list-groups and Buffer-menu-group-by (bug#69305)
authorJuri Linkov <juri@linkov.net>
Thu, 29 Feb 2024 17:50:04 +0000 (19:50 +0200)
committerEshel Yaron <me@eshelyaron.com>
Sat, 2 Mar 2024 06:31:54 +0000 (07:31 +0100)
* doc/lispref/modes.texi (Tabulated List Mode):
Add defvar tabulated-list-groups.

* lisp/buff-menu.el (Buffer-menu-group-by): New defcustom.
(Buffer-menu-unmark-all-buffers): Use tabulated-list-get-entry
to check whether the current line contains an entry.
(list-buffers-noselect): Enable outline-minor-mode
for tabulated-list-groups.
(list-buffers--refresh): When Buffer-menu-group-by is non-nil,
set tabulated-list-groups.
(Buffer-menu-group-by-mode, Buffer-menu-group-by-root):
New functions.

* lisp/emacs-lisp/tabulated-list.el (tabulated-list-groups):
New buffer-local variable.
(tabulated-list-print-fake-header): Add distinct overlay
property 'fake-header'.
(tabulated-list-header-overlay-p): Filter out overlays that
don't have the property 'fake-header'.
(tabulated-list-print): Use the variable 'tabulated-list-groups'
to sort entries in groups separately.
(tabulated-list-print-entries): New function factored out from
'tabulated-list-print'.

* test/lisp/emacs-lisp/tabulated-list-tests.el (tabulated-list-groups):
New test.

(cherry picked from commit 8305d0e0c909a5dd91a21cc1daea6298aae9eda7)

doc/lispref/modes.texi
etc/NEWS
lisp/buff-menu.el
lisp/emacs-lisp/tabulated-list.el
test/lisp/emacs-lisp/tabulated-list-tests.el

index 630e42e6878cce25ac45afffcaee5bbc89f20584..7a4a722d595b988ea8b47a1d5774a032c4b2cd40 100644 (file)
@@ -1246,6 +1246,41 @@ Otherwise, the value should be a function which returns a list of the
 above form when called with no arguments.
 @end defvar
 
+@defvar tabulated-list-groups
+This buffer-local variable specifies the groups of entries displayed in
+the Tabulated List buffer.  Its value should be either a list, or a
+function.
+
+If the value is a list, each list element corresponds to one group, and
+should have the form @w{@code{(@var{group-name} @var{entries})}}, where
+@var{group-name} is a string inserted before all group entries, and
+@var{entries} have the same format as @code{tabulated-list-entries}
+(see above).
+
+Otherwise, the value should be a function which returns a list of the
+above form when called with no arguments.
+
+You can use @code{seq-group-by} to create @code{tabulated-list-groups}
+from @code{tabulated-list-entries}.  For example:
+
+@smallexample
+@group
+  (setq tabulated-list-groups
+        (seq-group-by 'Buffer-menu-group-by-mode
+                      tabulated-list-entries))
+@end group
+@end smallexample
+
+where you can define @code{Buffer-menu-group-by-mode} like this:
+
+@smallexample
+@group
+(defun Buffer-menu-group-by-mode (entry)
+  (concat "* " (aref (cadr entry) 5)))
+@end group
+@end smallexample
+@end defvar
+
 @defvar tabulated-list-revert-hook
 This normal hook is run prior to reverting a Tabulated List buffer.  A
 derived mode can add a function to this hook to recompute
index 9f59daa238603e00250fbbaf9a096fc2eee21ff5..ca241478f92ff04591302aa3ac5be16b3d48f23c 100644 (file)
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -1468,6 +1468,11 @@ This user option lets you customize the sample text that
 "*Completions*" buffer.  By default this is set to the string
 "SAMPLE", which retains compatibility with Emacs 29.
 
+---
+*** New user option 'Buffer-menu-group-by'.
+It splits buffers by groups that are displayed with headings
+in Outline minor mode.
+
 ---
 *** New command 'Buffer-menu-toggle-internal'.
 This command toggles the display of internal buffers in Buffer Menu mode;
@@ -2226,6 +2231,10 @@ inside 'treesit-language-source-alist', so that calling
 It may be useful, for example, for the purposes of bisecting a
 treesitter grammar.
 
++++
+** New buffer-local variable 'tabulated-list-groups'.
+It prints and sorts the groups of entries separately.
+
 \f
 * Changes in Emacs 30.1 on Non-Free Operating Systems
 
index e13c3b56b4e051a3aa1f3a3fd4d310a0b62a6b0a..1d52feb5733012e07cc301340a344ac5e57041c7 100644 (file)
@@ -95,6 +95,25 @@ as it is by default."
   :group 'Buffer-menu
   :version "22.1")
 
+(defcustom Buffer-menu-group-by nil
+  "If non-nil, buffers are grouped by function.
+This function takes one argument: a list of entries in the same format
+as in `tabulated-list-entries', and should return a list in the format
+suitable for `tabulated-list-groups'.  Also when this variable is non-nil,
+then `outline-minor-mode' is enabled in the Buffer Menu.  Then with the
+default value of `outline-regexp' you can use Outline minor mode commands
+to show/hide groups of buffers.
+The default options can group by a mode, and by a root directory of
+a project or just `default-directory'."
+  :type '(choice (const :tag "No grouping" nil)
+                 (function-item :tag "Group by mode"
+                                Buffer-menu-group-by-mode)
+                 (function-item :tag "Group by project root or directory"
+                                Buffer-menu-group-by-root)
+                 (function :tag "Custom function"))
+  :group 'Buffer-menu
+  :version "30.1")
+
 (defvar-local Buffer-menu-files-only nil
   "Non-nil if the current Buffer Menu lists only file buffers.
 This is set by the prefix argument to `buffer-menu' and related
@@ -408,14 +427,12 @@ When called interactively prompt for MARK;  RET remove all marks."
   (interactive "cRemove marks (RET means all):" Buffer-menu-mode)
   (save-excursion
     (goto-char (point-min))
-    (when (tabulated-list-header-overlay-p)
-      (forward-line))
     (while (not (eobp))
-      (let ((xmarks (list (aref (tabulated-list-get-entry) 0)
-                          (aref (tabulated-list-get-entry) 2))))
-        (when (or (char-equal mark ?\r)
-                  (member (char-to-string mark) xmarks))
-          (Buffer-menu--unmark)))
+      (when-let ((entry (tabulated-list-get-entry)))
+        (let ((xmarks (list (aref entry 0) (aref entry 2))))
+          (when (or (char-equal mark ?\r)
+                    (member (char-to-string mark) xmarks))
+            (Buffer-menu--unmark))))
       (forward-line))))
 
 (defun Buffer-menu-unmark-all ()
@@ -674,7 +691,12 @@ See more at `Buffer-menu-filter-predicate'."
       (setq Buffer-menu-buffer-list buffer-list)
       (setq Buffer-menu-filter-predicate filter-predicate)
       (list-buffers--refresh buffer-list old-buffer)
-      (tabulated-list-print))
+      (tabulated-list-print)
+      (when tabulated-list-groups
+        (setq-local outline-minor-mode-cycle t
+                    outline-minor-mode-highlight t
+                    outline-minor-mode-use-buttons 'in-margins)
+        (outline-minor-mode 1)))
     buffer))
 
 (defun Buffer-menu-mouse-select (event)
@@ -750,7 +772,11 @@ See more at `Buffer-menu-filter-predicate'."
                  `("Mode" ,Buffer-menu-mode-width t)
                  '("File" 1 t)))
     (setq tabulated-list-use-header-line Buffer-menu-use-header-line)
-    (setq tabulated-list-entries (nreverse entries)))
+    (setq tabulated-list-entries (nreverse entries))
+    (when Buffer-menu-group-by
+      (setq tabulated-list-groups
+            (seq-group-by Buffer-menu-group-by
+                          tabulated-list-entries))))
   (tabulated-list-init-header))
 
 (defun tabulated-list-entry-size-> (entry1 entry2)
@@ -769,4 +795,14 @@ See more at `Buffer-menu-filter-predicate'."
          (abbreviate-file-name list-buffers-directory))
        (t "")))
 
+(defun Buffer-menu-group-by-mode (entry)
+  (concat "* " (aref (cadr entry) 5)))
+
+(declare-function project-root "project" (project))
+(defun Buffer-menu-group-by-root (entry)
+  (concat "* " (with-current-buffer (car entry)
+                 (if-let ((project (project-current)))
+                     (project-root project)
+                   default-directory))))
+
 ;;; buff-menu.el ends here
index 9884a2fc24b91cc92a15cb735a96fcdce9ece128..c86e3f9c5dfbf034c0833d6ce8d83bcffd06fbd9 100644 (file)
@@ -139,6 +139,21 @@ If `tabulated-list-entries' is a function, it is called with no
 arguments and must return a list of the above form.")
 (put 'tabulated-list-entries 'permanent-local t)
 
+(defvar-local tabulated-list-groups nil
+  "Groups displayed in the current Tabulated List buffer.
+This should be either a function, or a list.
+If a list, each element has the form (GROUP-NAME ENTRIES),
+where:
+
+ - GROUP-NAME is a group name as a string, which is displayed
+   at the top line of each group.
+
+ - ENTRIES is a list described in `tabulated-list-entries'.
+
+If `tabulated-list-groups' is a function, it is called with no
+arguments and must return a list of the above form.")
+(put 'tabulated-list-groups 'permanent-local t)
+
 (defvar-local tabulated-list-padding 0
   "Number of characters preceding each Tabulated List mode entry.
 By default, lines are padded with spaces, but you can use the
@@ -362,15 +377,17 @@ Do nothing if `tabulated-list--header-string' is nil."
       (if tabulated-list--header-overlay
           (move-overlay tabulated-list--header-overlay (point-min) (point))
         (setq-local tabulated-list--header-overlay
-                    (make-overlay (point-min) (point))))
-      (overlay-put tabulated-list--header-overlay
-                   'face 'tabulated-list-fake-header))))
+                    (make-overlay (point-min) (point)))
+        (overlay-put tabulated-list--header-overlay 'fake-header t)
+        (overlay-put tabulated-list--header-overlay
+                     'face 'tabulated-list-fake-header)))))
 
 (defsubst tabulated-list-header-overlay-p (&optional pos)
   "Return non-nil if there is a fake header.
 Optional arg POS is a buffer position where to look for a fake header;
 defaults to `point-min'."
-  (overlays-at (or pos (point-min))))
+  (seq-find (lambda (o) (overlay-get o 'fake-header))
+            (overlays-at (or pos (point-min)))))
 
 (defun tabulated-list-revert (&rest _ignored)
   "The `revert-buffer-function' for `tabulated-list-mode'.
@@ -427,6 +444,9 @@ This sorts the `tabulated-list-entries' list if sorting is
 specified by `tabulated-list-sort-key'.  It then erases the
 buffer and inserts the entries with `tabulated-list-printer'.
 
+If `tabulated-list-groups' is non-nil, each group of entries
+is printed and sorted separately.
+
 Optional argument REMEMBER-POS, if non-nil, means to move point
 to the entry with the same ID element as the current line.
 
@@ -437,6 +457,9 @@ be removed from entries that haven't changed (see
 `tabulated-list-put-tag').  Don't use this immediately after
 changing `tabulated-list-sort-key'."
   (let ((inhibit-read-only t)
+        (groups (if (functionp tabulated-list-groups)
+                   (funcall tabulated-list-groups)
+                 tabulated-list-groups))
        (entries (if (functionp tabulated-list-entries)
                     (funcall tabulated-list-entries)
                   tabulated-list-entries))
@@ -447,7 +470,14 @@ changing `tabulated-list-sort-key'."
         (setq saved-col (current-column)))
     ;; Sort the entries, if necessary.
     (when sorter
-      (setq entries (sort entries sorter)))
+      (if groups
+          (setq groups
+                (mapcar (lambda (group)
+                          (cons (car group) (sort (cdr group) sorter)))
+                        groups))
+        (setq entries (sort entries sorter))))
+    (unless (functionp tabulated-list-groups)
+      (setq tabulated-list-groups groups))
     (unless (functionp tabulated-list-entries)
       (setq tabulated-list-entries entries))
     ;; Without a sorter, we have no way to just update.
@@ -459,6 +489,25 @@ changing `tabulated-list-sort-key'."
       (unless tabulated-list-use-header-line
         (tabulated-list-print-fake-header)))
     ;; Finally, print the resulting list.
+    (if groups
+        (dolist (group groups)
+          (insert (car group) ?\n)
+          (when-let ((saved-pt-new (tabulated-list-print-entries
+                                    (cdr group) sorter update entry-id)))
+            (setq saved-pt saved-pt-new)))
+      (setq saved-pt (tabulated-list-print-entries
+                      entries sorter update entry-id)))
+    (when update
+      (delete-region (point) (point-max)))
+    (set-buffer-modified-p nil)
+    ;; If REMEMBER-POS was specified, move to the "old" location.
+    (if saved-pt
+       (progn (goto-char saved-pt)
+              (move-to-column saved-col))
+      (goto-char (point-min)))))
+
+(defun tabulated-list-print-entries (entries sorter update entry-id)
+  (let (saved-pt)
     (while entries
       (let* ((elt (car entries))
              (tabulated-list--near-rows
@@ -495,14 +544,7 @@ changing `tabulated-list-sort-key'."
               (forward-line 1)
               (delete-region old (point))))))
       (setq entries (cdr entries)))
-    (when update
-      (delete-region (point) (point-max)))
-    (set-buffer-modified-p nil)
-    ;; If REMEMBER-POS was specified, move to the "old" location.
-    (if saved-pt
-       (progn (goto-char saved-pt)
-              (move-to-column saved-col))
-      (goto-char (point-min)))))
+    saved-pt))
 
 (defun tabulated-list-print-entry (id cols)
   "Insert a Tabulated List entry at point.
index 8be2be3139e9e2d063cbcf70d0802d93b9ec07b4..e53268b3f14dc3bd55a4f0fc248ba707c74ad8bc 100644 (file)
    (should-error (tabulated-list-sort) :type 'user-error)
    (should-error (tabulated-list-sort 4) :type 'user-error)))
 
+(ert-deftest tabulated-list-groups ()
+  (with-temp-buffer
+    (tabulated-list-mode)
+    (setq tabulated-list-groups
+          (reverse
+           (seq-group-by (lambda (b) (concat "* " (aref (cadr b) 3)))
+                         tabulated-list--test-entries)))
+    (setq tabulated-list-format tabulated-list--test-format)
+    (setq tabulated-list-padding 7)
+    (tabulated-list-init-header)
+    (tabulated-list-print)
+    ;; Basic printing.
+    (should (string-equal
+             (buffer-substring-no-properties (point-min) (point-max))
+             "\
+* installed
+       zzzz-game  zzzz-game  2113      installed   play zzzz in Emacs
+       mode       mode       1128      installed   A simple mode for editing Actionscript 3 files
+* available
+       abc-mode   abc-mode   944       available   Major mode for editing abc music files
+* obsolete
+       4clojure   4clojure   1507      obsolete    Open and evaluate 4clojure.com questions
+"))
+    ;; Sort and preserve position.
+    (forward-line 2)
+    (let ((pos (thing-at-point 'line)))
+      (tabulated-list-next-column 2)
+      (tabulated-list-sort)
+      (should (equal (thing-at-point 'line) pos))
+      (should (string-equal
+               (buffer-substring-no-properties (point-min) (point-max))
+               "\
+* installed
+       mode       mode       1128      installed   A simple mode for editing Actionscript 3 files
+       zzzz-game  zzzz-game  2113      installed   play zzzz in Emacs
+* available
+       abc-mode   abc-mode   944       available   Major mode for editing abc music files
+* obsolete
+       4clojure   4clojure   1507      obsolete    Open and evaluate 4clojure.com questions
+")))))
+
 ;;; tabulated-list-tests.el ends here