Theo's ʕ•ᴥ•ʔ Park

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" }}}

simple-tabpanel

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" }}}

bufferpanel

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.

#vim #vimscript