Skip to content

Commit

Permalink
feat!: allow buffer name and range. (#22)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
famiu committed Oct 6, 2022
1 parent 46255e4 commit bdaae05
Show file tree
Hide file tree
Showing 4 changed files with 200 additions and 90 deletions.
26 changes: 18 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ Neovim's default `:bdelete` command can be quite annoying, since it also messes

## Requirements

- Neovim >= 0.5
- Latest Neovim stable version.

**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.
**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.

## Installation

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

## Usage

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.
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.

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.
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.

If deletion isn't being forced, you're instead prompted for action for every modified buffer.

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

```lua
-- Force delete current buffer
-- Forcibly delete current buffer
require('bufdelete').bufdelete(0, true)

-- Wipeout buffer number 100 without force
require('bufdelete').bufwipeout(100)

-- Delete every buffer from buffer 7 to buffer 30 without force
require('bufdelete').bufdelete({7, 30})

-- Delete buffer matching foo.txt with force
require('bufdelete').bufdelete("foo.txt", true)
```

## Behavior

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.
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.

## User autocommands

bufdelete.nvim triggers the following User autocommands (see `:help User` for more information):
- `BDeletePre` - Prior to deleting a buffer.
- `BDeletePost` - After deleting a buffer.
- `BDeletePre {range_start,range_end}` - Prior to deleting a buffer.
- `BDeletePost {range_start,range_end}` - After deleting a buffer.

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}`.

## Support

Expand Down
242 changes: 181 additions & 61 deletions lua/bufdelete/init.lua
Original file line number Diff line number Diff line change
@@ -1,101 +1,221 @@
local api = vim.api
local cmd = vim.cmd
local bo = vim.bo

if vim.fn.has('nvim-0.8') == 0 then
api.nvim_err_writeln('bufdelete.nvim is only available for Neovim versions 0.8 and above')
return
end

local M = {}

-- Common kill function for bdelete and bwipeout
local function buf_kill(kill_command, bufnr, force)
-- If buffer is modified and force isn't true, print error and abort
if not force and bo[bufnr].modified then
api.nvim_echo({{
string.format(
'No write since last change for buffer %d. Would you like to:\n' ..
'(s)ave and close\n(i)gnore changes and close\n(c)ancel',
bufnr
)
}}, false, {})

local choice = string.char(vim.fn.getchar())

if choice == 's' or choice == 'S' then
vim.cmd('write')
elseif choice == 'i' or choice == 'I' then
force = true;
else
return
end
-- Common kill function for Bdelete and Bwipeout.
local function buf_kill(range, force, wipeout)
if range == nil then
return
end

if bufnr == 0 or bufnr == nil then
bufnr = api.nvim_get_current_buf()
if range[1] == 0 then
range[1] = api.nvim_get_current_buf()
range[2] = range[1]
end

if force then
kill_command = kill_command .. '!'
-- Whether to forcibly close buffer. If true, use force. If false, simply ignore the buffer.
local bufnr_force

-- If force is disabled, check for modified buffers in range.
if not force then
-- List of modified buffers the user asked to close with force.
bufnr_force = {}

for bufnr=range[1], range[2] do
-- If buffer is modified, prompt user for action.
if api.nvim_buf_is_loaded(bufnr) and bo[bufnr].modified then
api.nvim_echo({{
string.format(
'No write since last change for buffer %d (%s). Would you like to:\n' ..
'(s)ave and close\n(i)gnore changes and close\n(c)ancel',
bufnr, api.nvim_buf_get_name(bufnr)
)
}}, false, {})

local choice = string.char(vim.fn.getchar())

if choice == 's' or choice == 'S' then -- Save changes to the buffer.
api.nvim_buf_call(bufnr, function() cmd.write() end)
elseif choice == 'i' or choice == 'I' then -- Ignore and forcibly close.
bufnr_force[bufnr] = true
end -- Otherwise, do nothing with this buffer.

-- Clear message area.
cmd.echo('""')
cmd.redraw()
end
end
end

-- Get list of windows IDs with the buffer to close
-- Get list of windows IDs with the buffers to close.
local windows = vim.tbl_filter(
function(win) return api.nvim_win_get_buf(win) == bufnr end,
function(win)
local bufnr = api.nvim_win_get_buf(win)
return bufnr >= range[1] and bufnr <= range[2]
end,
api.nvim_list_wins()
)

-- Get list of valid and listed buffers
-- Get list of loaded and listed buffers.
local buffers = vim.tbl_filter(
function(buf) return
api.nvim_buf_is_valid(buf) and bo[buf].buflisted
function(buf)
return api.nvim_buf_is_loaded(buf) and bo[buf].buflisted
end,
api.nvim_list_bufs()
)

-- If there is only one buffer (which has to be the current one), Neovim will automatically
-- create a new buffer on :bd.
-- For more than one buffer, pick the next buffer (wrapping around if necessary)
if #buffers > 1 then
for i, v in ipairs(buffers) do
if v == bufnr then
local next_buffer = buffers[i % #buffers + 1]
for _, win in ipairs(windows) do
api.nvim_win_set_buf(win, next_buffer)
end
-- Get list of loaded and listed buffers outside the range.
local buffers_outside_range = vim.tbl_filter(
function(buf)
return buf < range[1] or buf > range[2]
end,
buffers
)

-- Switch the windows containing the target buffers to a buffer that's not going to be closed.
-- Create a new buffer if necessary.
local switch_bufnr
-- If there are buffers outside range, just switch all target windows to one of them.
if #buffers_outside_range > 0 then
local buffer_before_range -- Buffer right before the range.
-- First, try to find a buffer after the range. If there are no buffers after the range,
-- use the buffer right before the range instead.
for _, v in ipairs(buffers_outside_range) do
if v < range[1] then
buffer_before_range = v
end
if v > range[2] then
switch_bufnr = v
break
end
end
-- Couldn't find buffer after range, use buffer before range instead.
if switch_bufnr == nil then
switch_bufnr = buffer_before_range
end
-- Otherwise create a new buffer and switch all windows to that.
else
switch_bufnr = api.nvim_create_buf(true, false)

if switch_bufnr == 0 then
api.nvim_err_writeln("bufdelete.nvim: Failed to create buffer")
end
end

-- Check if buffer still exists, to ensure the target buffer wasn't killed
-- due to options like bufhidden=wipe.
if api.nvim_buf_is_valid(bufnr) then
-- Execute the BDeletePre and BDeletePost autocommands before and after deleting the buffer
api.nvim_exec_autocmds("User", { pattern = "BDeletePre" })
vim.cmd(string.format('%s %d', kill_command, bufnr))
api.nvim_exec_autocmds("User", { pattern = "BDeletePost" })
-- Switch all target windows to the selected buffer.
for _, win in ipairs(windows) do
api.nvim_win_set_buf(win, switch_bufnr)
end

-- Trigger BDeletePre autocommand.
api.nvim_exec_autocmds("User", {
pattern = string.format("BDeletePre {%d,%d}", range[1], range[2])
})
-- Close all target buffers one by one
for bufnr=range[1], range[2] do
if api.nvim_buf_is_loaded(bufnr) then
-- If buffer is modified and it shouldn't be forced to close, do nothing.
local use_force = force or bufnr_force[bufnr]
if not bo[bufnr].modified or use_force then
if wipeout then
cmd.bwipeout({ args = {bufnr}, bang = use_force })
else
cmd.bdelete({ args = {bufnr}, bang = use_force })
end
end
end
end
-- Trigger BDeletePost autocommand.
api.nvim_exec_autocmds("User", {
pattern = string.format("BDeletePost {%d,%d}", range[1], range[2])
})
end

-- Kill the target buffer (or the current one if 0/nil) while retaining window layout
function M.bufdelete(bufnr, force)
buf_kill('bd', bufnr, force)
-- Find the first buffer whose name matches the provided pattern. Returns buffer handle.
-- Errors if buffer is not found.
local function find_buffer_with_pattern(pat)
for _, bufnr in ipairs(api.nvim_list_bufs()) do
if api.nvim_buf_is_loaded(bufnr) and api.nvim_buf_get_name(bufnr):match(pat) then
return bufnr
end
end

api.nvim_err_writeln("bufdelete.nvim: No matching buffer for " .. pat)
end

-- Wipe the target buffer (or the current one if 0/nil) while retaining window layout
function M.bufwipeout(bufnr, force)
buf_kill('bw', bufnr, force)
local function get_range(buffer_or_range)
if buffer_or_range == nil then
return { 0, 0 }
elseif type(buffer_or_range) == 'number' and buffer_or_range >= 0
and api.nvim_buf_is_valid(buffer_or_range)
then
return { buffer_or_range, buffer_or_range }
elseif type(buffer_or_range) == 'string' then
local bufnr = find_buffer_with_pattern(buffer_or_range)
return bufnr ~= nil and { bufnr, bufnr } or nil
elseif type(buffer_or_range) == 'table' and #buffer_or_range == 2
and type(buffer_or_range[1]) == 'number' and buffer_or_range[1] > 0
and type(buffer_or_range[2]) == 'number' and buffer_or_range[2] > 0
and api.nvim_buf_is_valid(buffer_or_range[1])
and api.nvim_buf_is_valid(buffer_or_range[2])
then
if buffer_or_range[1] > buffer_or_range[2] then
buffer_or_range[1], buffer_or_range[2] = buffer_or_range[2], buffer_or_range[1]
end
return buffer_or_range
else
api.nvim_err_writeln('bufdelete.nvim: Invalid bufnr or range value provided')
return
end
end

-- Wrapper around buf_kill for use with vim commands
local function buf_kill_cmd(kill_command, bufnr, bang)
buf_kill(kill_command, tonumber(bufnr == '' and '0' or bufnr), bang == '!')
-- Kill the target buffer(s) (or the current one if 0/nil) while retaining window layout.
-- Can accept range to kill multiple buffers.
function M.bufdelete(buffer_or_range, force)
buf_kill(get_range(buffer_or_range), force, false)
end

-- Wrappers around bufdelete and bufwipeout for use with vim commands
function M.bufdelete_cmd(bufnr, bang)
buf_kill_cmd('bd', bufnr, bang)
-- Wipe the target buffer(s) (or the current one if 0/nil) while retaining window layout.
-- Can accept range to wipe multiple buffers.
function M.bufwipeout(buffer_or_range, force)
buf_kill(get_range(buffer_or_range), force, true)
end

function M.bufwipeout_cmd(bufnr, bang)
buf_kill_cmd('bw', bufnr, bang)
-- Wrapper around buf_kill for use with vim commands.
local function buf_kill_cmd(opts, wipeout)
local range
if opts.range == 0 then
if #opts.fargs == 1 then -- Buffer name is provided
local bufnr = find_buffer_with_pattern(opts.fargs[1])
if bufnr == nil then
return
end
range = { bufnr, bufnr }
else
range = { opts.line2, opts.line2 }
end
else
if #opts.fargs == 1 then
api.nvim_err_writeln("bufdelete.nvim: Cannot use buffer name and buffer number at the "
.. "same time")
else
range = { opts.range == 2 and opts.line1 or opts.line2, opts.line2 }
end
end
buf_kill(range, opts.bang, wipeout)
end

-- Define Bdelete and Bwipeout.
api.nvim_create_user_command('Bdelete', function(opts) buf_kill_cmd(opts, false) end,
{ bang = true, count = true, addr = 'buffers', nargs = '?' })
api.nvim_create_user_command('Bwipeout', function(opts) buf_kill_cmd(opts, true) end,
{ bang = true, count = true, addr = 'buffers', nargs = '?' })

return M
1 change: 1 addition & 0 deletions plugin/bufdelete.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require('bufdelete')
21 changes: 0 additions & 21 deletions plugin/bufdelete.vim

This file was deleted.

0 comments on commit bdaae05

Please sign in to comment.