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' })