Another little piece from my Emacs config that I’ve decided to turn into a small post, following up on previous one. This time, we’re going to make tabs work as in most graphical editors.
Tabs were added with
global-tab-line-mode in Emacs 27, and are pretty
simple tabs, that are being displayed on the top of a window, and by
default their semantics are not very useful in my opinion. The main
problem is that you get only two policies of how closing of a tab will
behave. It is either
bury-buffer, which simply hides the tab, and
moves the buffer to the end of buffer list, and
always kills the buffer, and hides the tab, and you can’t really
combine those behaviors by default.
This is not very useful, because you can have many windows, with same buffers in each one, and when you want to close the window you can’t do that by closing last tab. For example, in Atom, if we have such configuration:
If we close
file-B tab we will be left with one window:
This is useful, and works the same way in VSCode or Sublime
Text. Let’s try the same thing in
emacs -q. First we create two
windows, with tab in each:
Then we press little close button on
And it replaces it with
file-A tab. This happens because when we click
on the close button it calls
bury-buffer, so the tab goes away, new
buffer is displayed, and we’re getting new tab for it. This is not
great. I think when we close last tab in the window, the window should
be killed as in Atom1.
The other thing is this. In Atom we can have two windows with the same buffers opened like this:
And if you close one of the tabs the file will not be closed until all tabs are closed, but window will be:
In Emacs, however, we can’t close tab via close button, because it will cycle buffers, so instead we can switch to second policy of killing buffer instead. But it will not work either, because it will not kill the window, and it will kill buffer in all windows. So how do we fix this?
First let’s establish some logical rules:
- Buffer can exist without a tab, but tab can not exist without a buffer,
- If tab was closed buffer should be killed only if there are no tabs for this buffer left,
- If tab was closed and there are no more tabs in the window, window should be killed.
These three rules are basically how tabs work in modern editors. Now we need to implement this in Emacs Lisp. Since there’s no way of configuring which function is used for closing a tab, we’re going to redefine it locally in our configuration file:
(defun tab-line-close-tab (&optional e) "Close the selected tab. If tab is presented in another window, close the tab by using `bury-buffer` function. If tab is uniq to all existing windows, kill the buffer with `kill-buffer` function. Lastly, if no tabs left in the window, it is deleted with `delete-window` function." (interactive "e") (let* ((posnp (event-start e)) (window (posn-window posnp)) (buffer (get-pos-property 1 'tab (car (posn-string posnp))))) (with-selected-window window (let ((tab-list (tab-line-tabs-window-buffers)) (buffer-list (flatten-list (seq-reduce (lambda (list window) (select-window window t) (cons (tab-line-tabs-window-buffers) list)) (window-list) nil)))) (select-window window) (if (> (seq-count (lambda (b) (eq b buffer)) buffer-list) 1) (progn (if (eq buffer (current-buffer)) (bury-buffer) (set-window-prev-buffers window (assq-delete-all buffer (window-prev-buffers))) (set-window-next-buffers window (delq buffer (window-next-buffers)))) (unless (cdr tab-list) (ignore-errors (delete-window window)))) (and (kill-buffer buffer) (unless (cdr tab-list) (ignore-errors (delete-window window))))))) (force-mode-line-update)))
Now let’s break this down. First we’re storing our
buffer, then with our selected
window we get list of tabs via call to
tab-line-tabs-window-buffers, which effectively returns a list of
buffers that have tabs associated with those. Then we get full list of
buffers in all windows and store it to
buffer-list variable, that we
will need later.
Next we select the
window, and count how many times we find buffer in
our list of buffers from all windows. If it is more than 1, than we
can we’re going to bury that buffer. If it is 1 we’re going to kill
it, since it is the last tab of that buffer.
There’s another check in there, that checks if the tab is last in the
list of tabs for that window, and if it is, e.g. if
nil we’ve just closed the last tab in the window, so we can
kill it as well.
In the end we force mode-line update, because it updates tab line as well. So now, with this function (and the rest of my config) let’s try previous examples in Emacs:
Now we close
file-B and its respective window both were killed. Now we
create windows with same buffer:
And if we close right tab, we see that window is deleted, but buffer is not affected:
And if we had tab
file-A in first window and two tabs for
file-B in second, closing
file-A in second window will not kill
file-A buffer, and will not kill the window, because tab for
file-A tab doesn’t kill
file-A buffer or window:
file-B tab kills its buffer and window:
This makes tabs in Emacs behave as in Atom or VSCode.
Atom actually has a setting for it, so when you close last tab it can display an empty window, in which you can later drag and drop new tabs, but in Emacs there are no such thing as empty windows, so it makes sense to always kill the window if there are no tabs left. ↩︎