Skip to content

Commit dfe3092

Browse files
committed
fix(ALL): handle :%bdelete if a module has persistent scratch buffer
Details: - For either performance or user convenience (less increasing of buffer ids), some modules create a helper scratch buffer once and reuse it whenever it is needed. Like 'mini.clue' and 'mini.notify' use one buffer to show its content. Or like 'mini.icons' and 'mini.notify' have specific buffers to enable certain kind of behavior. Those need to be handled (re-created and re-cached) if the user deletes all buffers. This was previously handled for `:%bwipeout` case, but not for `:%bdelete`. The difference is that `:bdelete` only unloads all buffers without actually deleting them. Among other things, this means that all their options are removed. In particular 'buftype' option is reset making those buffers regular and not scratch (`buftype=nofile`) yet still unlisted ("invisible" to the user). This can have problematic side effects. Like if the intended scratch buffer is used to show text, it will become modified which has negative user effects (like `:quit` doesn't work since there are modified buffers). - A solution is to handle buffer deletion not by checking with `nvim_buf_is_valid()`, but by a combination of `nvim_buf_is_loaded()` check and `pcall(nvim_buf_delete, buf_id, { force = true })` cleanup if the buffer is not loaded. Resolve #2365
1 parent c67822c commit dfe3092

File tree

12 files changed

+201
-23
lines changed

12 files changed

+201
-23
lines changed

lua/mini/animate.lua

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1803,7 +1803,8 @@ H.make_openclose_step = function(action_type, win_id, config)
18031803
end
18041804

18051805
-- Empty buffer should always be valid (might have been closed by user command)
1806-
if H.empty_buf_id == nil or not vim.api.nvim_buf_is_valid(H.empty_buf_id) then
1806+
if H.empty_buf_id == nil or not vim.api.nvim_buf_is_loaded(H.empty_buf_id) then
1807+
pcall(vim.api.nvim_buf_delete, H.empty_buf_id, { force = true })
18071808
H.empty_buf_id = vim.api.nvim_create_buf(false, true)
18081809
H.set_buf_name(H.empty_buf_id, 'open-close-scratch')
18091810
end

lua/mini/clue.lua

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1719,7 +1719,8 @@ end
17191719
-- Buffer ---------------------------------------------------------------------
17201720
H.buffer_update = function()
17211721
local buf_id = H.state.buf_id
1722-
if not H.is_valid_buf(buf_id) then
1722+
if not H.is_loaded_buf(buf_id) then
1723+
pcall(vim.api.nvim_buf_delete, buf_id, { force = true })
17231724
buf_id = vim.api.nvim_create_buf(false, true)
17241725
H.set_buf_name(buf_id, 'content')
17251726
end
@@ -2008,6 +2009,8 @@ end
20082009

20092010
H.is_valid_buf = function(buf_id) return type(buf_id) == 'number' and vim.api.nvim_buf_is_valid(buf_id) end
20102011

2012+
H.is_loaded_buf = function(buf_id) return type(buf_id) == 'number' and vim.api.nvim_buf_is_loaded(buf_id) end
2013+
20112014
H.is_valid_win = function(win_id) return type(win_id) == 'number' and vim.api.nvim_win_is_valid(win_id) end
20122015

20132016
H.fit_to_width = function(text, width)

lua/mini/completion.lua

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1762,8 +1762,10 @@ end
17621762

17631763
-- Helpers for floating windows -----------------------------------------------
17641764
H.ensure_buffer = function(cache, name)
1765-
if H.is_valid_buf(cache.bufnr) then return end
1765+
if H.is_loaded_buf(cache.bufnr) then return end
17661766

1767+
pcall(vim.api.nvim_buf_delete, cache.bufnr, { force = true })
1768+
cache.hl_filetype = nil
17671769
local buf_id = vim.api.nvim_create_buf(false, true)
17681770
cache.bufnr = buf_id
17691771
H.set_buf_name(buf_id, name)
@@ -1840,7 +1842,7 @@ H.close_action_window = function(cache)
18401842
cache.win_id = nil
18411843

18421844
-- For some reason 'buftype' might be reset. Ensure that buffer is scratch.
1843-
if H.is_valid_buf(cache.bufnr) then vim.bo[cache.bufnr].buftype = 'nofile' end
1845+
if H.is_loaded_buf(cache.bufnr) then vim.bo[cache.bufnr].buftype = 'nofile' end
18441846
end
18451847

18461848
-- Utilities ------------------------------------------------------------------
@@ -1853,7 +1855,7 @@ end
18531855

18541856
H.set_buf_name = function(buf_id, name) vim.api.nvim_buf_set_name(buf_id, 'minicompletion://' .. buf_id .. '/' .. name) end
18551857

1856-
H.is_valid_buf = function(buf_id) return type(buf_id) == 'number' and vim.api.nvim_buf_is_valid(buf_id) end
1858+
H.is_loaded_buf = function(buf_id) return type(buf_id) == 'number' and vim.api.nvim_buf_is_loaded(buf_id) end
18571859

18581860
H.is_valid_win = function(win_id) return type(win_id) == 'number' and vim.api.nvim_win_is_valid(win_id) end
18591861

lua/mini/icons.lua

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2209,7 +2209,8 @@ H.filetype_match = function(filename)
22092209
-- (needed because the function in many ambiguous cases prefers to return
22102210
-- nothing if there is no buffer supplied)
22112211
local buf_id = H.scratch_buf_id
2212-
if buf_id == nil or not vim.api.nvim_buf_is_valid(buf_id) then
2212+
if buf_id == nil or not vim.api.nvim_buf_is_loaded(buf_id) then
2213+
pcall(vim.api.nvim_buf_delete, buf_id, { force = true })
22132214
buf_id = vim.api.nvim_create_buf(false, true)
22142215
H.set_buf_name(buf_id, 'filetype-match-scratch')
22152216
H.scratch_buf_id = buf_id

lua/mini/map.lua

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -552,7 +552,8 @@ MiniMap.open = function(opts)
552552

553553
-- Open buffer and window
554554
local buf_id = MiniMap.current.buf_data.map
555-
if buf_id == nil or not vim.api.nvim_buf_is_valid(buf_id) then
555+
if buf_id == nil or not vim.api.nvim_buf_is_loaded(buf_id) then
556+
pcall(vim.api.nvim_buf_delete, buf_id, { force = true })
556557
buf_id = H.create_map_buffer()
557558
MiniMap.current.buf_data.map = buf_id
558559
end

lua/mini/notify.lua

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,7 @@ MiniNotify.refresh = function()
483483

484484
-- Refresh buffer
485485
local buf_id = H.cache.buf_id
486-
if not H.is_valid_buf(buf_id) then buf_id = H.buffer_create() end
486+
if not H.is_loaded_buf(buf_id) then buf_id = H.buffer_create(buf_id) end
487487
H.buffer_refresh(buf_id, notif_arr)
488488

489489
-- Refresh window
@@ -746,7 +746,8 @@ H.lsp_progress_handler = function(err, result, ctx, config)
746746
end
747747

748748
-- Buffer ---------------------------------------------------------------------
749-
H.buffer_create = function()
749+
H.buffer_create = function(prev_buf_id)
750+
pcall(vim.api.nvim_buf_delete, prev_buf_id, { force = true })
750751
local buf_id = vim.api.nvim_create_buf(false, true)
751752
H.set_buf_name(buf_id, 'content')
752753
vim.bo[buf_id].filetype = 'mininotify'
@@ -903,14 +904,15 @@ end
903904

904905
H.set_buf_name = function(buf_id, name) vim.api.nvim_buf_set_name(buf_id, 'mininotify://' .. buf_id .. '/' .. name) end
905906

906-
H.is_valid_buf = function(buf_id) return type(buf_id) == 'number' and vim.api.nvim_buf_is_valid(buf_id) end
907+
H.is_loaded_buf = function(buf_id) return type(buf_id) == 'number' and vim.api.nvim_buf_is_loaded(buf_id) end
907908

908909
H.is_valid_win = function(win_id) return type(win_id) == 'number' and vim.api.nvim_win_is_valid(win_id) end
909910

910911
H.is_win_in_tabpage = function(win_id) return vim.api.nvim_win_get_tabpage(win_id) == vim.api.nvim_get_current_tabpage() end
911912

912913
H.is_textlock = function()
913-
if not H.is_valid_buf(H.cache.textlock_buf_id) then
914+
if not H.is_loaded_buf(H.cache.textlock_buf_id) then
915+
pcall(vim.api.nvim_buf_delete, H.cache.textlock_buf_id, { force = true })
914916
H.cache.textlock_buf_id = vim.api.nvim_create_buf(false, true)
915917
H.set_buf_name(H.cache.textlock_buf_id, 'textlock-check-scratch')
916918
end

tests/test_animate.lua

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2497,6 +2497,32 @@ T['Open']['triggers done event'] = function()
24972497
eq(child.lua_get('_G.inside_done_event'), true)
24982498
end
24992499

2500+
T['Open']['handles deleting all buffers'] = function()
2501+
child.bo.modified = false
2502+
local validate = function()
2503+
child.cmd('topleft vertical new')
2504+
sleep(small_time)
2505+
2506+
-- Special helper buffer should be always present and work as expected
2507+
local helper_buf_id, ref_pattern = nil, '^minianimate://%d+/open%-close%-scratch$'
2508+
for _, buf_id in ipairs(child.api.nvim_list_bufs()) do
2509+
if string.find(child.api.nvim_buf_get_name(buf_id), ref_pattern) ~= nil then helper_buf_id = buf_id end
2510+
end
2511+
eq(type(helper_buf_id), 'number')
2512+
eq(child.api.nvim_get_option_value('modified', { buf = helper_buf_id }), false)
2513+
2514+
sleep(3 * step_time + small_time)
2515+
end
2516+
2517+
validate()
2518+
2519+
child.cmd('%bwipeout')
2520+
validate()
2521+
2522+
child.cmd('%bdelete')
2523+
validate()
2524+
end
2525+
25002526
T['Open']['respects `vim.{g,b}.minianimate_disable`'] = new_set({
25012527
parametrize = { { 'g' }, { 'b' } },
25022528
}, {

tests/test_clue.lua

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1790,6 +1790,31 @@ T['Showing keys']['does not trigger unnecessary events'] = function()
17901790
eq(child.lua_get('_G.n_events'), vim.NIL)
17911791
end
17921792

1793+
T['Showing keys']['handles deleting all buffers'] = function()
1794+
make_test_map('n', '<Space>aa')
1795+
load_module({ triggers = { { mode = 'n', keys = '<Space>' } }, window = { delay = 0 } })
1796+
1797+
local validate = function()
1798+
type_keys(' ')
1799+
type_keys('<Esc>')
1800+
1801+
-- Special helper buffer should be always present and work as expected
1802+
local helper_buf_id, ref_pattern = nil, '^miniclue://%d+/content$'
1803+
for _, buf_id in ipairs(child.api.nvim_list_bufs()) do
1804+
if string.find(child.api.nvim_buf_get_name(buf_id), ref_pattern) ~= nil then helper_buf_id = buf_id end
1805+
end
1806+
eq(type(helper_buf_id), 'number')
1807+
eq(child.api.nvim_get_option_value('modified', { buf = helper_buf_id }), false)
1808+
end
1809+
1810+
validate()
1811+
1812+
child.cmd('%bwipeout')
1813+
validate()
1814+
child.cmd('%bdelete')
1815+
validate()
1816+
end
1817+
17931818
T['Showing keys']['respects `vim.b.miniclue_config`'] = function()
17941819
make_test_map('n', '<Space>a')
17951820
load_module({ triggers = { { mode = 'n', keys = '<Space>' } }, window = { delay = 0 } })

tests/test_completion.lua

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,17 @@ local validate_single_floating_win = function(opts)
128128
end
129129
end
130130

131+
local validate_helper_buf = function(name_suffix)
132+
-- Special helper buffer should be always present and work as expected
133+
local helper_buf_id, ref_pattern = nil, '^minicompletion://%d+/' .. vim.pesc(name_suffix) .. '$'
134+
for _, buf_id in ipairs(child.api.nvim_list_bufs()) do
135+
if string.find(child.api.nvim_buf_get_name(buf_id), ref_pattern) then helper_buf_id = buf_id end
136+
end
137+
eq(type(helper_buf_id), 'number')
138+
eq(child.api.nvim_get_option_value('modified', { buf = helper_buf_id }), false)
139+
return helper_buf_id
140+
end
141+
131142
-- Time constants
132143
local default_completion_delay, default_info_delay, default_signature_delay = 100, 100, 50
133144
local small_time = helpers.get_time_const(10)
@@ -1875,6 +1886,32 @@ T['Information window']['handles outdated scheduled showing'] = function()
18751886
eq(child.cmd_capture('messages'), '')
18761887
end
18771888

1889+
T['Information window']['handles deleting all buffers'] = function()
1890+
local validate = function()
1891+
new_buffer()
1892+
mock_lsp()
1893+
validate_info_win(default_info_delay)
1894+
local helper_buf_id = validate_helper_buf('item-info')
1895+
1896+
-- Should highlight with attached markdown tree-sitter parser
1897+
child.lua('_G.helper_buf_id = ' .. helper_buf_id)
1898+
eq(child.lua_get('vim.treesitter.get_parser(_G.helper_buf_id, "markdown") ~= nil', { helper_buf_id }), true)
1899+
1900+
child.ensure_normal_mode()
1901+
for _, buf_id in ipairs(child.api.nvim_list_bufs()) do
1902+
child.api.nvim_set_option_value('modified', false, { buf = buf_id })
1903+
end
1904+
end
1905+
1906+
validate()
1907+
1908+
child.cmd('%bwipeout')
1909+
validate()
1910+
1911+
child.cmd('%bdelete')
1912+
validate()
1913+
end
1914+
18781915
T['Information window']['respects `vim.{g,b}.minicompletion_disable`'] = new_set({
18791916
parametrize = { { 'g' }, { 'b' } },
18801917
}, {
@@ -2154,15 +2191,30 @@ T['Signature help']['is closed when forced outside of Insert mode'] = new_set(
21542191
}
21552192
)
21562193

2157-
T['Signature help']['handles all buffer wipeout'] = function()
2158-
validate_signature_win(default_signature_delay)
2159-
child.ensure_normal_mode()
2194+
T['Signature help']['handles deleting all buffers'] = function()
2195+
local validate = function()
2196+
new_buffer()
2197+
child.bo.filetype = 'aaa'
2198+
mock_lsp()
2199+
validate_signature_win(default_signature_delay)
2200+
local helper_buf_id = validate_helper_buf('signature-help')
21602201

2161-
child.cmd('%bw!')
2162-
new_buffer()
2163-
mock_lsp()
2202+
-- Should highlight with custom syntax
2203+
eq(child.api.nvim_get_option_value('syntax', { buf = helper_buf_id }), 'aaa')
21642204

2165-
validate_signature_win(default_signature_delay)
2205+
child.ensure_normal_mode()
2206+
for _, buf_id in ipairs(child.api.nvim_list_bufs()) do
2207+
child.api.nvim_set_option_value('modified', false, { buf = buf_id })
2208+
end
2209+
end
2210+
2211+
validate()
2212+
2213+
child.cmd('%bwipeout')
2214+
validate()
2215+
2216+
child.cmd('%bdelete')
2217+
validate()
21662218
end
21672219

21682220
T['Signature help']['respects `vim.{g,b}.minicompletion_disable`'] = new_set({

tests/test_icons.lua

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -653,16 +653,28 @@ T['get()']['can be used without `setup()`'] = function()
653653
eq(child.lua_get('{ require("mini.icons").get("default", "file") }'), { '󰈔', 'MiniIconsGrey', false })
654654
end
655655

656-
T['get()']['can be used after deleting all buffers'] = function()
656+
T['get()']['handles deleting all buffers'] = function()
657657
-- As `vim.filetype.match()` requries a buffer to be more useful, make sure
658-
-- that this cached buffer is persistent
658+
-- that there is a persistent helper buffer
659+
local validate = function()
660+
local helper_buf_id, ref_pattern = nil, '^miniicons://%d+/filetype%-match%-scratch$'
661+
for _, buf_id in ipairs(child.api.nvim_list_bufs()) do
662+
if string.find(child.api.nvim_buf_get_name(buf_id), ref_pattern) ~= nil then helper_buf_id = buf_id end
663+
end
664+
eq(type(helper_buf_id), 'number')
665+
eq(child.api.nvim_get_option_value('modified', { buf = helper_buf_id }), false)
666+
end
667+
659668
eq(get('file', 'hello.xpm'), { '󰍹', 'MiniIconsYellow', false })
660-
-- The helper scratch buffer should be properly named
661-
eq(child.api.nvim_buf_get_name(2), 'miniicons://2/filetype-match-scratch')
669+
validate()
662670

663671
child.cmd('%bwipeout')
664672
eq(get('file', 'hello.tcsh'), { '', 'MiniIconsAzure', false })
665-
eq(child.api.nvim_buf_get_name(3), 'miniicons://3/filetype-match-scratch')
673+
validate()
674+
675+
child.cmd('%bdelete')
676+
eq(get('file', 'hello.zsh'), { '', 'MiniIconsGreen', false })
677+
validate()
666678
end
667679

668680
T['get()']['uses width one glyphs'] = function()

0 commit comments

Comments
 (0)