Vim Tip #30: Numbering Your Tabs

2020-06-14(Sun)

tags: Vim

Vim Tips

[I've significantly updated this code. Don't use this! See this instead.]

Vim likes to think of files as "buffers," but I prefer them as "tabs." To use Vim, it's really important to understand the distinction even if you prefer tabs, and if you don't ... stay away from this tip.

I use tabs a lot in Vim, and the fastest way to get around is the Normal mode command <n>gt, where <n> is a number, and gt means "go tab". gt on its own - without a number - simply moves to the next tab. But if you have several tabs open, typing it over and over becomes annoying and using tab numbers becomes desirable. But if you have four or more tabs open, looking at the Tabline and figuring out the number of the tab you're headed for takes a few seconds - it takes you out of the flow of your work. I figured there must be a way to add tab numbers to the Tabline. There absolutely is, but things turn ugly quite quickly. If you read :help setting-tabline, the documentation will set you on the path - even including example code. Unfortunately the example code is quite rudimentary. It even says "You will want to reduce the width of labels in a clever way when there is not enough room." So ... you gave us example code, but it's actually less functional than the default Vim Tabline? Yes, yes it is. Searching online shows that I'm far from the only person to have had this idea. Unfortunately, I tested a couple of the proffered code blocks and found that most of them weren't, as the documentation put it, "clever." If you open a lot of tabs, the Tabline becomes too long to see all of it ... and if you can't see all of it, it's not very useful is it?

I decided to try to fix this myself. Is what I've created good? Hell no - but I'm pleased with it because I set out a couple days ago with no damn idea if I could do this at all, and it does what I wanted. And I hope to improve it further.

What it does:

  • shows the filename devoid of path in each tab
  • shows a pipe (as a separator) and a tab number at the right of each tab
  • if the tab contains more than one window, shows a window count and a pipe as a separator on the left of the tab
  • if the contents of any window in the tab are modified, shows a "+" at the far left of the tab
  • if the focussed window in a tab is a help file, removes the ".txt" extension and prefaces the name with "h:"
  • uses a simplistic shortening scheme to reduce the length of filenames when there's a lot of tabs, truncating from the middle and adding an unusual character (I chose "◊", but there are many options shown below and it should be easy for you to change in the code) to indicate truncation. This truncation code is NOT well tested, and probably gets ugly when tab widths drop below 10 characters.
  • overrides the :colorscheme colours and enforces the Vim "default" tab colours regardless of other settings. This is okay for me because I use airline - which also has fixed colours, and I like the default Tabline colours and really don't like many of the Tabline colours offered by various colorschemes. Others may well object to this behaviour.

Here's the current code. To use this, copy this code into ~/.vim/plugin/Tabline.vim and it will be automatically loaded the next time you start Vim. It's just as easily removed: either delete it or change the extension to anything not .vim. Or you can comment out the set tabline=%!MyTabLine() line and turn it on and off at will as noted in the code.

" code initially borrowed from :help setting-tabline.
"
" Turn off (revert to Vim's standard behaviour):
"   :set tabline=
" Turn on:
"   :set tabline=%!MyTabLine()

scriptencoding utf-8

" comment this out if you prefer the default to be Vim's standard tabs:
set tabline=%!MyTabLine()

function SetTabColours()
    " These are permanent overrides for Tabline colours.  All colours are
    " cleared on colorscheme change, so this sets the colours every time
    " the Tabline is set.
    hi GOTabLine     cterm=underline ctermfg=15 ctermbg=242 gui=underline guibg=DarkGrey
    hi GOTabLineSel  cterm=bold      ctermfg=15 ctermbg=0   gui=bold      guibg=DarkGrey
    hi GOTabLineFill cterm=reverse   ctermfg=15 ctermbg=0   gui=bold      guibg=Black
endfunction

function MyTabLabel(n, twidth)
    " a list of all the buffer numbers in the current tab:
    let buflist = tabpagebuflist(a:n)
    let winnr = tabpagewinnr(a:n)
    let l:displaywincount = ''
    let l:modifiedCount = 0
    let l:showModified = ''
    let l:filename = bufname(buflist[winnr - 1])
    " Get just the filename, no path:
    let l:shortfilename = matchstr(l:filename, '[^/]*$')
    let l:wincount = len(buflist)
    if l:wincount > 1
        let l:displaywincount = string(l:wincount) . '|'
    endif
    for b in buflist
        if getbufvar( b, '&modified' )
            let l:modifiedCount += 1
        endif
    endfor
    if l:modifiedCount > 0
        let l:showModified = '+ '
    endif
    " modify the display title if it's a help buffer
    if getbufvar( buflist[winnr - 1], '&buftype' ) ==# 'help'
        let l:shortfilename = 'h:' . substitute(l:shortfilename, '.txt$', '', '')
    endif

    " Final trim
    if strlen(l:shortfilename) > a:twidth
        let l:tailLength = 4
        " l:shortener is the inserted special character when a tab title is
        " shortened: preferably something quickly visually identifiable as NOT
        " in your standard character set and thus extremely unlikely to
        " actually be part of a filename.  Possible choices:
        "   ∥ ‖ ∷ ∓ ≈ ⌒ ∙ ⊙ ⊚ ● ⊥ ┅ ⑀ □ ▢ ▣ ◆ ◇ ◊
        let l:shortener = "◊"
        " the filename is too long, crop it - l:shortfilename needs to get
        " shorter:
        let l:shortfilename =
            \ strcharpart(l:shortfilename, 0, a:twidth - l:tailLength)
            \ . l:shortener
            \ . strcharpart(l:shortfilename, strlen(l:shortfilename) - l:tailLength)
    endif

    return l:showModified . l:displaywincount . l:shortfilename . '|' . a:n
endfunction

function MyTabLine()
    call SetTabColours()  " force my choice of tab highlight colours, overriding colorscheme
    let tabTitleWidth = ( &columns / tabpagenr('$')) - 6

    let s = ''
    for i in range(tabpagenr('$'))
        " select the highlighting
        if i + 1 == tabpagenr()
            let s .= '%#GOTabLineSel#'
        else
            let s .= '%#GOTabLine#'
        endif
        " set the tab page number (for mouse clicks)
        let s .= '%' . (i + 1) . 'T'
        " the label is made by MyTabLabel()
        let s .= ' %{MyTabLabel(' . (i + 1) . ', ' . tabTitleWidth . ')} '
    endfor
    " after the last tab fill with TabLineFill and reset tab page nr
    let s .= '%#GOTabLineFill#%T'
    " right-align the label to close the current tab page
    if tabpagenr('$') > 1
        let s .= '%=%#GOTabLine#%999XX'
    endif
    return s
endfunction

What's wrong with it:

  • the truncation code is too simplistic
  • there may be issues with mouse support as I almost never use or test it
  • there are thousands of circumstances under which this has NOT been tested

What I'd like it to do:

  • I'd like better colourization, notably a single character to indicate a modified tab in white on red
  • better handling of in-tab terminals
  • if there's a lot of tab space, provide partial paths as well as filename
  • if there are more tabs than space and the Tabline is longer than the terminal width, centre the current tab ... the documentation's demonstration implementation forces the first tab (or several tabs) off screen to the left, and if you do 1gt you're jumping to a tab that's never displayed (the file(s) are, but not the associated tab). The Tabline should be shifted so that you see the tab you're on).