2026-01-25 09:38:17 +01:00

220 lines
8.7 KiB
VimL

" Vim global plugin for rendering the buffer list in the tabline
" Licence: The MIT License (MIT)
" Commit: $Format:%H$
" {{{ Copyright (c) 2015 Aristotle Pagaltzis <pagaltzis@gmx.de>
"
" Permission is hereby granted, free of charge, to any person obtaining a copy
" of this software and associated documentation files (the "Software"), to deal
" in the Software without restriction, including without limitation the rights
" to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
" copies of the Software, and to permit persons to whom the Software is
" furnished to do so, subject to the following conditions:
"
" The above copyright notice and this permission notice shall be included in
" all copies or substantial portions of the Software.
"
" THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
" IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
" FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
" AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
" LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
" OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
" THE SOFTWARE.
" }}}
if v:version < 700
echoerr printf('Vim 7 is required for buftabline (this is only %d.%d)',v:version/100,v:version%100)
finish
endif
scriptencoding utf-8
hi default link BufTabLineCurrent TabLineSel
hi default link BufTabLineActive PmenuSel
hi default link BufTabLineHidden TabLine
hi default link BufTabLineFill TabLineFill
hi default link BufTabLineModifiedCurrent BufTabLineCurrent
hi default link BufTabLineModifiedActive BufTabLineActive
hi default link BufTabLineModifiedHidden BufTabLineHidden
let g:buftabline_numbers = get(g:, 'buftabline_numbers', 0)
let g:buftabline_indicators = get(g:, 'buftabline_indicators', 0)
let g:buftabline_separators = get(g:, 'buftabline_separators', 0)
let g:buftabline_show = get(g:, 'buftabline_show', 2)
let g:buftabline_plug_max = get(g:, 'buftabline_plug_max', 10)
function! buftabline#user_buffers() " help buffers are always unlisted, but quickfix buffers are not
return filter(range(1,bufnr('$')),'buflisted(v:val) && "quickfix" !=? getbufvar(v:val, "&buftype")')
endfunction
function! s:switch_buffer(bufnum, clicks, button, mod)
execute 'buffer' a:bufnum
endfunction
function s:SID()
return matchstr(expand('<sfile>'), '<SNR>\d\+_')
endfunction
let s:dirsep = fnamemodify(getcwd(),':p')[-1:]
let s:centerbuf = winbufnr(0)
let s:tablineat = has('tablineat')
let s:sid = s:SID() | delfunction s:SID
function! buftabline#render()
let show_num = g:buftabline_numbers == 1
let show_ord = g:buftabline_numbers == 2
let show_mod = g:buftabline_indicators
let lpad = g:buftabline_separators ? nr2char(0x23B8) : ' '
let bufnums = buftabline#user_buffers()
let centerbuf = s:centerbuf " prevent tabline jumping around when non-user buffer current (e.g. help)
" pick up data on all the buffers
let tabs = []
let path_tabs = []
let tabs_per_tail = {}
let currentbuf = winbufnr(0)
let screen_num = 0
for bufnum in bufnums
let screen_num = show_num ? bufnum : show_ord ? screen_num + 1 : ''
let tab = { 'num': bufnum, 'pre': '' }
let tab.hilite = currentbuf == bufnum ? 'Current' : bufwinnr(bufnum) > 0 ? 'Active' : 'Hidden'
if currentbuf == bufnum | let [centerbuf, s:centerbuf] = [bufnum, bufnum] | endif
let bufpath = bufname(bufnum)
if strlen(bufpath)
let tab.path = fnamemodify(bufpath, ':p:~:.')
let tab.sep = strridx(tab.path, s:dirsep, strlen(tab.path) - 2) " keep trailing dirsep
let tab.label = tab.path[tab.sep + 1:]
let pre = screen_num
if getbufvar(bufnum, '&mod')
let tab.hilite = 'Modified' . tab.hilite
if show_mod | let pre = '+' . pre | endif
endif
if strlen(pre) | let tab.pre = pre . ' ' | endif
let tabs_per_tail[tab.label] = get(tabs_per_tail, tab.label, 0) + 1
let path_tabs += [tab]
elseif -1 < index(['nofile','acwrite'], getbufvar(bufnum, '&buftype')) " scratch buffer
let tab.label = ( show_mod ? '!' . screen_num : screen_num ? screen_num . ' !' : '!' )
else " unnamed file
let tab.label = ( show_mod && getbufvar(bufnum, '&mod') ? '+' : '' )
\ . ( screen_num ? screen_num : '*' )
endif
let tabs += [tab]
endfor
" disambiguate same-basename files by adding trailing path segments
while len(filter(tabs_per_tail, 'v:val > 1'))
let [ambiguous, tabs_per_tail] = [tabs_per_tail, {}]
for tab in path_tabs
if -1 < tab.sep && has_key(ambiguous, tab.label)
let tab.sep = strridx(tab.path, s:dirsep, tab.sep - 1)
let tab.label = tab.path[tab.sep + 1:]
endif
let tabs_per_tail[tab.label] = get(tabs_per_tail, tab.label, 0) + 1
endfor
endwhile
" now keep the current buffer center-screen as much as possible:
" 1. setup
let lft = { 'lasttab': 0, 'cut': '.', 'indicator': '<', 'width': 0, 'half': &columns / 2 }
let rgt = { 'lasttab': -1, 'cut': '.$', 'indicator': '>', 'width': 0, 'half': &columns - lft.half }
" 2. sum the string lengths for the left and right halves
let currentside = lft
let lpad_width = strwidth(lpad)
for tab in tabs
let tab.width = lpad_width + strwidth(tab.pre) + strwidth(tab.label) + 1
let tab.label = lpad . tab.pre . substitute(strtrans(tab.label), '%', '%%', 'g') . ' '
if centerbuf == tab.num
let halfwidth = tab.width / 2
let lft.width += halfwidth
let rgt.width += tab.width - halfwidth
let currentside = rgt
continue
endif
let currentside.width += tab.width
endfor
if currentside is lft " centered buffer not seen?
" then blame any overflow on the right side, to protect the left
let [lft.width, rgt.width] = [0, lft.width]
endif
" 3. toss away tabs and pieces until all fits:
if ( lft.width + rgt.width ) > &columns
let oversized
\ = lft.width < lft.half ? [ [ rgt, &columns - lft.width ] ]
\ : rgt.width < rgt.half ? [ [ lft, &columns - rgt.width ] ]
\ : [ [ lft, lft.half ], [ rgt, rgt.half ] ]
for [side, budget] in oversized
let delta = side.width - budget
" toss entire tabs to close the distance
while delta >= tabs[side.lasttab].width
let delta -= remove(tabs, side.lasttab).width
endwhile
" then snip at the last one to make it fit
let endtab = tabs[side.lasttab]
while delta > ( endtab.width - strwidth(strtrans(endtab.label)) )
let endtab.label = substitute(endtab.label, side.cut, '', '')
endwhile
let endtab.label = substitute(endtab.label, side.cut, side.indicator, '')
endfor
endif
if len(tabs) | let tabs[0].label = substitute(tabs[0].label, lpad, ' ', '') | endif
let swallowclicks = '%'.(1 + tabpagenr('$')).'X'
return s:tablineat
\ ? join(map(tabs,'"%#BufTabLine".v:val.hilite."#" . "%".v:val.num."@'.s:sid.'switch_buffer@" . strtrans(v:val.label)'),'') . '%#BufTabLineFill#' . swallowclicks
\ : swallowclicks . join(map(tabs,'"%#BufTabLine".v:val.hilite."#" . strtrans(v:val.label)'),'') . '%#BufTabLineFill#'
endfunction
function! buftabline#update(zombie)
set tabline=
" silent! is used here because neovim lacks guioptions and v0.11.0 broke backcompat
" by making setting it an E519 unsupported error instead of the previous no-op
" N.B. :set processes options in order and will abort on error even under silent!
if tabpagenr('$') > 1 | silent! set showtabline=2 guioptions+=e | return | endif
silent! set guioptions-=e
if 0 == g:buftabline_show
set showtabline=1
return
elseif 1 == g:buftabline_show
" account for BufDelete triggering before buffer is actually deleted
let bufnums = filter(buftabline#user_buffers(), 'v:val != a:zombie')
let &g:showtabline = 1 + ( len(bufnums) > 1 )
elseif 2 == g:buftabline_show
set showtabline=2
endif
set tabline=%!buftabline#render()
endfunction
augroup BufTabLine
autocmd!
autocmd VimEnter * call buftabline#update(0)
autocmd TabEnter * call buftabline#update(0)
autocmd BufAdd * call buftabline#update(0)
autocmd FileType qf call buftabline#update(0)
autocmd BufDelete * call buftabline#update(str2nr(expand('<abuf>')))
augroup END
for s:n in range(1, g:buftabline_plug_max) + ( g:buftabline_plug_max > 0 ? [-1] : [] )
let s:b = s:n == -1 ? -1 : s:n - 1
execute printf("noremap <silent> <Plug>BufTabLine.Go(%d) :<C-U>exe 'b'.get(buftabline#user_buffers(),%d,'')<cr>", s:n, s:b)
endfor
unlet! s:n s:b
if v:version < 703
function s:transpile()
let [ savelist, &list ] = [ &list, 0 ]
redir => src
silent function buftabline#render
redir END
let &list = savelist
let src = substitute(src, '\n\zs[0-9 ]*', '', 'g')
let src = substitute(src, 'strwidth(strtrans(\([^)]\+\)))', 'strlen(substitute(\1, ''\p\|\(.\)'', ''x\1'', ''g''))', 'g')
return src
endfunction
exe "delfunction buftabline#render\n" . s:transpile()
delfunction s:transpile
endif