/bufferline.nvim

A snazzy bufferline for Neovim

Primary LanguageLuaThe UnlicenseUnlicense

bufferline.nvim

A snazzy πŸ’… buffer line (with minimal tab integration) for Neovim built using lua.

Demo GIF

This plugin shamelessly attempts to emulate the aesthetics of GUI text editors/Doom Emacs. It was inspired by a screenshot of DOOM Emacs using centaur tabs.

Table of Contents

Features

  • Colours derived from colorscheme where possible.

  • Sort buffers by extension, directory or pass in a custom compare function

  • Configuration via lua functions for greater customization.

Alternate styling

slanted tabs

NOTE: some terminals require special characters to be padded so set the style to padded_slant if the appearance isn't right in your terminal emulator. Please keep in mind though that results may vary depending on your terminal emulator of choice and this style might will not work for all terminals

see: :h bufferline-styling

LSP error indicators

LSP error

NOTE: This only works with neovim's native lsp.

Sidebar offset

explorer header

Buffer numbers

bufferline with numbers

Ordinal number and buffer number with a customized number styles.

numbers

Buffer pick

bufferline pick

Unique buffer names

duplicate names

Close icons

close button

Buffer re-ordering

re-order buffers

This order can be persisted between sessions (enabled by default).

Requirements

Installation

Lua

-- using packer.nvim
use {'akinsho/bufferline.nvim', requires = 'kyazdani42/nvim-web-devicons'}

Vimscript

Plug 'kyazdani42/nvim-web-devicons' " Recommended (for coloured icons)
" Plug 'ryanoasis/vim-devicons' Icons without colours
Plug 'akinsho/bufferline.nvim'

What about Tabs?

This plugin, as the name implies, shows a user their buffers not tabs if you're unclear as to what the difference is please read :help tabpage. It does include minimal indicators which show how many tabs you have open and which is focused. These are not however part of the bufferline proper and tabs cannot currently replace buffers.

If you are interested in contributing a PR for tab related functionality please raise an issue to discuss.

N.B: please don't open a feature request for this. It isn't something I plan on personally implementing but will happily help a willing contributor who wants to add this themselves.

Caveats

  • This won't appeal to everyone's tastes. This plugin is opinionated about how the tabline looks, it's unlikely to please everyone.

  • I want to prevent this becoming a pain to maintain so I'll be conservative about what I add.

  • This plugin relies on some basic highlights being set by your colour scheme i.e. Normal, String, TabLineSel (WildMenu as fallback), Comment. It's unlikely to work with all colour schemes. You can either try manually overriding the colours or manually creating these highlight groups before loading this plugin.

  • If the contrast in your colour scheme isn't very high, think an all black colour scheme, some of the highlights of this plugin won't really work as intended since it depends on darkening things.

Usage

See the docs for details :h bufferline.nvim

You need to be using termguicolors for this plugin to work, as it reads the hex gui color values of various highlight groups.

Vimscript

" In your init.lua or init.vim
set termguicolors
lua << EOF
require("bufferline").setup{}
EOF

Lua

vim.opt.termguicolors = true
require("bufferline").setup{}

You can close buffers by clicking the close icon or by right clicking the tab anywhere

A few of this plugins commands can be mapped for ease of use.

" These commands will navigate through buffers in order regardless of which mode you are using
" e.g. if you change the order of buffers :bnext and :bprevious will not respect the custom ordering
nnoremap <silent>[b :BufferLineCycleNext<CR>
nnoremap <silent>b] :BufferLineCyclePrev<CR>

" These commands will move the current buffer backwards or forwards in the bufferline
nnoremap <silent><mymap> :BufferLineMoveNext<CR>
nnoremap <silent><mymap> :BufferLineMovePrev<CR>

" These commands will sort buffers by directory, language, or a custom criteria
nnoremap <silent>be :BufferLineSortByExtension<CR>
nnoremap <silent>bd :BufferLineSortByDirectory<CR>
nnoremap <silent><mymap> :lua require'bufferline'.sort_buffers_by(function (buf_a, buf_b) return buf_a.id < buf_b.id end)<CR>

If you manually arrange your buffers using :BufferLineMove{Prev|Next} during an nvim session this can be persisted for the session. This is enabled by default but you need to ensure that your sessionoptions+=globals otherwise the session file will not track global variables which is the mechanism used to store your sort order.

Configuration

require('bufferline').setup {
  options = {
    numbers = "none" | "ordinal" | "buffer_id" | "both" | function({ ordinal, id, lower, raise }): string,
    --- @deprecated, please specify numbers as a function to customize the styling
    number_style = "superscript" | "subscript" | "" | { "none", "subscript" }, -- buffer_id at index 1, ordinal at index 2
    close_command = "bdelete! %d",       -- can be a string | function, see "Mouse actions"
    right_mouse_command = "bdelete! %d", -- can be a string | function, see "Mouse actions"
    left_mouse_command = "buffer %d",    -- can be a string | function, see "Mouse actions"
    middle_mouse_command = nil,          -- can be a string | function, see "Mouse actions"
    -- NOTE: this plugin is designed with this icon in mind,
    -- and so changing this is NOT recommended, this is intended
    -- as an escape hatch for people who cannot bear it for whatever reason
    indicator_icon = 'β–Ž',
    buffer_close_icon = 'ο™•',
    modified_icon = '●',
    close_icon = '',
    left_trunc_marker = '',
    right_trunc_marker = 'ο‚©',
    --- name_formatter can be used to change the buffer's label in the bufferline.
    --- Please note some names can/will break the
    --- bufferline so use this at your discretion knowing that it has
    --- some limitations that will *NOT* be fixed.
    name_formatter = function(buf)  -- buf contains a "name", "path" and "bufnr"
      -- remove extension from markdown files for example
      if buf.name:match('%.md') then
        return vim.fn.fnamemodify(buf.name, ':t:r')
      end
    end,
    max_name_length = 18,
    max_prefix_length = 15, -- prefix used when a buffer is de-duplicated
    tab_size = 18,
    diagnostics = false | "nvim_lsp",
    diagnostics_update_in_insert = false,
    diagnostics_indicator = function(count, level, diagnostics_dict, context)
      return "("..count..")"
    end,
    -- NOTE: this will be called a lot so don't do any heavy processing here
    custom_filter = function(buf_number)
      -- filter out filetypes you don't want to see
      if vim.bo[buf_number].filetype ~= "<i-dont-want-to-see-this>" then
        return true
      end
      -- filter out by buffer name
      if vim.fn.bufname(buf_number) ~= "<buffer-name-I-dont-want>" then
        return true
      end
      -- filter out based on arbitrary rules
      -- e.g. filter out vim wiki buffer from tabline in your work repo
      if vim.fn.getcwd() == "<work-repo>" and vim.bo[buf_number].filetype ~= "wiki" then
        return true
      end
    end,
    offsets = {{filetype = "NvimTree", text = "File Explorer" | function , text_align = "left" | "center" | "right"}},
    show_buffer_icons = true | false, -- disable filetype icons for buffers
    show_buffer_close_icons = true | false,
    show_close_icon = true | false,
    show_tab_indicators = true | false,
    persist_buffer_sort = true, -- whether or not custom sorted buffers should persist
    -- can also be a table containing 2 custom separators
    -- [focused and unfocused]. eg: { '|', '|' }
    separator_style = "slant" | "thick" | "thin" | { 'any', 'any' },
    enforce_regular_tabs = false | true,
    always_show_bufferline = true | false,
    sort_by = 'id' | 'extension' | 'relative_directory' | 'directory' | 'tabs' | function(buffer_a, buffer_b)
      -- add custom logic
      return buffer_a.modified > buffer_b.modified
    end
  }
}

Feature overview

LSP indicators

By setting diagnostics = "nvim_lsp" you will get an indicator in the bufferline for a given tab if it has any errors This will allow you to tell at a glance if a particular buffer has errors. Currently only the native neovim lsp is supported, mainly because it has the easiest API for fetching all errors for all buffers (with an attached lsp client).

In order to customise the appearance of the diagnostic count you can pass a custom function in your setup.

custom indicator

Snippet
-- rest of config ...

--- count is an integer representing total count of errors
--- level is a string "error" | "warning"
--- diagnostics_dict is a dictionary from error level ("error", "warning" or "info")to number of errors for each level.
--- this should return a string
--- Don't get too fancy as this function will be executed a lot
diagnostics_indicator = function(count, level, diagnostics_dict, context)
  local icon = level:match("error") and " " or " "
  return " " .. icon .. count
end

diagnostics_indicator

Snippet
diagnostics_indicator = function(count, level, diagnostics_dict, context)
  local s = " "
  for e, n in pairs(diagnostics_dict) do
    local sym = e == "error" and " "
      or (e == "warning" and " " or "ο„©" )
    s = s .. n .. sym
  end
  return s
end

The highlighting for the file name if there is an error can be changed by replacing the highlights for see :h bufferline-lua-highlights.

Conditional buffer based LSP indicators

LSP indicators can additionally be reported conditionally, based on buffer context. For instance, you could disable reporting LSP indicators for the current buffer and only have them appear for other buffers.

diagnostics_indicator = function(count, level, diagnostics_dict, context)
  if context.buffer:current() then
    return ''
  end

  return ''
end

current visible

The first bufferline shows diagnostic.lua as the currently opened current buffer. It has LSP reported errors, but they don't show up in the bufferline. The second bufferline shows 500-nvim-bufferline.lua as the currently opened current buffer. Because the 'faulty' diagnostic.lua buffer has now transitioned from current to visible, the LSP indicator does show up.

Regular tab sizes

Generally this plugin enforces a minimum tab size so that the buffer line appears consistent. Where a tab is smaller than the tab size it is padded. If it is larger than the tab size it is allowed to grow up to the max name length specified (+ the other indicators). If you set enforce_regular_tabs = true tabs will be prevented from extending beyond the tab size and all tabs will be the same length

Numbers

numbers

You can prefix buffer names with either the ordinal or buffer id, using the numbers option. Currently this can be specified as either a string of buffer_id | ordinal or a function This function allows maximum flexibility in determining the appearance of this section. It is passed a table with the following keys:

  • raise - a helper function to convert the passed number to superscript e.g. raise(buffer_id).
  • lower - a helper function to convert the passed number to subscript e.g. lower(buffer_id).
  • ordinal - The buffer ordinal number.
  • buffer_id - The buffer ID.
  -- For ⁸·₂
  numbers = function(opts)
    return string.format('%sΒ·%s', opts.raise(opts.id), opts.lower(opts.ordinal))
  end,

  -- For β‚ˆ.β‚‚
  numbers = function(opts)
    return string.format('%s.%s', opts.lower(opts.id), opts.lower(opts.ordinal))
  end,

  -- For 2.)8.) - change he order of arguments to change the order in the string
  numbers = function(opts)
    return string.format('%s.)%s.)', opts.ordinal, opts.id)
  end,

  -- For 8|Β² -
  numbers = function(opts)
    return string.format('%s|%s.)', opts.id, opts.raise(opts.ordinal))
  end,

Sorting

Bufferline allows you to sort the visible buffers by extension, directory or tabs:

NOTE: If using a plugin such as vim-rooter and you want to sort by path, prefer using directory rather than relative_directory. Relative directory works by ordering relative paths first, however if you move from project to project and vim switches its directory, the bufferline will re-order itself as a different set of buffers will now be relative.

" Using vim commands
:BufferLineSortByExtension
:BufferLineSortByDirectory
:BufferLineSortByTabs
-- Or using lua functions
:lua require'bufferline'.sort_buffers_by('extension')
:lua require'bufferline'.sort_buffers_by('directory')
:lua require'bufferline'.sort_buffers_by('tabs')

For more advanced usage you can provide a custom compare function which will receive two buffers to compare. You can see what fields are available to use using

sort_by = function(buffer_a, buffer_b)
  print(vim.inspect(buffer_a))
-- add custom logic
  local mod_a = vim.loop.fs_stat(buffer_a.path).mtime.sec
  local mod_b = vim.loop.fs_stat(buffer_b.path).mtime.sec
  return mod_a > mod_b
end

When using a sorted bufferline it's advisable that you use the BufferLineCycleNext and BufferLineCyclePrev commands since these will traverse the bufferline bufferlist in order whereas bnext and bprev will cycle buffers according to the buffer numbers given by vim.

Closing buffers

Bufferline provides a few commands to handle closing buffers visible in the tabline using BufferLineCloseRight and BufferLineCloseLeft. As their names suggest these commands will close all visible buffers to the left or right of the current buffer. Another way to close any single buffer is the BufferLinePickClose command (see below).

Sidebar offset

You can prevent the bufferline drawing above a vertical sidebar split such as a file explorer. To do this you must set the offsets configuration option to a list of tables containing the details of the window to avoid. NOTE: this is only relevant for left or right aligned sidebar windows such as NvimTree, NERDTree or Vista

offsets = {
  {
    filetype = "NvimTree",
    text = "File Explorer",
    highlight = "Directory",
    text_align = "left"
  }
}

The filetype is used to check whether a particular window is a match, the text is optional and will show above the window if specified. text can be either a string or a function which should also return a string. See the example below.

offsets = {
  {
    filetype = "NvimTree",
    text = function()
      return vim.fn.getcwd()
    end,
    highlight = "Directory",
    text_align = "left"
  }
}

If it is too long it will be truncated. The highlight controls what highlight is shown above the window. You can also change the alignment of the text in the offset section using text_align which can be set to left, right or center. You can also add a padding key which should be an integer if you want the offset to be larger than the window width.

Buffer pick functionality

Using the BufferLinePick command will allow for easy selection of a buffer in view. Trigger the command, using :BufferLinePick or better still map this to a key, e.g.

nnoremap <silent> gb :BufferLinePick<CR>

then pick a buffer by typing the character for that specific buffer that appears

bufferline_pick

Likewise, BufferLinePickClose closes the buffer instead of viewing it.

BufferLineGoToBuffer

You can select a buffer by it's visible position in the bufferline using the BufferLineGoToBuffer command. This means that if you have 60 buffers open but only 7 visible in the bufferline then using BufferLineGoToBuffer 4 will go to the 4th visible buffer not necessarily the 5 in the absolute list of open buffers.

<- (30) | buf31 | buf32 | buf33 | buf34 | buf35 | buf36 | buf37 (24) ->

Using BufferLineGoToBuffer 4 will open buf34 as it is the 4th visible buffer.

This can then be mapped using

nnoremap <silent><leader>1 <Cmd>BufferLineGoToBuffer 1<CR>
nnoremap <silent><leader>2 <Cmd>BufferLineGoToBuffer 2<CR>
nnoremap <silent><leader>3 <Cmd>BufferLineGoToBuffer 3<CR>
nnoremap <silent><leader>4 <Cmd>BufferLineGoToBuffer 4<CR>
nnoremap <silent><leader>5 <Cmd>BufferLineGoToBuffer 5<CR>
nnoremap <silent><leader>6 <Cmd>BufferLineGoToBuffer 6<CR>
nnoremap <silent><leader>7 <Cmd>BufferLineGoToBuffer 7<CR>
nnoremap <silent><leader>8 <Cmd>BufferLineGoToBuffer 8<CR>
nnoremap <silent><leader>9 <Cmd>BufferLineGoToBuffer 9<CR>

Mouse actions

You can configure different type of mouse clicks to behave differently. The current mouse click types are

  • Left - left_mouse_command
  • Right - right_mouse_command
  • Middle - middle_mouse_command
  • Close - close_command

Currently left mouse opens the selected buffer but the command can be tweaked using left_mouse_command which can be specified as either a lua function or string which uses lua's printf style string formatting e.g. buffer %d

You can do things like open a vertical split on right clicking the buffer name for example using

right_mouse_command = "vertical sbuffer %d"

Or you can set the value to a function and handle the click action however you please for example you can use another plugin such as bufdelete.nvim to handle closing the buffer using the close_command.

left_mouse_command = function(bufnum)
   require('bufdelete').bufdelete(bufnum, true)
end

Custom functions

A user can also execute arbitrary functions against a buffer using the buf_exec function. For example

    require('bufferline').buf_exec(
        4, -- the forth visible buffer from the left
        user_function -- an arbitrary user function which gets passed the buffer
    )

    -- e.g.
    function _G.bdel(num)
        require('bufferline').buf_exec(num, function(buf, visible_buffers)
            vim.cmd('bdelete '..buf.id)
        end
    end

    vim.cmd [[
        command -count Bdel <Cmd>lua _G.bdel(<count>)<CR>
    ]]

Custom area

custom area

You can also add custom content at the start or end of the bufferline using custom_areas this option allows a user to specify a function which should return the text and highlight for that text to be shown in a list of tables. For example:

custom_areas = {
  right = function()
    local result = {}
    local error = vim.lsp.diagnostic.get_count(0, [[Error]])
    local warning = vim.lsp.diagnostic.get_count(0, [[Warning]])
    local info = vim.lsp.diagnostic.get_count(0, [[Information]])
    local hint = vim.lsp.diagnostic.get_count(0, [[Hint]])

    if error ~= 0 then
      table.insert(result, {text = " ο™™ " .. error, guifg = "#EC5241"})
    end

    if warning ~= 0 then
      table.insert(result, {text = " ο”© " .. warning, guifg = "#EFB839"})
    end

    if hint ~= 0 then
      table.insert(result, {text = "  " .. hint, guifg = "#A3BA5E"})
    end

    if info ~= 0 then
      table.insert(result, {text = " ο„© " .. info, guifg = "#7EA9A7"})
    end
    return result
  end,
}

Please note that this function will be called a lot and should be as inexpensive as possible so it does not block rendering the tabline.

FAQ

  • Why isn't the bufferline appearing?

    The most common reason for this that has come up in various issues is it clashes with another plugin. Please make sure that you do not have another bufferline plugin installed.

    If you are using airline make sure you set let g:airline#extensions#tabline#enabled = 0. If you are using lightline this also takes over the tabline by default and needs to be deactivated.

  • Doesn't this plugin go against the "vim way"?

    This is much better explained by buftablines's author. Please read this for a more comprehensive answer to this questions. The short answer to this is buffers represent files in nvim and tabs, a collection of windows (or just one). Vim natively allows visualising tabs i.e. collections of window, but not just the files that are open. There are endless debates on this topic, but allowing a user to see what files they have open doesn't go against any clearly stated vim philosophy. It's a text editor and not a religion πŸ™. Obviously this won't appeal to everyone, which isn't really a feasible objective anyway.