Fixing horizontal scrolling in tabulated-list-mode
Yeah, it's gonna be a specific one. TL;DR; tabulated-list-hscroll implements
some fixes for horizontal srcolling in tabulated-list-mode.
In Emacs, there's a special mode called tabulated-list-mode used for displaying
tabulated data. vtable is a more recent alternative, but tabulated-list-mode is
still used fairly ubiquitously, for example in package-list-packages,
disk-usage, elpaca, etc.
I've been working with tabulated-list-mode for an Emacs MS SQL browser1,
and I noticed something: the header line, where column names are displayed,
doesn't scroll horizontally with the window.
And good lord, does it bother me. It's a table. The column names should align with the data.
I've worked horizontal scrolling into header-lines before, at least naively. All you need to do is take a substring with the hscroll amount:
(setq header-line-format '(:eval (substring "Yeah so this is a really long header line just to illustrate a point, pay me no mind. Anyway, you should see it scroll with the window horizontally." (window-hscroll))))
To wrap an existing header-line, you can do something like:
(setq header-line-format `(:eval (substring (format-mode-line ,header-line-format) (window-hscroll))))
Easy enough, but… nope, this doesn't work in tabulated-list-mode. This is is
because it concatenates column names in the header line like this, then uses
"specified spaces" via text propreties to align to the columns:
column1 column2 colmun3 ...
Which is neat-o and cool, but it breaks my dumb header line hscroll fix. vtable
actually constructs the header-line similarly.
Because I don't have a more clever solution, I'll just have to make the header line dumber.
The plan is to replace the spaces with special alignment display properties with actual space characters in the string. Here's what I came up with. First, a function that takes a propertized string and replaces specified spaces with real spaces:
(defun tabulated-list-hscroll-normalize-spaces (str) "Replace specified spaces in STR with regular spaces. See Info node `(elisp)Specified Space'." (with-temp-buffer (insert " ") (insert str) (goto-char (point-min)) (let ((start)) (while (setq start (next-single-property-change (point) 'display)) (goto-char start) (let* ((end (or (next-single-property-change (point) 'display) (point-max))) (value (get-text-property start 'display))) (when (and (listp value) (eql (car value) 'space) (eql (cadr value) :align-to)) (let ((num-spaces (- (eval (caddr value)) (- start 2)))) (delete-region start end) (insert (make-string (if (> num-spaces 0) num-spaces 0) ? )) ;; Move back in case there's another specified space immediately ;; after (when (> 0 num-spaces) (backward-char 1)))))) (buffer-substring 1 (point-max)))))
Gross. I'm sure I could have done that more elegantly. Moving on.
Then we just have to advise the tabulated-list-mode function that initializes
the header-line, calling our function to modify the header after it's
done2:
(define-advice tabulated-list-init-header (:after (&rest _) tabulated-list-hscroll-normalize-spaces) "Replace `tabulated-list-mode' header-line with a version normalized using `tabulated-list-hscroll-normalize-spaces' that scrolls with the window." (let* ((header-string (if (stringp header-line-format) header-line-format (caddr header-line-format))) (normalized-header (tabulated-list-hscroll-normalize-spaces header-string)) (segment `(:eval (substring ,normalized-header (window-hscroll))))) (if (stringp header-line-format) (setq header-line-format segment) (setf (caddr header-line-format) segment))))
Oh, but sorting by a column is resetting the hscroll. So let's advise the column sort function to restore it afterward:
(define-advice tabulated-list-col-sort (:around (fn &optional e) tabulated-list-hsrcoll-restore-hscroll) "Restore hscroll after sorting a `tabulated-list-mode' buffer by column." (let ((hscroll (window-hscroll))) (save-excursion (funcall fn e)) (set-window-hscroll nil hscroll)))
Great. Tables that stay aligned, and stay put.
Footnotes:
I wouldn't choose to work with MS SQL, but since it's what we use at
work, why not make it easier to use in Emacs? I may give vtable another try
later, but I think I actually prefer tabulated-list-mode for this.
The header-line-format in tabulated-list-mode in recent emacs is three
components, with the last being the special string. In older versions of Emacs,
it's just the string. Hence the stringp and caddr shenanigans.