VS Code's preview tab, for Emacs.
Browsing a project leaves a trail of buffers behind. Every file you glanced at
from the file tree, every definition you jumped to, every grep hit you opened
sits in the buffer list forever, and C-x b slowly turns into an archive of
things you looked at once.
VS Code solves this with the preview tab: a file opened by browsing goes into
a single italicised tab, the next such file replaces it, and it only becomes a
real tab once you edit it. preview-tab-mode brings that behaviour to Emacs.
(preview-tab-mode 1)That's it. Now opening a file from Dired, Treemacs, Magit, xref, grep results
or consult puts it in one temporary buffer, marked in the mode line. Open the
next one and the previous is gone.
A buffer becomes a preview when a command in preview-tab-commands visits a
file that was not already open.
It stops being a preview — permanently — when you edit it (the first change
promotes it) or run preview-tab-keep. A file the command has already
written into, as Org capture does with a new note, is never a preview at all.
The preview is killed when the next preview takes its place, unless it is modified, visible in another window, or running a process. Those quietly become ordinary buffers instead.
Two things deliberately do not happen, both matching VS Code:
- A buffer that was already open is never demoted to a preview.
- Visiting an ordinary buffer does not close the standing preview. It survives until another preview replaces it, not until you look away.
find-file is not a preview source: typing a file name is a deliberate act,
and VS Code does not preview from Quick Open either. To peek at a named file,
use preview-tab-find-file — or turn the rule off, see
Previewing named files.
| Command | What it does |
|---|---|
preview-tab-mode |
Turn the whole thing on or off. Turning it off makes every preview permanent. |
preview-tab-find-file |
Visit a file as a preview — the "just let me look at it" counterpart to find-file. Files already open are left alone. |
preview-tab-keep |
Keep the current preview buffer for good. |
Suggested bindings:
(global-set-key (kbd "C-c f t") #'preview-tab-find-file)
(global-set-key (kbd "C-c t p") #'preview-tab-keep)On MELPA. No dependencies beyond Emacs 27.1.
M-x package-install RET preview-tab RET, or with use-package:
(use-package preview-tab
:ensure t
:config (preview-tab-mode 1))The same works with elpaca; for straight.el, write :straight t instead of
:ensure t.
Doom's own jump and search commands are in the default list, so nothing beyond the usual two blocks is needed:
;; packages.el
(package! preview-tab)
;; config.el
(use-package! preview-tab
:config (preview-tab-mode 1))Then run doom sync and restart Emacs.
That covers +lookup/definition, +lookup/references, +lookup/implementations
and +lookup/type-definition, along with the +default/search-* family and the
+vertico/, +ivy/ and +helm/ project searches they dispatch to.
+lookup/file is left out, like find-file: pointing at a path and asking for
it is a deliberate act. It comes along with
preview-tab-include-named-files if you want it.
One more Doom default is worth changing. Out of the box, consult shows you the
file under point as you move down the candidate list; Doom turns that off, so
nothing appears until you press C-SPC on a candidate. To get the automatic
display back:
(after! consult
(consult-customize
consult-ripgrep consult-git-grep consult-grep consult-recent-file
consult-source-recent-file consult-source-project-recent-file
+default/search-project +default/search-cwd
:preview-key '(:debounce 0.2 any)))That is consult's own preview, not this package's: it only affects what you see
while still choosing, and consult closes those buffers itself when the session
ends. preview-tab is unaffected either way — the file you finally pick becomes
a preview if the command that opened it is in preview-tab-commands.
| Option | Default | Meaning |
|---|---|---|
preview-tab-commands |
Dired, Treemacs, Magit, xref, compile/grep, flymake, consult, Doom | Commands whose file visits are previews. |
preview-tab-include-named-files |
nil | Whether find-file, magit-find-file, … preview too. |
preview-tab-slant-faces |
vanilla, doom-modeline, centaur-tabs faces | Faces italicised while a buffer is a preview. tab-line-mode is handled separately, by preview-tab-tab-line-face. |
preview-tab-indicator |
auto |
auto, icon, label, both, or nil. |
preview-tab-icon |
"nf-md-eye_outline" |
nerd-icons Material Design icon name. |
preview-tab-label |
"PREVIEW" |
Text marker. |
RET on a file in a Magit status or diff buffer runs magit-diff-visit-file,
a preview source out of the box — as are its worktree, other-window and
other-frame variants.
One caveat, and it is Magit's design rather than this package's:
magit-diff-visit-file does not always visit a file. Which side of the diff
it opens depends on the line point is on — an added or context line gives the
new side, a removed line the old one. For a committed or staged change both
sides are blobs, from a commit or from the index; a blob buffer is read-only,
visits nothing on disk, and is left alone. Untracked files, and unstaged changes
seen from an added or context line, open the real worktree file — as does
magit-diff-visit-worktree-file (C-j, or C-<return>) from anywhere. Those
become previews.
Commands that open a file you named — find-file, magit-find-file,
org-roam-node-find, org-open-at-point… — are not preview sources. If you'd
rather they were:
(setopt preview-tab-include-named-files t)setopt arrives in Emacs 29. Before that — here and everywhere else this README
reaches for it — customize-set-variable does the same job:
(customize-set-variable 'preview-tab-include-named-files t).
In Doom, add :custom (preview-tab-include-named-files t) to the use-package!
block under Doom Emacs rather than writing a second one. :custom
goes through customize-set-variable, which is the machinery this option needs,
and it is applied before :config, so the mode comes up already knowing.
That takes in all of them, along with the other-window and other-frame variants
of the first two. It also reaches further than the commands it names: a great
deal of Emacs opens files by calling find-file, so project-find-file,
recentf-open-files and file registers start previewing too. Anything going
through find-file-noselect instead is untouched — bookmarks, for one.
org-roam-node-find goes that way too, which is why the option names it
outright. A title with no note behind it starts a capture instead, and the new
note is kept as an ordinary buffer.
I run with this on myself: the wider reach turns out to be the same instinct one step out, not a surprise in kind.
For find-file alone, leave the option off and list it where everything else
goes:
(setopt preview-tab-commands (cons #'find-file preview-tab-commands))Listing a command from a package you have not installed is harmless — the advice attaches to the bare symbol and only fires if that package loads. Add things freely, as long as you do it before turning the mode on:
(add-to-list 'preview-tab-commands #'my-jump-to-thing)
(preview-tab-mode 1)Afterwards add-to-list won't reattach the advice. Go through the customize
machinery instead, which will:
(setopt preview-tab-commands (cons #'my-jump-to-thing preview-tab-commands))One limit: only files opened while the command itself runs are picked up. A
command that hands the visit off to a timer or post-command-hook has already
returned by the time the file appears.
The preview buffer's path is italicised and a small marker is shown: an eye icon
with nerd-icons installed, otherwise the text PREVIEW. Set
preview-tab-indicator to both for icon and label, or to nil for italics
only — closest to what VS Code actually does. preview-tab-icon takes any name
from the nerd-icons Material Design set; one it has no icon for draws nothing
and says so in *Warnings*. If your mode line isn't italicised, add its faces
to preview-tab-slant-faces.
Under tab-line-mode the preview's tab is italicised as well, and stays that
way when you select another tab — the VS Code look. This is done by
preview-tab-tab-line-face, which the mode adds to
tab-line-tab-face-functions, and it needs Emacs 28.1, where that hook
arrived. On Emacs 27 the tabs are not slanted; the mode-line marker is
unaffected.
consult kills the buffers it opens while you scroll through candidates.
preview-tab picks up at the file you actually chose, which consult keeps.
make compile # byte-compile, warnings are errors
make lint # package-lint + checkdoc
make test # behavioural suite
make test-tty # frame-dependent suite
make allThe suite is split in two because emacs --batch has no frame, and
format-mode-line returns the empty string for every input there. Mode-line
tests therefore run on a terminal frame under a pty via util-linux script(1)
(BSD and macOS ship a different program under that name; the target checks up
front). To exercise the icon tests, point make at a nerd-icons checkout:
make test-tty NERD_ICONS=~/.emacs.d/elpa/nerd-iconsGPL-3.0-or-later. See LICENSE.
