Commit 8305d0e0 authored by Juri Linkov's avatar Juri Linkov
Browse files

Add tabulated-list-groups and Buffer-menu-group-by (bug#69305)

* 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.
parent e68f95e6
Pipeline #28381 failed with stages
in 45 minutes and 57 seconds
......@@ -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
......
......@@ -1356,6 +1356,11 @@ will return the URL for that bug.
This allows for rcirc logs to use a custom timestamp format, than the
chat buffers use by default.
---
*** 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;
......@@ -2070,6 +2075,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.
* Changes in Emacs 30.1 on Non-Free Operating Systems
......
......@@ -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
......@@ -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.
......
......@@ -130,4 +130,45 @@
(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
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment