Skip to content
This repository was archived by the owner on Feb 17, 2025. It is now read-only.

Commit bdaae05

Browse files
authored
feat!: allow buffer name and range. (#22)
Adds support for using buffer name and range with `:Bdelete`, `:Bwipeout` and their Lua function counterparts. Also contains some refactors and documentation improvements. BREAKING-CHANGE: Removes support for older Neovim versions. Changes `BDeletePre` and `BDeletePost` autocommand patterns to also contain range.
1 parent 46255e4 commit bdaae05

File tree

4 files changed

+200
-90
lines changed

4 files changed

+200
-90
lines changed

README.md

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ Neovim's default `:bdelete` command can be quite annoying, since it also messes
66

77
## Requirements
88

9-
- Neovim >= 0.5
9+
- Latest Neovim stable version.
1010

11-
**NOTE:** The plugin may work on older versions, but I can't test it out myself. So if you use an older Neovim version and the plugin works for you. Please file an issue informing me about your current Neovim version.
11+
**NOTE:** The plugin may work on older versions, but it will always be developed with the latest stable version in mind. So if you use a distribution or operating system that has older versions instead of the newest one, either compile the latest version of Neovim from source or use the plugin with the older version at your own risk. Do NOT open any issues if the plugin doesn't work with an older version of Neovim.
1212

1313
## Installation
1414

@@ -24,29 +24,39 @@ Plug 'famiu/bufdelete.nvim'
2424

2525
## Usage
2626

27-
bufdelete.nvim is quite straightforward to use. It provides two commands, `:Bdelete` and `:Bwipeout`. They work similarly to `:bdelete` and `:bwipeout`, except they keep your window layout intact. It's also possible to use `:Bdelete!` or `:Bwipeout!` to force the deletion. You may also pass a buffer number to either of those two commands to delete that buffer instead of the current one.
27+
bufdelete.nvim is quite straightforward to use. It provides two commands, `:Bdelete` and `:Bwipeout`. They work similarly to `:bdelete` and `:bwipeout`, except they keep your window layout intact. It's also possible to use `:Bdelete!` or `:Bwipeout!` to force the deletion. You may also pass a buffer number, range or buffer name / regexp to either of those two commands.
2828

29-
There's also two Lua functions provided by bufdelete.nvim, `bufdelete` and `bufwipeout`, which do the same thing as their command counterparts. Both of them take two arguments, `bufnr` and `force`, where `bufnr` is the number of the buffer, and `force` determines whether to force the deletion or not. If `bufnr` is either `0` or `nil`, it deletes the current buffer instead.
29+
There's also two Lua functions provided by bufdelete.nvim, `bufdelete` and `bufwipeout`, which do the same thing as their command counterparts. Both of them take two arguments, `buffer_or_range` and `force`. `buffer_or_range` is the buffer number (e.g. `12`), buffer name / regexp (e.g. `foo.txt` or `^bar.txt$`) or a range, which is a table containing two buffer numbers (e.g. `{7, 13}`). `force` determines whether to force the deletion or not. If `buffer_or_range` is either `0` or `nil`, it deletes the current buffer instead. Note that you can't use `0` or `nil` if `buffer_or_range` is a range.
30+
31+
If deletion isn't being forced, you're instead prompted for action for every modified buffer.
3032

3133
Here's an example of how to use the functions:
3234

3335
```lua
34-
-- Force delete current buffer
36+
-- Forcibly delete current buffer
3537
require('bufdelete').bufdelete(0, true)
3638

3739
-- Wipeout buffer number 100 without force
3840
require('bufdelete').bufwipeout(100)
41+
42+
-- Delete every buffer from buffer 7 to buffer 30 without force
43+
require('bufdelete').bufdelete({7, 30})
44+
45+
-- Delete buffer matching foo.txt with force
46+
require('bufdelete').bufdelete("foo.txt", true)
3947
```
4048

4149
## Behavior
4250

43-
By default, when you delete a buffer, bufdelete.nvim switches to the next buffer (wrapping around if necessary) in every window where the target buffer was open. If no buffer other than the target buffer was open, bufdelete.nvim creates an empty buffer and switches to it instead.
51+
By default, when you delete buffers, bufdelete.nvim switches to a different buffer in every window where one of the target buffers was open. If no buffer other than the target buffers was open, bufdelete creates an empty buffer and switches to it instead.
4452

4553
## User autocommands
4654

4755
bufdelete.nvim triggers the following User autocommands (see `:help User` for more information):
48-
- `BDeletePre` - Prior to deleting a buffer.
49-
- `BDeletePost` - After deleting a buffer.
56+
- `BDeletePre {range_start,range_end}` - Prior to deleting a buffer.
57+
- `BDeletePost {range_start,range_end}` - After deleting a buffer.
58+
59+
In both of these cases, `range_start` and `range_end` are replaced by the start and end of the buffer range, respectively. For example, if you use `require('bufdelete').bufdelete({1, 42})`, the autocommand patterns will be `BDeletePre {1,42}` and `BDeletePost {1,42}`.
5060

5161
## Support
5262

lua/bufdelete/init.lua

Lines changed: 181 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,221 @@
11
local api = vim.api
2+
local cmd = vim.cmd
23
local bo = vim.bo
34

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+
410
local M = {}
511

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
2716
end
2817

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]
3121
end
3222

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
3555
end
3656

37-
-- Get list of windows IDs with the buffer to close
57+
-- Get list of windows IDs with the buffers to close.
3858
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,
4063
api.nvim_list_wins()
4164
)
4265

43-
-- Get list of valid and listed buffers
66+
-- Get list of loaded and listed buffers.
4467
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
4770
end,
4871
api.nvim_list_bufs()
4972
)
5073

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+
)
6181

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
6296
break
6397
end
6498
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
65110
end
66111

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)
74115
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+
})
75139
end
76140

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)
80151
end
81152

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
85177
end
86178

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)
90183
end
91184

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)
95189
end
96190

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)
99213
end
100214

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+
101221
return M

plugin/bufdelete.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
require('bufdelete')

plugin/bufdelete.vim

Lines changed: 0 additions & 21 deletions
This file was deleted.

0 commit comments

Comments
 (0)