|
1 | 1 | local api = vim.api |
| 2 | +local cmd = vim.cmd |
2 | 3 | local bo = vim.bo |
3 | 4 |
|
| 5 | +if vim.fn.has('nvim-0.8') == 0 then |
| 6 | + api.nvim_err_writeln('bufdelete.nvim is only available for Neovim versions 0.8 and above') |
| 7 | + return |
| 8 | +end |
| 9 | + |
4 | 10 | local M = {} |
5 | 11 |
|
6 | | --- Common kill function for bdelete and bwipeout |
7 | | -local function buf_kill(kill_command, bufnr, force) |
8 | | - -- If buffer is modified and force isn't true, print error and abort |
9 | | - if not force and bo[bufnr].modified then |
10 | | - api.nvim_echo({{ |
11 | | - string.format( |
12 | | - 'No write since last change for buffer %d. Would you like to:\n' .. |
13 | | - '(s)ave and close\n(i)gnore changes and close\n(c)ancel', |
14 | | - bufnr |
15 | | - ) |
16 | | - }}, false, {}) |
17 | | - |
18 | | - local choice = string.char(vim.fn.getchar()) |
19 | | - |
20 | | - if choice == 's' or choice == 'S' then |
21 | | - vim.cmd('write') |
22 | | - elseif choice == 'i' or choice == 'I' then |
23 | | - force = true; |
24 | | - else |
25 | | - return |
26 | | - end |
| 12 | +-- Common kill function for Bdelete and Bwipeout. |
| 13 | +local function buf_kill(range, force, wipeout) |
| 14 | + if range == nil then |
| 15 | + return |
27 | 16 | end |
28 | 17 |
|
29 | | - if bufnr == 0 or bufnr == nil then |
30 | | - bufnr = api.nvim_get_current_buf() |
| 18 | + if range[1] == 0 then |
| 19 | + range[1] = api.nvim_get_current_buf() |
| 20 | + range[2] = range[1] |
31 | 21 | end |
32 | 22 |
|
33 | | - if force then |
34 | | - kill_command = kill_command .. '!' |
| 23 | + -- Whether to forcibly close buffer. If true, use force. If false, simply ignore the buffer. |
| 24 | + local bufnr_force |
| 25 | + |
| 26 | + -- If force is disabled, check for modified buffers in range. |
| 27 | + if not force then |
| 28 | + -- List of modified buffers the user asked to close with force. |
| 29 | + bufnr_force = {} |
| 30 | + |
| 31 | + for bufnr=range[1], range[2] do |
| 32 | + -- If buffer is modified, prompt user for action. |
| 33 | + if api.nvim_buf_is_loaded(bufnr) and bo[bufnr].modified then |
| 34 | + api.nvim_echo({{ |
| 35 | + string.format( |
| 36 | + 'No write since last change for buffer %d (%s). Would you like to:\n' .. |
| 37 | + '(s)ave and close\n(i)gnore changes and close\n(c)ancel', |
| 38 | + bufnr, api.nvim_buf_get_name(bufnr) |
| 39 | + ) |
| 40 | + }}, false, {}) |
| 41 | + |
| 42 | + local choice = string.char(vim.fn.getchar()) |
| 43 | + |
| 44 | + if choice == 's' or choice == 'S' then -- Save changes to the buffer. |
| 45 | + api.nvim_buf_call(bufnr, function() cmd.write() end) |
| 46 | + elseif choice == 'i' or choice == 'I' then -- Ignore and forcibly close. |
| 47 | + bufnr_force[bufnr] = true |
| 48 | + end -- Otherwise, do nothing with this buffer. |
| 49 | + |
| 50 | + -- Clear message area. |
| 51 | + cmd.echo('""') |
| 52 | + cmd.redraw() |
| 53 | + end |
| 54 | + end |
35 | 55 | end |
36 | 56 |
|
37 | | - -- Get list of windows IDs with the buffer to close |
| 57 | + -- Get list of windows IDs with the buffers to close. |
38 | 58 | local windows = vim.tbl_filter( |
39 | | - function(win) return api.nvim_win_get_buf(win) == bufnr end, |
| 59 | + function(win) |
| 60 | + local bufnr = api.nvim_win_get_buf(win) |
| 61 | + return bufnr >= range[1] and bufnr <= range[2] |
| 62 | + end, |
40 | 63 | api.nvim_list_wins() |
41 | 64 | ) |
42 | 65 |
|
43 | | - -- Get list of valid and listed buffers |
| 66 | + -- Get list of loaded and listed buffers. |
44 | 67 | local buffers = vim.tbl_filter( |
45 | | - function(buf) return |
46 | | - api.nvim_buf_is_valid(buf) and bo[buf].buflisted |
| 68 | + function(buf) |
| 69 | + return api.nvim_buf_is_loaded(buf) and bo[buf].buflisted |
47 | 70 | end, |
48 | 71 | api.nvim_list_bufs() |
49 | 72 | ) |
50 | 73 |
|
51 | | - -- If there is only one buffer (which has to be the current one), Neovim will automatically |
52 | | - -- create a new buffer on :bd. |
53 | | - -- For more than one buffer, pick the next buffer (wrapping around if necessary) |
54 | | - if #buffers > 1 then |
55 | | - for i, v in ipairs(buffers) do |
56 | | - if v == bufnr then |
57 | | - local next_buffer = buffers[i % #buffers + 1] |
58 | | - for _, win in ipairs(windows) do |
59 | | - api.nvim_win_set_buf(win, next_buffer) |
60 | | - end |
| 74 | + -- Get list of loaded and listed buffers outside the range. |
| 75 | + local buffers_outside_range = vim.tbl_filter( |
| 76 | + function(buf) |
| 77 | + return buf < range[1] or buf > range[2] |
| 78 | + end, |
| 79 | + buffers |
| 80 | + ) |
61 | 81 |
|
| 82 | + -- Switch the windows containing the target buffers to a buffer that's not going to be closed. |
| 83 | + -- Create a new buffer if necessary. |
| 84 | + local switch_bufnr |
| 85 | + -- If there are buffers outside range, just switch all target windows to one of them. |
| 86 | + if #buffers_outside_range > 0 then |
| 87 | + local buffer_before_range -- Buffer right before the range. |
| 88 | + -- First, try to find a buffer after the range. If there are no buffers after the range, |
| 89 | + -- use the buffer right before the range instead. |
| 90 | + for _, v in ipairs(buffers_outside_range) do |
| 91 | + if v < range[1] then |
| 92 | + buffer_before_range = v |
| 93 | + end |
| 94 | + if v > range[2] then |
| 95 | + switch_bufnr = v |
62 | 96 | break |
63 | 97 | end |
64 | 98 | end |
| 99 | + -- Couldn't find buffer after range, use buffer before range instead. |
| 100 | + if switch_bufnr == nil then |
| 101 | + switch_bufnr = buffer_before_range |
| 102 | + end |
| 103 | + -- Otherwise create a new buffer and switch all windows to that. |
| 104 | + else |
| 105 | + switch_bufnr = api.nvim_create_buf(true, false) |
| 106 | + |
| 107 | + if switch_bufnr == 0 then |
| 108 | + api.nvim_err_writeln("bufdelete.nvim: Failed to create buffer") |
| 109 | + end |
65 | 110 | end |
66 | 111 |
|
67 | | - -- Check if buffer still exists, to ensure the target buffer wasn't killed |
68 | | - -- due to options like bufhidden=wipe. |
69 | | - if api.nvim_buf_is_valid(bufnr) then |
70 | | - -- Execute the BDeletePre and BDeletePost autocommands before and after deleting the buffer |
71 | | - api.nvim_exec_autocmds("User", { pattern = "BDeletePre" }) |
72 | | - vim.cmd(string.format('%s %d', kill_command, bufnr)) |
73 | | - api.nvim_exec_autocmds("User", { pattern = "BDeletePost" }) |
| 112 | + -- Switch all target windows to the selected buffer. |
| 113 | + for _, win in ipairs(windows) do |
| 114 | + api.nvim_win_set_buf(win, switch_bufnr) |
74 | 115 | end |
| 116 | + |
| 117 | + -- Trigger BDeletePre autocommand. |
| 118 | + api.nvim_exec_autocmds("User", { |
| 119 | + pattern = string.format("BDeletePre {%d,%d}", range[1], range[2]) |
| 120 | + }) |
| 121 | + -- Close all target buffers one by one |
| 122 | + for bufnr=range[1], range[2] do |
| 123 | + if api.nvim_buf_is_loaded(bufnr) then |
| 124 | + -- If buffer is modified and it shouldn't be forced to close, do nothing. |
| 125 | + local use_force = force or bufnr_force[bufnr] |
| 126 | + if not bo[bufnr].modified or use_force then |
| 127 | + if wipeout then |
| 128 | + cmd.bwipeout({ args = {bufnr}, bang = use_force }) |
| 129 | + else |
| 130 | + cmd.bdelete({ args = {bufnr}, bang = use_force }) |
| 131 | + end |
| 132 | + end |
| 133 | + end |
| 134 | + end |
| 135 | + -- Trigger BDeletePost autocommand. |
| 136 | + api.nvim_exec_autocmds("User", { |
| 137 | + pattern = string.format("BDeletePost {%d,%d}", range[1], range[2]) |
| 138 | + }) |
75 | 139 | end |
76 | 140 |
|
77 | | --- Kill the target buffer (or the current one if 0/nil) while retaining window layout |
78 | | -function M.bufdelete(bufnr, force) |
79 | | - buf_kill('bd', bufnr, force) |
| 141 | +-- Find the first buffer whose name matches the provided pattern. Returns buffer handle. |
| 142 | +-- Errors if buffer is not found. |
| 143 | +local function find_buffer_with_pattern(pat) |
| 144 | + for _, bufnr in ipairs(api.nvim_list_bufs()) do |
| 145 | + if api.nvim_buf_is_loaded(bufnr) and api.nvim_buf_get_name(bufnr):match(pat) then |
| 146 | + return bufnr |
| 147 | + end |
| 148 | + end |
| 149 | + |
| 150 | + api.nvim_err_writeln("bufdelete.nvim: No matching buffer for " .. pat) |
80 | 151 | end |
81 | 152 |
|
82 | | --- Wipe the target buffer (or the current one if 0/nil) while retaining window layout |
83 | | -function M.bufwipeout(bufnr, force) |
84 | | - buf_kill('bw', bufnr, force) |
| 153 | +local function get_range(buffer_or_range) |
| 154 | + if buffer_or_range == nil then |
| 155 | + return { 0, 0 } |
| 156 | + elseif type(buffer_or_range) == 'number' and buffer_or_range >= 0 |
| 157 | + and api.nvim_buf_is_valid(buffer_or_range) |
| 158 | + then |
| 159 | + return { buffer_or_range, buffer_or_range } |
| 160 | + elseif type(buffer_or_range) == 'string' then |
| 161 | + local bufnr = find_buffer_with_pattern(buffer_or_range) |
| 162 | + return bufnr ~= nil and { bufnr, bufnr } or nil |
| 163 | + elseif type(buffer_or_range) == 'table' and #buffer_or_range == 2 |
| 164 | + and type(buffer_or_range[1]) == 'number' and buffer_or_range[1] > 0 |
| 165 | + and type(buffer_or_range[2]) == 'number' and buffer_or_range[2] > 0 |
| 166 | + and api.nvim_buf_is_valid(buffer_or_range[1]) |
| 167 | + and api.nvim_buf_is_valid(buffer_or_range[2]) |
| 168 | + then |
| 169 | + if buffer_or_range[1] > buffer_or_range[2] then |
| 170 | + buffer_or_range[1], buffer_or_range[2] = buffer_or_range[2], buffer_or_range[1] |
| 171 | + end |
| 172 | + return buffer_or_range |
| 173 | + else |
| 174 | + api.nvim_err_writeln('bufdelete.nvim: Invalid bufnr or range value provided') |
| 175 | + return |
| 176 | + end |
85 | 177 | end |
86 | 178 |
|
87 | | --- Wrapper around buf_kill for use with vim commands |
88 | | -local function buf_kill_cmd(kill_command, bufnr, bang) |
89 | | - buf_kill(kill_command, tonumber(bufnr == '' and '0' or bufnr), bang == '!') |
| 179 | +-- Kill the target buffer(s) (or the current one if 0/nil) while retaining window layout. |
| 180 | +-- Can accept range to kill multiple buffers. |
| 181 | +function M.bufdelete(buffer_or_range, force) |
| 182 | + buf_kill(get_range(buffer_or_range), force, false) |
90 | 183 | end |
91 | 184 |
|
92 | | --- Wrappers around bufdelete and bufwipeout for use with vim commands |
93 | | -function M.bufdelete_cmd(bufnr, bang) |
94 | | - buf_kill_cmd('bd', bufnr, bang) |
| 185 | +-- Wipe the target buffer(s) (or the current one if 0/nil) while retaining window layout. |
| 186 | +-- Can accept range to wipe multiple buffers. |
| 187 | +function M.bufwipeout(buffer_or_range, force) |
| 188 | + buf_kill(get_range(buffer_or_range), force, true) |
95 | 189 | end |
96 | 190 |
|
97 | | -function M.bufwipeout_cmd(bufnr, bang) |
98 | | - buf_kill_cmd('bw', bufnr, bang) |
| 191 | +-- Wrapper around buf_kill for use with vim commands. |
| 192 | +local function buf_kill_cmd(opts, wipeout) |
| 193 | + local range |
| 194 | + if opts.range == 0 then |
| 195 | + if #opts.fargs == 1 then -- Buffer name is provided |
| 196 | + local bufnr = find_buffer_with_pattern(opts.fargs[1]) |
| 197 | + if bufnr == nil then |
| 198 | + return |
| 199 | + end |
| 200 | + range = { bufnr, bufnr } |
| 201 | + else |
| 202 | + range = { opts.line2, opts.line2 } |
| 203 | + end |
| 204 | + else |
| 205 | + if #opts.fargs == 1 then |
| 206 | + api.nvim_err_writeln("bufdelete.nvim: Cannot use buffer name and buffer number at the " |
| 207 | + .. "same time") |
| 208 | + else |
| 209 | + range = { opts.range == 2 and opts.line1 or opts.line2, opts.line2 } |
| 210 | + end |
| 211 | + end |
| 212 | + buf_kill(range, opts.bang, wipeout) |
99 | 213 | end |
100 | 214 |
|
| 215 | +-- Define Bdelete and Bwipeout. |
| 216 | +api.nvim_create_user_command('Bdelete', function(opts) buf_kill_cmd(opts, false) end, |
| 217 | + { bang = true, count = true, addr = 'buffers', nargs = '?' }) |
| 218 | +api.nvim_create_user_command('Bwipeout', function(opts) buf_kill_cmd(opts, true) end, |
| 219 | + { bang = true, count = true, addr = 'buffers', nargs = '?' }) |
| 220 | + |
101 | 221 | return M |
0 commit comments