Skip to main content

Shortcuts for hidden VIM buffers

This is a draft — please don't share

INTRO INTRO is its ability to juggle multiple buffers. If you enable it in your .vimrc (and you should!), you can even hide buffers that have unsaved changes.

This is a powerful feature. It lets you have work in progress on more files than will fit on your screen, without breaking your concentration to stop and save every time you want to switch to a different file.

Vim provides some commands for juggling multiple buffers. And there are useful plugins that provide more. (BufTabline will play a big role in this post.) But at some point, you’ll want to write your own. This tutorial shows how to write your own scripts that follow these patterns:

  • Act on a buffer — for instance, to view it in the current window, unload it, save it, or rever it.
  • Act on a window where a buffer is displayed — for instance, to jump to that window.
  • Act on every window where a buffer is displayed — for instance, to close them all.

I’ll also show you how to replace Vim’s not-so-user-friendly system of buffer numbers — where the first buffer open could easily be buffer number 376 — with something more intuitive, by tapping into BufTabline’s alternative system for your own use.

Setup

This tutorial leans heavily on the BufTabline plugin, and asks you to make changes to your .vimrc if you want to follow along. You probably shouldn’t make permanent changes to your .vimrc just because a strange lady on the internet told you to. Either save a copy, or start a new throwaway .vimrc and run Vim with it by launching it as follows.

> vim -u my-throwaway-vimrc

How you install BufTabline depends on how you’re managing Vim packages.

Add this to the appropriate part of your .vimrc
 Plugin 'ap/vim-buftabline' 
Then, restart Vim and install plugins.
    :PluginInstall
Add this to the appropriate part of your .vimrc
 Plug 'ap/vim-buftabline' 
Then, restart Vim and install plugins.
    :PlugInstall

Fake buffer numbers

The biggest challenge in extending BufTabline is that the buffer numbers it uses are fake.

That is, Vim has its own internal buffer numbers. (I’m going to write these internal buffer numbers in red.) These are guaranteed to be unique throughout a session. Suppose you open example.txt and Vim gives it the buffer number 1.

[img]

Suppose, now, that you close it and open second_example.txt. Even though there is only one buffer open, Vim doesn’t reuse the buffer number 1; it assigns second_example.txt to buffer number 2.

[img]

This means Vim’s real buffer numbers get large quickly. And it means they’re not terribly useful in keybindings. If buffer_that_i_opened_at_the_end_of_a_long_day.txt is the first of the open buffers, it’s annoying to constantly refer to it in keybindings as 247.


So BufTabline establishes a system of fake buffer numbers, which it calls “ordinal numbers.” The first of the open buffers will always have an ordinal number of 1, no matter how many other buffers were opened and closed before it.

For ease of reference, let’s make ordinal buffer numbers visible at the top of the screen. Add this to your .vimrc:

let g:buftabline_indicators=1
let g:buftabline_numbers=2

Let’s also make a keybinding that will show and hide the real buffer numbers, in case we need to peek at thim. Add this to your .vimrc; now, if your local leader key is \, then typing \n shows the real buffer numbers — in bold font, so you know they’re fake — and typing \n again restores the convenient fake ones.

function! SwapBuftablineNumbers()
  if g:buftabline_numbers ==? 2
    let g:buftabline_numbers=1
    highlight BufTabLineCurrent gui=bold 
    highlight BufTabLineActive  gui=bold 
    highlight BufTabLineHidden  gui=bold 
    highlight BufTabLineFill    gui=bold  
  else
    let g:buftabline_numbers=2
    highlight BufTabLineCurrent gui=none 
    highlight BufTabLineActive  gui=none 
    highlight BufTabLineHidden  gui=none 
    highlight BufTabLineFill    gui=none  
  endif
  :call buftabline#update(0)
endfunction

nnoremap <localleader>n :call SwapBuftablineNumbers()<CR>

Restart Vim (or :source $MYVIMRC to update its settings while it runs). Now, if you keep opening and closing buffers, you’ll find that buffer_that_i_opened_at_the_end_of_a_long_day.txt gets a fake number based only on the buffers that are open. If it’s the second open buffer, it appears with ordinal number 2. And if you enter \n to show the real numbers, you’ll see that its real number is higher.

Sandbox

Let’s set up an environment to play in.

At the command prompt, enter

> vim a.txt b.txt. c.txt d.txt

Vim starts up with four throwaway buffers open.

Now unload two of them. Enter :bd to unload the buffer containing a.txt, and enter :bd again to unload the buffer containing b.txt. Now, here’s what’s left:

File name Real number Ordinal number
c.txt 3 1
d.txt 4 2

In the tabline, the ordinal numbers are what appears, so at the top of the scree we see:

1 c.txt  2 d.txt

To visit a buffer by its real number, use the :b command: Entering :b3 visits c.txt, and entering :b4 visits d.txt.

What about visiting by ordinal number? Now we have two steps — first, convert the ordinal number into a real number, and next, visit the buffer by its real number.

BufTabline provides us a table to use for the first step: enter :echo buftabline#user_buffers() and you should see displayed the list [3,4] — meaning that ordinal number 1 corresponds to real buffer number 3 and ordinal number 2 corresponds to real buffer number 4. So now we can get the real buffer number of the nth-in-order buffer by looking up n-1 on this list. Get the real buffer number of the second-in-order buffer.

:echo get(buftabline#user_buffers(), 2-1)
4

Sure enough, its real number is 4.

Let’s define a function that will take care of this. Add this to your .vimrc and reload it or restart Vim.

function! RealBufNr(fake_nr)
  return get(buftabline@user_buffers(), a:n-1)
endfunction

Now we can pass this number to :b. Enter this code

:exe ':b' . RealBufNr(1)

to visit the buffer with ordinal number 1, and enter this

:exe ':b' . RealBufNr(2)

to visit the buffer with ordinal number 2.

For daily use, we can set up keybindings that call that code. Add this to your .vimrc.

nnoremap <leader>1 :exe ':b' . RealBufNr(1)<CR>
nnoremap <leader>2 :exe ':b' . RealBufNr(2)<CR>
nnoremap <leader>3 :exe ':b' . RealBufNr(3)<CR>
nnoremap <leader>4 :exe ':b' . RealBufNr(4)<CR>
nnoremap <leader>5 :exe ':b' . RealBufNr(5)<CR>
nnoremap <leader>6 :exe ':b' . RealBufNr(6)<CR>
nnoremap <leader>7 :exe ':b' . RealBufNr(7)<CR>
nnoremap <leader>8 :exe ':b' . RealBufNr(8)<CR>
nnoremap <leader>9 :exe ':b' . RealBufNr(9)<CR>

Well, this does the same thing as the keybindings from the last section. (Indeed, you can look at the source code and confirm that it does exactly the same thing.) But now we’re doing it from scratch — which means we know what we need to know to hack together different behaviours.

Go to window

If you work regularly with many windows on your screen, it’s easy to “lose” a buffer — you can see from the tabline that it’s open, and that it’s visible somewhere, but you’re not positive where. So you might want a keybinding that moves your cursor to whatever window is currently viewing the buffer.

This requires another step. After going from ordinal buffer number to real buffer number, we must then get the window number where that buffer is in view. Vim provides a function for this: bufwinnr(), which returns the first window number where the buffer is in view, or -1 if it’s not in view anywhere.

At the command prompt, enter

> vim a.txt b.txt. c.txt d.txt

Vim starts up with four throwaway buffers open. Now,

:bd
:bd
:vsplit

The resulting session has two buffers, each with its own real number and ordinal number, and two windows. Check the real number of the first buffer:

:echo RealBufNr(1)
3

Vim confirms that its real number is 3. Now check the window number:

:echo bufwinnr(3)
1

This buffer is open in two windows; but Vim only tells us about the first of them. Its window number is 1. And since the other buffer is not open anywhere, bufwinnr() returns -1 for it.

:echo bufwinnr(4)
-1

That last detail is important. We need to be prepared for bufwinnr() to signal failure by giving us something that isn’t a valid window number at all, and that might trigger strange behaviors in functions that are expecting a window number.

Go to a buffer by ordinal number

BufTabline provides builtin commands to go to a buffer by its ordinal number. To go to the first buffer in order, use <Plug>BufTabLine.Go(1). It’s convenient to create keybindings for the first nine buffers.

nmap <localleader>1 <Plug>BufTabLine.Go(1)
nmap <localleader>2 <Plug>BufTabLine.Go(2)
nmap <localleader>3 <Plug>BufTabLine.Go(3)
nmap <localleader>4 <Plug>BufTabLine.Go(4) 
nmap <localleader>5 <Plug>BufTabLine.Go(5)
nmap <localleader>6 <Plug>BufTabLine.Go(6)
nmap <localleader>7 <Plug>BufTabLine.Go(7)
nmap <localleader>8 <Plug>BufTabLine.Go(8)
nmap <localleader>9 <Plug>BufTabLine.Go(9)

Act on a buffer by ordinal number

Suppose I want a keybinding that will unload a particular buffer. Maybe I’m not working with it anymore and it’s clogging up the tabline.

Vim has a built-in command for this. But it identifies buffers by their “real,” internal number. :23 bdelete, or just :23 bd, unloads the buffer whose real number is 23.

It would be more convenient to identify the buffer to unload by its “fake” ordinal number.

Act on a buffer’s window by ordinal number

Some actions in Vim target a window rather than a buffer. For instance, if I want to move the cursor someplace, I need to give Vim a window number to move it to. Now I have three steps:

  1. Convert a fake buffer number to a real one.
  2. Given a real buffer number, identify windows where it is open.
  3. If step 2 succeeds, do something to those windows.