mirror of
https://github.com/neovim/neovim.git
synced 2026-07-17 14:41:33 +00:00
fix(statusline): defer redraw in aucmd context #40309
Problem: `nvim_exec_autocmds({buf=...})` may temporarily switch curwin/curbuf
through `aucmd_prepbuf()`. Requested statuslines/winbars before
`aucmd_restbuf()` may erroneously see the target window as current.
Solution: Track `aucmd_prepbuf()` window-switch depth and leave statusline/winbar
redraws marked dirty until the original window is restored.
This commit is contained in:
@@ -1346,6 +1346,9 @@ void aucmd_prepbuf(aco_save_T *aco, buf_T *buf)
|
||||
curbuf = buf;
|
||||
aco->new_curwin_handle = curwin->handle;
|
||||
set_bufref(&aco->new_curbuf, curbuf);
|
||||
if (aco->new_curwin_handle != aco->save_curwin_handle) {
|
||||
aucmd_prepbuf_depth++;
|
||||
}
|
||||
|
||||
aco->save_VIsual_active = VIsual_active;
|
||||
if (!same_buffer) {
|
||||
@@ -1481,6 +1484,10 @@ win_found:
|
||||
if (VIsual_active) {
|
||||
check_pos(curbuf, &VIsual);
|
||||
}
|
||||
if (aco->new_curwin_handle != aco->save_curwin_handle) {
|
||||
assert(aucmd_prepbuf_depth > 0);
|
||||
aucmd_prepbuf_depth--;
|
||||
}
|
||||
}
|
||||
|
||||
/// Schedules an autocommand event, to be executed at the next event-loop tick.
|
||||
|
||||
@@ -22,6 +22,7 @@ EXTERN win_T *last_cursormoved_win INIT( = NULL);
|
||||
EXTERN pos_T last_cursormoved INIT( = { 0, 0, 0 });
|
||||
|
||||
EXTERN bool autocmd_busy INIT( = false); ///< Is apply_autocmds() busy?
|
||||
EXTERN int aucmd_prepbuf_depth INIT( = 0); ///< Nested aucmd_prepbuf() window switches
|
||||
EXTERN int autocmd_no_enter INIT( = false); ///< Buf/WinEnter autocmds disabled
|
||||
EXTERN int autocmd_no_leave INIT( = false); ///< Buf/WinLeave autocmds disabled
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "nvim/api/private/defs.h"
|
||||
#include "nvim/api/private/helpers.h"
|
||||
#include "nvim/ascii_defs.h"
|
||||
#include "nvim/autocmd.h"
|
||||
#include "nvim/buffer.h"
|
||||
#include "nvim/buffer_defs.h"
|
||||
#include "nvim/charset.h"
|
||||
@@ -62,6 +63,18 @@ typedef enum {
|
||||
kNumBaseHexadecimal = 16,
|
||||
} NumberBase;
|
||||
|
||||
static bool stl_defer_redraw(win_T *wp)
|
||||
{
|
||||
if (aucmd_prepbuf_depth == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// aucmd_prepbuf() temporarily changes curwin/curbuf. Statusline and winbar
|
||||
// expressions can observe that context, so evaluate them after aucmd_restbuf().
|
||||
wp->w_redr_status = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Redraw the status line of window `wp`.
|
||||
///
|
||||
/// If inversion is possible we use it. Else '=' characters are used.
|
||||
@@ -77,6 +90,9 @@ void win_redr_status(win_T *wp)
|
||||
|| (wild_menu_showing != 0 && !ui_has(kUIWildmenu))) {
|
||||
return;
|
||||
}
|
||||
if (stl_defer_redraw(wp)) {
|
||||
return;
|
||||
}
|
||||
busy = true;
|
||||
wp->w_redr_status = false;
|
||||
if (wp->w_status_height == 0 && !(is_stl_global && wp == curwin)) {
|
||||
@@ -432,6 +448,9 @@ void win_redr_winbar(win_T *wp)
|
||||
if (entered) {
|
||||
return;
|
||||
}
|
||||
if (stl_defer_redraw(wp)) {
|
||||
return;
|
||||
}
|
||||
entered = true;
|
||||
|
||||
if (wp->w_winbar_height == 0 || !redrawing()) {
|
||||
|
||||
@@ -959,13 +959,16 @@ describe('statusline', function()
|
||||
it('no cmdline ruler for autocmd window #39938', function()
|
||||
command('set ruler laststatus=2')
|
||||
api.nvim_create_autocmd('BufDelete', { command = 'redrawstatus' })
|
||||
api.nvim_exec_autocmds('BufDelete', { buf = api.nvim_create_buf(true, true) })
|
||||
screen:expect([[
|
||||
local expected = [[
|
||||
^ |
|
||||
{1:~ }|*5
|
||||
{2:[No Name] 0,0-1 All}|
|
||||
{3:[No Name] 0,0-1 All}|
|
||||
|
|
||||
]])
|
||||
]]
|
||||
screen:expect(expected)
|
||||
api.nvim_exec_autocmds('BufDelete', { buf = api.nvim_create_buf(true, true) })
|
||||
-- The first matching frame can arrive before the deferred redraw settles.
|
||||
screen:expect_unchanged(true)
|
||||
end)
|
||||
|
||||
it('active window is correct after nvim_exec_autocmds({buf}) #40153', function()
|
||||
@@ -976,17 +979,32 @@ describe('statusline', function()
|
||||
api.nvim_set_current_buf(target)
|
||||
api.nvim_set_current_win(caller_win)
|
||||
exec_lua(function(buf)
|
||||
_G.in_aucmd_statusline = false
|
||||
_G.statusline_contexts = {}
|
||||
_G.Status = function()
|
||||
return vim.api.nvim_get_current_win() == vim.g.statusline_winid and 'CUR' or 'nc'
|
||||
local current = vim.api.nvim_get_current_win()
|
||||
local statusline = vim.g.statusline_winid
|
||||
local focus = current == statusline
|
||||
table.insert(_G.statusline_contexts, {
|
||||
current = current,
|
||||
focus = focus,
|
||||
in_aucmd = _G.in_aucmd_statusline,
|
||||
statusline = statusline,
|
||||
})
|
||||
return focus and 'CUR' or 'nc'
|
||||
end
|
||||
vim.o.statusline = '%!v:lua.Status()'
|
||||
vim.api.nvim_create_autocmd('User', {
|
||||
buffer = buf,
|
||||
callback = function(ev)
|
||||
_G.in_aucmd_statusline = true
|
||||
vim.api.nvim__redraw({ buf = ev.buf, statusline = true })
|
||||
_G.in_aucmd_statusline = false
|
||||
end,
|
||||
})
|
||||
end, target)
|
||||
-- Ignore statusline evaluations from setup; only the autocmd redraw matters.
|
||||
exec_lua('_G.statusline_contexts = {}')
|
||||
api.nvim_exec_autocmds('User', { buf = target })
|
||||
screen:expect([[
|
||||
|
|
||||
@@ -997,6 +1015,81 @@ describe('statusline', function()
|
||||
{3:CUR }|
|
||||
|
|
||||
]])
|
||||
-- `%!` may evaluate during the callback, but must not see the target as focused.
|
||||
eq(
|
||||
{},
|
||||
exec_lua(function(win)
|
||||
return vim
|
||||
.iter(_G.statusline_contexts)
|
||||
:filter(function(context)
|
||||
return context.in_aucmd and context.focus and context.current ~= win
|
||||
end)
|
||||
:totable()
|
||||
end, caller_win)
|
||||
)
|
||||
end)
|
||||
|
||||
it('defers statusline evaluation during nvim_exec_autocmds({buf}) #40153', function()
|
||||
command('set laststatus=2')
|
||||
local caller_win = api.nvim_get_current_win()
|
||||
command('split')
|
||||
local target = api.nvim_create_buf(true, false)
|
||||
api.nvim_set_current_buf(target)
|
||||
api.nvim_set_current_win(caller_win)
|
||||
exec_lua(function(buf)
|
||||
_G.in_aucmd_statusline = false
|
||||
_G.statusline_contexts = {}
|
||||
_G.Status = function()
|
||||
table.insert(_G.statusline_contexts, {
|
||||
in_aucmd = _G.in_aucmd_statusline,
|
||||
current = vim.api.nvim_get_current_win(),
|
||||
actual = tonumber(vim.g.actual_curwin or -1),
|
||||
})
|
||||
return vim.api.nvim_get_current_win() == tonumber(vim.g.actual_curwin or -1) and 'CUR'
|
||||
or 'nc'
|
||||
end
|
||||
-- `%{%...%}` sets g:actual_curwin while evaluating the statusline.
|
||||
vim.o.statusline = '%{%v:lua.Status()%}'
|
||||
vim.api.nvim_create_autocmd('User', {
|
||||
buffer = buf,
|
||||
callback = function(ev)
|
||||
_G.in_aucmd_statusline = true
|
||||
vim.api.nvim__redraw({ buf = ev.buf, statusline = true })
|
||||
_G.in_aucmd_statusline = false
|
||||
end,
|
||||
})
|
||||
end, target)
|
||||
screen:expect([[
|
||||
|
|
||||
{1:~ }|*2
|
||||
{2:nc }|
|
||||
^ |
|
||||
{1:~ }|
|
||||
{3:CUR }|
|
||||
|
|
||||
]])
|
||||
-- Ignore statusline evaluations from setup; only the autocmd redraw matters.
|
||||
exec_lua('_G.statusline_contexts = {}')
|
||||
api.nvim_exec_autocmds('User', { buf = target })
|
||||
-- No different frame should be flushed while the deferred redraw settles.
|
||||
screen:expect({ unchanged = true })
|
||||
local contexts = exec_lua(function()
|
||||
return _G.statusline_contexts
|
||||
end)
|
||||
eq(true, #contexts > 0)
|
||||
eq(caller_win, contexts[#contexts].actual)
|
||||
-- The statusline must be evaluated only after the autocmd context is restored.
|
||||
eq(
|
||||
{},
|
||||
exec_lua(function()
|
||||
return vim
|
||||
.iter(_G.statusline_contexts)
|
||||
:filter(function(context)
|
||||
return context.in_aucmd
|
||||
end)
|
||||
:totable()
|
||||
end)
|
||||
)
|
||||
end)
|
||||
end)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user