neovim

Personal neovim configuration files
git clone git://gtms.dev/neovim
Log | Files | Refs

bufremove.lua (6532B)


      1 -- MIT License Copyright (c) 2021 Evgeni Chasnovski
      2 ---@brief [[
      3 --- Lua module for minimal buffer removing (unshow, delete, wipeout), which
      4 --- saves window layout (opposite to builtin Neovim's commands). This is mostly
      5 --- a Lua implementation of
      6 --- [bclose.vim](https://vim.fandom.com/wiki/Deleting_a_buffer_without_closing_the_window).
      7 --- Other alternatives:
      8 --- - [vim-bbye](https://github.com/moll/vim-bbye)
      9 --- - [vim-sayonara](https://github.com/mhinz/vim-sayonara)
     10 ---
     11 --- # Notes
     12 --- 1. Which buffer to show in window(s) after its current buffer is removed is
     13 ---    decided by the algorithm:
     14 ---    - If alternate buffer (see |CTRL-^|) is listed (see |buflisted()|), use it.
     15 ---    - If previous listed buffer (see |bprevious|) is different, use it.
     16 ---    - Otherwise create a scratch one with `nvim_create_buf(true, true)` and use
     17 ---      it.
     18 ---
     19 ---@brief ]]
     20 ---@tag Bufremove bufremove
     21 -- Module and its helper
     22 local BufRemove = {}
     23 local H = {}
     24 
     25 -- Module functionality
     26 --- Delete buffer `buf_id` with |:bdelete| after unshowing it.
     27 ---
     28 ---@param buf_id number: Buffer identifier (see |bufnr()|) to use. Default: 0 for current.
     29 ---@param force boolean: Whether to ignore unsaved changes (using `!` version of command). Default: `false`.
     30 ---@return boolean: Whether operation was successful.
     31 function BufRemove.delete(buf_id, force)
     32   return H.unshow_and_cmd(buf_id, force, 'bdelete')
     33 end
     34 
     35 --- Wipeout buffer `buf_id` with |:bwipeout| after unshowing it.
     36 ---
     37 ---@param buf_id number: Buffer identifier (see |bufnr()|) to use. Default: 0 for current.
     38 ---@param force boolean: Whether to ignore unsaved changes (using `!` version of command). Default: `false`.
     39 ---@return boolean: Whether operation was successful.
     40 function BufRemove.wipeout(buf_id, force)
     41   return H.unshow_and_cmd(buf_id, force, 'bwipeout')
     42 end
     43 
     44 --- Stop showing buffer `buf_id` in all windows
     45 ---
     46 ---@param buf_id number: Buffer identifier (see |bufnr()|) to use. Default: 0 for current.
     47 ---@return boolean: Whether operation was successful.
     48 function BufRemove.unshow(buf_id)
     49   buf_id = H.normalize_buf_id(buf_id)
     50 
     51   if not H.is_valid_id(buf_id, 'buffer') then
     52     return false
     53   end
     54 
     55   vim.tbl_map(BufRemove.unshow_in_window, vim.fn.win_findbuf(buf_id))
     56 
     57   return true
     58 end
     59 
     60 --- Stop showing current buffer of window `win_id`
     61 ---@param win_id number: Window identifier (see |win_getid()|) to use. Default: 0 for current.
     62 ---@return boolean: Whether operation was successful.
     63 function BufRemove.unshow_in_window(win_id)
     64   win_id = (win_id == nil) and 0 or win_id
     65 
     66   if not H.is_valid_id(win_id, 'window') then
     67     return false
     68   end
     69 
     70   local cur_buf = vim.api.nvim_win_get_buf(win_id)
     71 
     72   -- Temporary use window `win_id` as current to have Vim's functions working
     73   vim.api.nvim_win_call(win_id, function()
     74     -- Try using alternate buffer
     75     local alt_buf = vim.fn.bufnr('#')
     76     if alt_buf ~= cur_buf and vim.fn.buflisted(alt_buf) == 1 then
     77       vim.api.nvim_win_set_buf(win_id, alt_buf)
     78       return
     79     end
     80 
     81     -- Try using previous buffer
     82     vim.cmd.bprevious()
     83     if cur_buf ~= vim.api.nvim_win_get_buf(win_id) then
     84       return
     85     end
     86 
     87     -- Create new listed scratch buffer
     88     local new_buf = vim.api.nvim_create_buf(true, true)
     89     vim.api.nvim_win_set_buf(win_id, new_buf)
     90   end)
     91 
     92   return true
     93 end
     94 
     95 -- Removing implementation
     96 function H.unshow_and_cmd(buf_id, force, cmd)
     97   buf_id = H.normalize_buf_id(buf_id)
     98   force = (force == nil) and false or force
     99 
    100   if not H.is_valid_id(buf_id, 'buffer') then
    101     return false
    102   end
    103 
    104   local fun_name = ({ ['bdelete'] = 'delete', ['bwipeout'] = 'wipeout' })[cmd]
    105   if not H.can_remove(buf_id, force, fun_name) then
    106     return false
    107   end
    108 
    109   -- Unshow buffer from all windows
    110   BufRemove.unshow(buf_id)
    111 
    112   -- Execute command
    113   local command = string.format('%s%s %d', cmd, force and '!' or '', buf_id)
    114   ---- Use `pcall` here to take care of case where `unshow()` was enough.
    115   ---- This can happen with 'bufhidden' option values:
    116   ---- - If `delete` then `unshow()` already `bdelete`d buffer. Without `pcall`
    117   ----   it gives E516 for `Bufremove.delete()` (`wipeout` works).
    118   ---- - If `wipe` then `unshow()` already `bwipeout`ed buffer. Without `pcall`
    119   ----   it gives E517 for module's `wipeout()` (still E516 for `delete()`).
    120   local ok, result = pcall(vim.cmd, command)
    121   if not (ok or result:find('E516') or result:find('E517')) then
    122     vim.notify('(bufremove) ' .. result)
    123     return false
    124   end
    125 
    126   return true
    127 end
    128 
    129 -- Utilities
    130 function H.is_valid_id(x, type)
    131   local is_valid = false
    132   if type == 'buffer' then
    133     is_valid = vim.api.nvim_buf_is_valid(x)
    134   elseif type == 'window' then
    135     is_valid = vim.api.nvim_win_is_valid(x)
    136   end
    137 
    138   if not is_valid then
    139     H.notify(string.format('%s is not a valid %s id.', tostring(x), type))
    140   end
    141   return is_valid
    142 end
    143 
    144 ---- Check if buffer can be removed with `Bufremove.fun_name` function
    145 function H.can_remove(buf_id, force, fun_name)
    146   if force then
    147     return true
    148   end
    149 
    150   if vim.api.nvim_get_option_value('modified', { buf = buf_id }) then
    151     H.notify(string.format('Buffer %d has unsaved changes. Use `Bufremove.%s(%d, true)` to force.', buf_id, fun_name,
    152       buf_id))
    153     return false
    154   end
    155   return true
    156 end
    157 
    158 ---- Compute 'true' buffer id (strictly positive integer). Treat `nil` and 0 as
    159 ---- current buffer.
    160 function H.normalize_buf_id(buf_id)
    161   if buf_id == nil or buf_id == 0 or buf_id == '' then
    162     return vim.api.nvim_get_current_buf()
    163   end
    164   return buf_id
    165 end
    166 
    167 function H.notify(msg)
    168   vim.notify(string.format('(bufremove) %s', msg))
    169 end
    170 
    171 vim.api.nvim_create_user_command('BufDelete', function(data)
    172   BufRemove.delete(data.args, data.bang)
    173 end, { nargs = '?', bang = true })
    174 vim.api.nvim_create_user_command('BufWipeout', function(data)
    175   BufRemove.wipeout(data.args, data.bang)
    176 end, { nargs = '?', bang = true })
    177 vim.api.nvim_create_user_command('BufUnshow', function(data)
    178   BufRemove.unshow(data.args)
    179 end, { nargs = '?' })
    180 vim.api.nvim_create_user_command('BufWinUnshow', function(data)
    181   BufRemove.unshow_in_window(data.args)
    182 end, { nargs = '?' })
    183 
    184 vim.keymap.set('n', '<leader>bd', BufRemove.delete, { desc = 'Buffer Delete' })
    185 vim.keymap.set('n', '<leader>bD', function()
    186   BufRemove.delete(0, true)
    187 end, { desc = 'Buffer Force Delete' })
    188 vim.keymap.set('n', '<leader>bw', BufRemove.wipeout, { desc = 'Buffer Wipeout' })
    189 vim.keymap.set('n', '<leader>bu', BufRemove.unshow, { desc = 'Buffer Unshow' })
    190 vim.keymap.set('n', '<leader>bU', BufRemove.unshow_in_window, { desc = 'Buffer Window Unshow' })