Playing around with the new vertical "tabpanel" in Vim: Simple tabpanel & "Bufferpanel"
In the patch 9.1.1391, the Vim added a new UI element: tabpanel. This would have garnered a lot of attention if it was in Neovim, but unfortunately, vanilla Vimmers did not seem to be too interested in the UI change. So I took the chance to become the first person (at least in r/vim) to configure a custom Tabpanel.
Simple Tabpanel
I started with a simple list of tabs with the number of windows.
I found :h 'tabline'
to be quite helpful.
This is essentially the vertical, Vimscript version of my Neovim tabline.
Clicking and dragging on the tabs work fine, but using the %X
or %T
flag did not work.
1" {{{ Simple Tabpanel
2set showtabpanel=2
3set fillchars+=tpl_vert:\|
4set tabpanelopt=vert,align:left,columns:9
5function! TabPanel() abort
6 let curr = g:actual_curtabpage
7
8 let s = printf("%2d", curr)
9 let numWin = len(tabpagebuflist(curr))
10 if numWin > 1
11 let s .= '| ' . numWin
12 endif
13
14 return s
15endfunction
16set tabpanel=%!TabPanel()
17" }}}
You can find the Bufferline in this post (the version in the screenshot is slightly modified).
“Bufferpanel”
But I think it makes much more sense to use the vertical space as the list of buffers, creating the “Bufferpanel.”
My first instinct was to simply replace separators in my Bufferline config with \n
, but it was not as easy as that.
The content of 'tabline'
is evaluated per tab, meaning if your function returns the list of buffers, it will be printed twice if you have two tabs open.
So I created a workaround where I only display the content for the first tab and nothing for others if they exist.
1" {{{ BufferPanel
2set showtabpanel=2
3set fillchars+=tpl_vert:\|
4set tabpanelopt=vert,align:left,columns:20
5function! BufferPanel() abort
6 let s = '%#TabPanelFill#'
7
8 " tabpanel is evaluated per tab; workaround to create the list only once
9 if g:actual_curtabpage == 1
10
11 " Get the list of buffers. Use bufexists() to include hidden buffers
12 let bufferNums = filter(range(1, bufnr('$')), 'buflisted(v:val)')
13
14 for i in bufferNums
15 " Highlight if it's the current buffer
16 let s .= (i == bufnr()) ? ('%#TabPanelSel#') : ('%#TabPanel#')
17
18 let s .= ' ' . i . ' ' " Append the buffer number
19
20 " Give a [NEW] flag to an unnamed buffer
21 if bufname(i) == ''
22 let s .= '[NEW]'
23 endif
24
25 " Append bufname
26 let bufname = fnamemodify(bufname(i), ':t')
27
28 " Truncate bufname
29 " -1 if vertical separators are on
30 " -3 for the buffer number
31 " -3 for the potential modified flag
32 " -2 for the ..
33 let lenLimit = 11
34 if len(bufname) > lenLimit
35 " expr-[:] is range-inclusive (i.e., [0:10] returns 11 char)
36 let bufname = bufname[0:lenLimit - 1] . '..'
37 endif
38
39 let s .= bufname
40
41 " Add modified & read only flag
42 if getbufvar(i, "&modified")
43 let s .= '[+]'
44 endif
45 if !getbufvar(i, "&modifiable")
46 let s .= '[-]'
47 endif
48 if getbufvar(i, "&readonly")
49 let s .= '[RO]'
50 endif
51
52 let s .= "\n"
53 endfor
54
55 let s .= "%#TabPanelFill#"
56 endif
57
58 return s
59endfunction
60
61set tabpanel=%!BufferPanel()
62" }}}
That looks quite slick, doesn’t it?
You probably want to add the following autocmd
to force redraw on buffer changes.
1" Force redraw on buffer changes
2autocmd BufAdd,BufCreate,BufDelete,BufWipeout * redrawtabpanel
Note that mouse clicking is completely broken with this.
I am not even sure if there is a way to fix it given that components like %T
and %X
do not seem to work.
I am also a bit disappointed that you cannot interact with the panel using the keyboard. I would love to be able to use it as a Netrw but for buffers. Nonetheless, we can at least make it toggle-able with the following keymap.
1" Toggle bufferpanel
2nnoremap <expr><silent> <leader>b &showtabpanel==2 ?
3 \ ':set showtabpanel=0<CR>' : ':set showtabpanel=2<CR>'
I also had to create the horizontal version of the simple Tabpanel to go with the Bufferpanel.
1" {{{ Simple Tabline
2fun! SpawnTabline()
3 let s = ' Tabs :) '
4
5 for i in range(1, tabpagenr('$')) " Loop through the number of tabs
6 " Highlight the current tab
7 let s .= (i == tabpagenr()) ? ('%#TabLineSel#') : ('%#TabLine#')
8 let s .= '%' . i . 'T ' " set the tab page number (for mouse clicks)
9 let s .= i " set page number string
10
11 " Add a number of window if applicable
12 let numWin = len(tabpagebuflist(i))
13 if numWin > 1
14 let s .= '[ ' . numWin . ']'
15 endif
16
17 let s .= ' '
18 endfor
19 let s .= '%#TabLineFill#%T' " Reset highlight
20
21 " Close button on the right if there are multiple tabs
22 if tabpagenr('$') > 1
23 let s .= '%=%#TabLineSel#%999X[X]'
24 endif
25 return s
26endfun
27
28set tabline=%!SpawnTabline() " Assign the tabline
29" }}}
Bug in 9.1.1400
Unfortunately, there is a bug in 9.1.1400 that for me makes tabpanel
unusable.
Essentially, when tabpanel
is on and set to occupy, say 20 columns, then the character in the 20th column from the opposite side gets rendered incorrectly.
I created the issue on GitHub (also include the video demonstration), fingers crossed it gets resolved soon.
Conclusion
Can’t wait for Neovim to implement this so that we can have an influx of crazy Lua plugins abusing this feature.