There are several packages which have a switch-to-buffer
-like command that
also suggests recent files in addition to existing buffers. The oldest one it
probably the built-in ido.el
with its concept of "virtual buffers" you can
enable with (setq ido-use-virtual-buffers t)
. The
consult package's consult-buffer
commands
even go a step further and provide even more sources for "virtual buffers" next
to recent files, e.g., bookmarks, registers, project files, or home-brewn
sources.
I've used ido.el
back in the days and over multiple intermediate steps
settled on the typical and awesome completion combo of
vertico,
corfu, and
marginalia. I've also tried
consult for some time but its previews,
i.e., that it immediately shows the completion candidate in a buffer, is a bit
too dynamic for my taste. Same for its search capabilities like
consult-ripgrep
where I prefer a more static UI like that of
rg. That said, what I've immediately missed
after removing the consult
package is the support for recent files in the
(now again stock) switch-to-buffer
command. I'm so used to switching to
virtual buffers; I never visit files in projects I'm working using C-x C-f
because C-x b
always worked, too. I never had to ask myself "did I already
find that file in this emacs session?" and then choose the right command, i.e.,
find-file
or switch-to-buffer
.
However, reading the docs showed that since ages (Emacs 20.3) one can define a
custom read-buffer-function
which is then used by read-buffer
, the internal
workhorse of the commands switch-to-buffer
/ pop-to-buffer
. So the
question is how to sneak in recent files in addition to actual live buffers.
The answer for me was to use a completion table which tries to do buffer
completion and if that runs out of candidates, switch to a completion table for
recent files. Such a "try this and if it won't work, try that" completion
table can be composed from existing tables using completion-table-in-turn
.
That works great for me but won't do the trick if you have a buffer foo.txt
and also a different recent file foo.txt
. In such a case, when you rather
want a completion table composed of other tables which should have the same
priority, you can use completion-table-merge
instead of
completion-table-in-turn
.
Long story short, here's the code which I'm using now:
(defconst th/read-buffer-or-recentf-command-alist
'((kill-buffer buffers)
(switch-to-buffer buffers-except recentf)
(pop-to-buffer buffers-except recentf))
"Alist with entries of the form (CMD . COMPLETES).
COMPLETES is a list defining what's completed where entries can
be:
- `buffers': completion for all buffers
- `buffers-except': completion for all buffers except the current one
- `recentf': completion for recent files which will be found on demand")
(defun th/read-buffer-or-recentf (prompt &optional
def require-match predicate)
(let* ((tables (or
(mapcar
(lambda (syms)
(pcase syms
('buffers #'internal-complete-buffer)
('buffers-except (internal-complete-buffer-except
(current-buffer)))
('recentf (completion-table-dynamic
(lambda (s) recentf-list)))
(unknown (error "Unknown case %S" unknown))))
(cdr (assoc this-command
th/read-buffer-or-recentf-command-alist)))
(list #'internal-complete-buffer
(completion-table-dynamic
(lambda (s) recentf-list)))))
(completion-table (apply #'completion-table-in-turn tables)))
;; `read-buffer-to-switch' (called by `switch-to-buffer') already sets
;; `internal-complete-buffer' as `minibuffer-completion-table' using
;; `minibuffer-with-setup-hook' before `read-buffer-function' is invoked by
;; `read-buffer', so we'd be restricted to buffers by default. Therefore,
;; append a function setting our completion table.
(minibuffer-with-setup-hook
(:append (lambda ()
(setq-local minibuffer-completion-table completion-table)))
(when-let ((result (completing-read prompt completion-table
predicate require-match nil
'buffer-name-history def)))
(cond
((get-buffer result) result)
((file-exists-p result) (buffer-name (find-file-noselect result)))
(t result))))))
(setq read-buffer-function #'th/read-buffer-or-recentf)
This custom read-buffer-function
just arranges that a suitable completion
table is used, then does the completion using completing-read
and if the
result is no buffer but an existing file, it finds it and returns the
corresponding new buffer.
The defconst
th/read-buffer-or-recentf-command-alist
allows for defining
what's suggested by completion on a command by command basis. For example,
completion for recent files makes totally no sense for kill-buffer
while
completing the current buffer is not too sensible for switch-to-buffer
and
pop-to-buffer
. If there's no entry in the alist, the default behavior is to
offer completion for all buffers and then for recent files.
It's important to note that in contrast to virtual buffers in ido.el
or
consult
, this read-buffer-function
is in effect whenever emacs queries for
a buffer, not just in certain buffer switching commands. As an example, when
doing M-x append-to-buffer RET
now I can also complete a recent file instead
of an existing buffer which will be opened on demand. That's quite cool. A
caveat is, however, that the position where the current region will be appended
depends on where point has been when I visited that file the last time because
I use save-place-mode
. If I wouldn't use it, the region would be prepended
to the beginning of the file/buffer which would also be awkward. But I guess
the actual inconsistency is due to the fact that append-to-buffer
inserts at
the location of point
which doesn't quite fit to the "append" in its name.
Anyway, after a few days of using it, I must say, it works extremely well for
me. One thing which doesn't work completely is marginalia
annotations.
During completion, buffers are annotated as usual, but when recent file
completions kick in, those are not. I've tried making them respond to
metadata
requests which didn't work out because right now, emacs' completion
framework doesn't really support combining completion tables which have
different metadata
, e.g., one says the category
is buffer
, the other says
it's file
. Well, that's probably a sensible assumption in all cases but this
one. :-)
Back to the blog by tags or to the overview of all posts.
Also, you can subscribe to my rss feed.