diff --git a/src/nvim/api/options.c b/src/nvim/api/options.c index b014a569a1..9abab26737 100644 --- a/src/nvim/api/options.c +++ b/src/nvim/api/options.c @@ -19,6 +19,7 @@ #include "nvim/option.h" #include "nvim/types_defs.h" #include "nvim/vim_defs.h" +#include "nvim/window.h" #include "api/options.c.generated.h" @@ -149,6 +150,27 @@ static buf_T *do_ft_buf(const char *filetype, aco_save_T *aco, bool *aco_used, E return ftbuf; } +static void wipe_ft_buf(buf_T *buf) + FUNC_ATTR_NONNULL_ALL +{ + block_autocmds(); + + bufref_T bufref; + set_bufref(&bufref, buf); + + close_windows(buf, false); + // Autocommands are blocked, but 'bufhidden' may have wiped it already. + // Also can't wipe if the buffer is somehow still in a window or current. + if (bufref_valid(&bufref) && buf != curbuf && buf->b_nwindows == 0) { + wipe_buffer(buf, false); + } + if (bufref_valid(&bufref)) { + buf->b_flags &= ~BF_DUMMY; // Couldn't wipe; keep it instead. + } + + unblock_autocmds(); +} + /// Gets the value of an option. The behavior of this function matches that of /// |:set|: the local value of an option is returned if it exists; otherwise, /// the global value is returned. Local values always correspond to the current @@ -191,10 +213,8 @@ Object nvim_get_option_value(String name, Dict(option) *opts, Error *err) aucmd_restbuf(&aco); } if (ftbuf != NULL) { - assert(curbuf != ftbuf); // safety check - wipe_buffer(ftbuf, false); + wipe_ft_buf(ftbuf); } - return (Object)OBJECT_INIT; } @@ -210,8 +230,7 @@ Object nvim_get_option_value(String name, Dict(option) *opts, Error *err) // restore curwin/curbuf and a few other things aucmd_restbuf(&aco); } - assert(curbuf != ftbuf); // safety check - wipe_buffer(ftbuf, false); + wipe_ft_buf(ftbuf); } if (ERROR_SET(err)) { diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua index 323b66ecbc..7fb5c2d3d5 100644 --- a/test/functional/api/vim_spec.lua +++ b/test/functional/api/vim_spec.lua @@ -1995,6 +1995,46 @@ describe('API', function() ) end) + it('does not crash if autocmds open dummy buffer in other windows', function() + exec [[ + autocmd FileType * ++once let g:dummy_buf = bufnr() | split + + " Autocommands should be blocked while Nvim attempts to wipe the buffer. + let g:wipe_events = [] + autocmd WinClosed * if winbufnr(expand('')) == g:dummy_buf + \| let g:wipe_events += ['WinClosed'] + \| endif + autocmd BufWipeout * if expand('') == g:dummy_buf + \| let g:wipe_events += ['BufWipeout'] + \| endif + ]] + api.nvim_get_option_value('formatexpr', { filetype = 'lua' }) + eq(0, eval('bufexists(g:dummy_buf)')) + eq({}, eval('win_findbuf(g:dummy_buf)')) + eq({}, eval('g:wipe_events')) + + -- Be an ABSOLUTE nuisance and make it the only window to prevent it from wiping. + -- Do it this way to avoid E813 from :only trying to close the autocmd window. + command('autocmd FileType * ++once let g:dummy_buf = bufnr() | split | wincmd w | quit') + api.nvim_get_option_value('formatexpr', { filetype = 'lua' }) + eq(1, eval('bufexists(g:dummy_buf)')) + + -- Ensure the buffer does not remain as a dummy by checking that we can switch to it. + local old_win = api.nvim_get_current_win() + command('execute g:dummy_buf "sbuffer"') + eq(eval('g:dummy_buf'), api.nvim_get_current_buf()) + neq(old_win, api.nvim_get_current_win()) + eq({}, eval('g:wipe_events')) + end) + + it('does not crash if dummy buffer wiped after autocommands', function() + -- Autocommands are blocked while Nvim attempts to wipe the buffer, but check something like + -- &bufhidden = "wipe" causing a premature wipe doesn't crash. + command('autocmd FileType * ++once setlocal bufhidden=wipe | split') + api.nvim_get_option_value('formatexpr', { filetype = 'lua' }) + assert_alive() + end) + it('sets dummy buffer options without side-effects', function() exec [[ let g:events = []