feat(api): add ev.win to autocmd callback args #39826

Problem: Autocmd callbacks currently have to rely on curwin, which may
already have changed by the time they run. This is especially noticeable
for WinNew, WinClosed, WinResized and WinScrolled.

Solution: Track the window in the autocmd context and expose it as ev.win.
Pass it explicitly for the window events that need it.
This commit is contained in:
glepnir
2026-09-10 03:42:20 +08:00
committed by GitHub
parent ac167f943a
commit 08dd3adffb
14 changed files with 209 additions and 35 deletions

View File

@@ -2204,15 +2204,17 @@ nvim_create_autocmd({event}, {opts}) *nvim_create_autocmd()*
triggered. Lua callback can return |lua-truthy| to delete
the autocommand. Callback receives one argument, a table
with keys: *event-args*
• buf: (`number`) <abuf>
• buf: (`integer`) <abuf>
• data: (`any`) Arbitrary data passed from
|nvim_exec_autocmds()| *event-data*
• event: (`vim.api.keyset.events`) Name of the triggered
event |autocmd-events|
• file: (`string`) <afile> (not expanded to a full path)
• group: (`number?`) Group id, if any
• id: (`number`) Autocommand id
• group: (`integer?`) Group id, if any
• id: (`integer`) Autocommand id
• match: (`string`) <amatch> (expanded to a full path)
• win: (`integer`) id of the window for the event, see
|window-ID|
• command (string?) Vim command executed on event. Not
allowed with {callback}.
• desc (`string?`) Description (for documentation and

View File

@@ -238,6 +238,7 @@ API
• |nvim_set_option_value()| returns the new option value.
• |nvim_create_autocmd()| is now |api-fast|, so it can be called from a fast
event context (e.g. |vim.uv| callbacks).
• |nvim_create_autocmd()| callback |event-args| provide `win`.
BUILD

View File

@@ -969,13 +969,14 @@ function vim.api.nvim_create_augroup(name, opts) end
--- - callback (`function|string?`) Lua function (or Vimscript function name, if string)
--- called when the event(s) is triggered. Lua callback can return `lua-truthy` to delete
--- the autocommand. Callback receives one argument, a table with keys: [event-args]()
--- - buf: (`number`) [<abuf>]
--- - buf: (`integer`) [<abuf>]
--- - data: (`any`) Arbitrary data passed from [nvim_exec_autocmds()] [event-data]()
--- - event: (`vim.api.keyset.events`) Name of the triggered event `autocmd-events`
--- - file: (`string`) [<afile>] (not expanded to a full path)
--- - group: (`number?`) Group id, if any
--- - id: (`number`) Autocommand id
--- - group: (`integer?`) Group id, if any
--- - id: (`integer`) Autocommand id
--- - match: (`string`) [<amatch>] (expanded to a full path)
--- - win: (`integer`) id of the window for the event, see `window-ID`
--- - command (string?) Vim command executed on event. Not allowed with {callback}.
--- - desc (`string?`) Description (for documentation and troubleshooting).
--- - group (`string|integer?`) Group name or id to match against.

View File

@@ -76,6 +76,7 @@ error('Cannot require a meta file')
--- @field match string expanded value of <amatch>
--- @field buf integer expanded value of <abuf>
--- @field file string expanded value of <afile>
--- @field win integer id of the window for the event, see |window-ID|
--- @field data? any arbitrary data passed from |nvim_exec_autocmds()|
--- @class vim.api.keyset.create_user_command.command_args

View File

@@ -373,13 +373,14 @@ cleanup:
/// - callback (`function|string?`) Lua function (or Vimscript function name, if string)
/// called when the event(s) is triggered. Lua callback can return |lua-truthy| to delete
/// the autocommand. Callback receives one argument, a table with keys: [event-args]()
/// - buf: (`number`) [<abuf>]
/// - buf: (`integer`) [<abuf>]
/// - data: (`any`) Arbitrary data passed from [nvim_exec_autocmds()] [event-data]()
/// - event: (`vim.api.keyset.events`) Name of the triggered event |autocmd-events|
/// - file: (`string`) [<afile>] (not expanded to a full path)
/// - group: (`number?`) Group id, if any
/// - id: (`number`) Autocommand id
/// - group: (`integer?`) Group id, if any
/// - id: (`integer`) Autocommand id
/// - match: (`string`) [<amatch>] (expanded to a full path)
/// - win: (`integer`) id of the window for the event, see |window-ID|
/// - command (string?) Vim command executed on event. Not allowed with {callback}.
/// - desc (`string?`) Description (for documentation and troubleshooting).
/// - group (`string|integer?`) Group name or id to match against.
@@ -764,8 +765,8 @@ void nvim_exec_autocmds(Object event, Dict(exec_autocmds) *opts, Arena *arena, E
FOREACH_ITEM(patterns, pat, {
char *fname = !has_buf ? pat.data.string.data : NULL;
did_aucmd |= apply_autocmds_group(event_nr, fname, NULL, true, au_group, b, NULL, data,
has_buf);
did_aucmd |= apply_autocmds_group(event_nr, fname, NULL, true, au_group, b, curwin, NULL,
data, has_buf);
})
})

View File

@@ -299,7 +299,7 @@ Window nvim_open_win(Buffer buf, Boolean enter, Dict(win_config) *config, Error
const bool result = ctx_switch(&switchwin, wp, tp, NULL, kCtxNoDisplay);
assert(result);
(void)result;
if (apply_autocmds(EVENT_WINNEW, NULL, NULL, false, curbuf)) {
if (apply_autocmds_win(EVENT_WINNEW, NULL, NULL, false, curbuf, wp)) {
tp = win_find_tabpage(wp);
}
ctx_restore(&switchwin);

View File

@@ -88,6 +88,10 @@ static const char e_autocommand_nesting_too_deep[]
// Code for automatic commands.
static AutoPatCmd *active_apc_list = NULL; // stack of active autocommands
/// Window handle for `ev.win`. Unlike autocmd_bufnr/_match this needn't be a
/// global: only au_callback() reads it.
static handle_T autocmd_winid = 0;
// ID for associating autocmds created via nvim_create_autocmd
// Used to delete autocmds from nvim_del_autocmd
static int next_augroup_id = 1;
@@ -1165,7 +1169,7 @@ int do_doautocmd(char *arg_start, bool do_msg, bool *did_something)
// Loop over the events.
while (*arg && !ends_excmd(*arg) && !ascii_iswhite(*arg)) {
if (apply_autocmds_group(event_name2nr(arg, &arg), fname, NULL, true, group,
curbuf, NULL, NULL, false)) {
curbuf, curwin, NULL, NULL, false)) {
nothing_done = false;
}
}
@@ -1311,9 +1315,11 @@ static void deferred_event(void **argv)
}
tv_dict_set_keys_readonly(v_event);
win_T *win = curwin; // before ctx_switch() may make the context window current
CtxSwitch aco = { 0 };
ctx_switch(&aco, NULL, NULL, buf, 0);
apply_autocmds_group(event, fname, fname_io, false, group, buf, eap, data, false);
apply_autocmds_group(event, fname, fname_io, false, group, buf, win, eap, data, false);
ctx_restore(&aco);
restore_v_event(v_event, &save_v_event);
@@ -1359,6 +1365,8 @@ void aucmd_defer_modified(buf_T *buf, bool new_val)
/// Execute autocommands for "event" and file name "fname".
///
/// `ev.win` is curwin; use apply_autocmds_win() for window-specific events.
///
/// @param event event that occurred
/// @param fname filename, NULL or empty means use actual file name
/// @param fname_io filename to use for <afile> on cmdline
@@ -1368,7 +1376,20 @@ void aucmd_defer_modified(buf_T *buf, bool new_val)
/// @return true if some commands were executed.
bool apply_autocmds(event_T event, char *fname, char *fname_io, bool force, buf_T *buf)
{
return apply_autocmds_group(event, fname, fname_io, force, AUGROUP_ALL, buf, NULL, NULL, false);
return apply_autocmds_group(event, fname, fname_io, force, AUGROUP_ALL, buf, curwin, NULL, NULL,
false);
}
/// Like apply_autocmds(), but with an explicit window for `ev.win`.
///
/// @param win Window for `ev.win`, NULL for the current window.
///
/// @return true if some commands were executed.
bool apply_autocmds_win(event_T event, char *fname, char *fname_io, bool force, buf_T *buf,
win_T *win)
{
return apply_autocmds_group(event, fname, fname_io, force, AUGROUP_ALL, buf, win, NULL, NULL,
false);
}
/// Like apply_autocmds(), but with extra "eap" argument. This takes care of
@@ -1385,7 +1406,8 @@ bool apply_autocmds(event_T event, char *fname, char *fname_io, bool force, buf_
bool apply_autocmds_exarg(event_T event, char *fname, char *fname_io, bool force, buf_T *buf,
exarg_T *eap)
{
return apply_autocmds_group(event, fname, fname_io, force, AUGROUP_ALL, buf, eap, NULL, false);
return apply_autocmds_group(event, fname, fname_io, force, AUGROUP_ALL, buf, curwin, eap, NULL,
false);
}
/// Like apply_autocmds(), but handles the caller's retval. If the script
@@ -1408,8 +1430,8 @@ bool apply_autocmds_retval(event_T event, char *fname, char *fname_io, bool forc
return false;
}
bool did_cmd = apply_autocmds_group(event, fname, fname_io, force, AUGROUP_ALL, buf, NULL, NULL,
false);
bool did_cmd = apply_autocmds_group(event, fname, fname_io, force, AUGROUP_ALL, buf, curwin, NULL,
NULL, false);
if (did_cmd && aborting()) {
*retval = FAIL;
}
@@ -1453,12 +1475,13 @@ bool trigger_cursorhold(void) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
/// @param force Ignore autocmd_busy (force "++nested" behavior)
/// @param group autocmd group ID or AUGROUP_ALL
/// @param buf Buffer for <abuf>
/// @param win Window for `ev.win`, NULL for the current window.
/// @param eap Ex command arguments
/// @param with_buf Run callbacks with "buf" as the current buffer
///
/// @return true if some commands were executed.
bool apply_autocmds_group(event_T event, char *fname, char *fname_io, bool force, int group,
buf_T *buf, exarg_T *eap, Object *data, bool with_buf)
buf_T *buf, win_T *win, exarg_T *eap, Object *data, bool with_buf)
{
char *sfname = NULL; // short file name
bool retval = false;
@@ -1539,6 +1562,7 @@ bool apply_autocmds_group(event_T event, char *fname, char *fname_io, bool force
char *save_autocmd_fname = autocmd_fname;
bool save_autocmd_fname_full = autocmd_fname_full;
int save_autocmd_bufnr = autocmd_bufnr;
handle_T save_autocmd_winid = autocmd_winid;
char *save_autocmd_match = autocmd_match;
int save_autocmd_busy = autocmd_busy;
int save_autocmd_nested = autocmd_nested;
@@ -1640,6 +1664,10 @@ bool apply_autocmds_group(event_T event, char *fname, char *fname_io, bool force
goto BYPASS_AU;
}
// Set the window handle for `ev.win`. After the last "goto BYPASS_AU", which
// skips the restore below, and before ctx_switch().
autocmd_winid = win != NULL ? win->handle : (curwin != NULL ? curwin->handle : 0);
if (with_buf && buf != NULL && buf != curbuf) {
ctx_switch(&aco, NULL, NULL, buf, 0);
}
@@ -1770,6 +1798,7 @@ bool apply_autocmds_group(event_T event, char *fname, char *fname_io, bool force
autocmd_fname = save_autocmd_fname;
autocmd_fname_full = save_autocmd_fname_full;
autocmd_bufnr = save_autocmd_bufnr;
autocmd_winid = save_autocmd_winid;
autocmd_match = save_autocmd_match;
current_sctx = save_current_sctx;
restore_funccal();
@@ -1839,7 +1868,7 @@ void do_termresponse_autocmd(const String sequence, uint64_t channel_id)
MAXSIZE_TEMP_DICT(data, 2);
PUT_C(data, "sequence", STRING_OBJ(sequence));
PUT_C(data, "chan", INTEGER_OBJ((Integer)channel_id));
apply_autocmds_group(EVENT_TERMRESPONSE, NULL, NULL, true, AUGROUP_ALL, NULL, NULL,
apply_autocmds_group(EVENT_TERMRESPONSE, NULL, NULL, true, AUGROUP_ALL, NULL, curwin, NULL,
&DICT_OBJ(data), false);
termresponse_changed = true;
termresponse_chan_id = channel_id;
@@ -1945,12 +1974,13 @@ static bool au_callback(const AutoCmd *ac, const AutoPatCmd *apc)
{
Callback callback = ac->handler_fn;
if (callback.type == kCallbackLua) {
MAXSIZE_TEMP_DICT(data, 7);
MAXSIZE_TEMP_DICT(data, 8);
PUT_C(data, "id", INTEGER_OBJ(ac->id));
PUT_C(data, "event", CSTR_AS_OBJ(event_nr2name(apc->event)));
PUT_C(data, "file", CSTR_AS_OBJ(apc->afile_orig));
PUT_C(data, "match", CSTR_AS_OBJ(autocmd_match));
PUT_C(data, "buf", INTEGER_OBJ(autocmd_bufnr));
PUT_C(data, "win", INTEGER_OBJ(autocmd_winid));
if (apc->data) {
PUT_C(data, "data", *apc->data);

View File

@@ -636,8 +636,8 @@ bool close_buffer(win_T *win, buf_T *buf, int action, bool abort_if_last, bool i
if (win_valid && win->w_buffer == buf && buf->b_nwindows == 1) {
buf->b_locked++;
buf->b_locked_split++;
if (apply_autocmds(EVENT_BUFWINLEAVE, buf->b_fname, buf->b_fname, false,
buf) && !bufref_valid(&bufref)) {
if (apply_autocmds_win(EVENT_BUFWINLEAVE, buf->b_fname, buf->b_fname, false,
buf, win) && !bufref_valid(&bufref)) {
// Autocommands deleted the buffer.
emsg(_(e_auabort));
return false;

View File

@@ -4725,7 +4725,7 @@ bool before_quit_autocmds(win_T *wp, bool quit_all, bool forceit)
if (*get_vim_var_str(VV_EXITREASON) == NUL) {
set_vim_var_string(VV_EXITREASON, S_LEN("quit"));
}
apply_autocmds(EVENT_QUITPRE, NULL, NULL, false, wp->w_buffer);
apply_autocmds_win(EVENT_QUITPRE, NULL, NULL, false, wp->w_buffer, wp);
// Bail out when autocommands closed the window.
// Refuse to quit when the buffer in the last window is being closed (can

View File

@@ -795,7 +795,7 @@ void getout(int exitval)
bufref_T bufref;
set_bufref(&bufref, buf);
apply_autocmds(EVENT_BUFWINLEAVE, buf->b_fname, buf->b_fname, false, buf);
apply_autocmds_win(EVENT_BUFWINLEAVE, buf->b_fname, buf->b_fname, false, buf, wp);
if (bufref_valid(&bufref)) {
buf_set_changedtick(buf, -1); // note that we did it already
}

View File

@@ -1184,7 +1184,7 @@ void do_autocmd_progress(MsgID msg_id, HlMessage msg, MessageData *msg_data)
apply_autocmds_group(EVENT_PROGRESS,
(msg_data && msg_data->source.size > 0) ? msg_data->source.data : "", NULL,
true,
AUGROUP_ALL, NULL, NULL, &DICT_OBJ(data), false);
AUGROUP_ALL, NULL, curwin, NULL, &DICT_OBJ(data), false);
kv_destroy(messages);
}

View File

@@ -280,7 +280,7 @@ static void emit_termrequest(void **argv)
VTERM_TERMINATOR_BEL ? STATIC_CSTR_AS_OBJ("\x07") : STATIC_CSTR_AS_OBJ("\x1b\\"));
term->refcount++;
apply_autocmds_group(EVENT_TERMREQUEST, NULL, NULL, true, AUGROUP_ALL, buf, NULL,
apply_autocmds_group(EVENT_TERMREQUEST, NULL, NULL, true, AUGROUP_ALL, buf, curwin, NULL,
&DICT_OBJ(data), false);
term->refcount--;
xfree(sequence);
@@ -744,7 +744,7 @@ void terminal_close(Terminal **termpp, int status)
PUT_C(data, "pos", INTEGER_OBJ(pos));
apply_autocmds_group(EVENT_TERMCLOSE, NULL, NULL, status >= 0, AUGROUP_ALL,
buf, NULL, &DICT_OBJ(data), false);
buf, curwin, NULL, &DICT_OBJ(data), false);
restore_v_event(dict, &save_v_event);
}

View File

@@ -3119,7 +3119,7 @@ static void do_autocmd_winclosed(win_T *win)
recursive = true;
char winid[NUMBUFLEN];
vim_snprintf(winid, sizeof(winid), "%d", win->handle);
apply_autocmds(EVENT_WINCLOSED, winid, winid, false, win->w_buffer);
apply_autocmds_win(EVENT_WINCLOSED, winid, winid, false, win->w_buffer, win);
recursive = false;
}
@@ -6084,17 +6084,21 @@ void may_trigger_win_scrolled_resized(void)
// Save window info before autocmds since they can free windows
char resize_winid[NUMBUFLEN];
win_T *resize_win = NULL;
bufref_T resize_bufref;
if (trigger_resize) {
vim_snprintf(resize_winid, sizeof(resize_winid), "%d", first_size_win->handle);
set_bufref(&resize_bufref, first_size_win->w_buffer);
resize_win = first_size_win;
vim_snprintf(resize_winid, sizeof(resize_winid), "%d", resize_win->handle);
set_bufref(&resize_bufref, resize_win->w_buffer);
}
char scroll_winid[NUMBUFLEN];
win_T *scroll_win = NULL;
bufref_T scroll_bufref;
if (trigger_scroll) {
vim_snprintf(scroll_winid, sizeof(scroll_winid), "%d", first_scroll_win->handle);
set_bufref(&scroll_bufref, first_scroll_win->w_buffer);
scroll_win = first_scroll_win;
vim_snprintf(scroll_winid, sizeof(scroll_winid), "%d", scroll_win->handle);
set_bufref(&scroll_bufref, scroll_win->w_buffer);
}
// If both are to be triggered do WinResized first.
@@ -6105,7 +6109,9 @@ void may_trigger_win_scrolled_resized(void)
if (tv_dict_add_list(v_event, S_LEN("windows"), windows_list) == OK) {
tv_dict_set_keys_readonly(v_event);
buf_T *buf = bufref_valid(&resize_bufref) ? resize_bufref.br_buf : curbuf;
apply_autocmds(EVENT_WINRESIZED, resize_winid, resize_winid, false, buf);
// May have been freed by an earlier autocmd.
win_T *win = win_valid_any_tab(resize_win) ? resize_win : curwin;
apply_autocmds_win(EVENT_WINRESIZED, resize_winid, resize_winid, false, buf, win);
}
restore_v_event(v_event, &save_v_event);
}
@@ -6120,7 +6126,9 @@ void may_trigger_win_scrolled_resized(void)
tv_dict_unref(scroll_dict);
buf_T *buf = bufref_valid(&scroll_bufref) ? scroll_bufref.br_buf : curbuf;
apply_autocmds(EVENT_WINSCROLLED, scroll_winid, scroll_winid, false, buf);
// May have been freed by the WinResized autocmds above.
win_T *win = win_valid_any_tab(scroll_win) ? scroll_win : curwin;
apply_autocmds_win(EVENT_WINSCROLLED, scroll_winid, scroll_winid, false, buf, win);
restore_v_event(v_event, &save_v_event);
}

View File

@@ -370,6 +370,7 @@ describe('autocmd api', function()
event = event,
match = amatch,
file = exec_pat,
win = api.nvim_get_current_win(),
buf = 1,
}, api.nvim_get_var('autocmd_args'))
@@ -392,6 +393,7 @@ describe('autocmd api', function()
event = event,
match = amatch,
file = exec_pat,
win = api.nvim_get_current_win(),
buf = 1,
}, api.nvim_get_var('autocmd_args'))
end
@@ -406,6 +408,134 @@ describe('autocmd api', function()
end)
end)
describe('ev.win #25844', function()
it('is the current window for events that are not about a window', function()
local win = api.nvim_get_current_win()
exec_lua(function()
vim.api.nvim_create_autocmd('BufAdd', {
callback = function(ev)
vim.g.ev_win = ev.win
end,
})
end)
command('badd some_file')
eq(win, api.nvim_get_var('ev_win'))
end)
it('WinClosed reports the closed window', function()
command('split')
local closed_win = api.nvim_get_current_win()
exec_lua(function()
vim.api.nvim_create_autocmd('WinClosed', {
callback = function(ev)
vim.g.ev_win = ev.win
vim.g.ev_match = ev.match
end,
})
end)
command('quit')
eq(closed_win, api.nvim_get_var('ev_win'))
eq(tostring(closed_win), api.nvim_get_var('ev_match'))
end)
it('WinNew reports the new window, not the focused one', function()
local origin_win = api.nvim_get_current_win()
local buf = api.nvim_create_buf(false, true)
exec_lua(function()
vim.api.nvim_create_autocmd('WinNew', {
callback = function(ev)
vim.g.ev_win = ev.win
end,
})
end)
local new_win = api.nvim_open_win(buf, false, {
relative = 'editor',
row = 0,
col = 0,
width = 10,
height = 5,
})
eq(origin_win, api.nvim_get_current_win())
eq(new_win, api.nvim_get_var('ev_win'))
end)
it('WinResized reports the resized window, not curwin', function()
local top_win = api.nvim_get_current_win()
command('botright split')
local bot_win = api.nvim_get_current_win()
neq(top_win, bot_win)
command('redraw') -- flush the WinResized caused by the split itself
exec_lua(function()
vim.api.nvim_create_autocmd('WinResized', {
callback = function(ev)
vim.g.ev_win = ev.win
vim.g.ev_match = ev.match
vim.g.ev_curwin = vim.api.nvim_get_current_win()
end,
})
end)
command('resize 3')
command('redraw')
-- ev.win is the first window whose size changed, matching <amatch>.
eq(top_win, api.nvim_get_var('ev_win'))
eq(tostring(top_win), api.nvim_get_var('ev_match'))
eq(bot_win, api.nvim_get_var('ev_curwin'))
end)
it('WinScrolled reports the scrolled window, not curwin', function()
command('split')
local scrolled = api.nvim_get_current_win()
exec_lua(function()
local lines = {}
for i = 1, 30 do
lines[i] = 'line ' .. i
end
vim.api.nvim_buf_set_lines(0, 0, -1, false, lines)
end)
command('wincmd w')
neq(scrolled, api.nvim_get_current_win())
command('redraw')
exec_lua(function(scroll_win)
vim.api.nvim_create_autocmd('WinScrolled', {
callback = function(ev)
vim.g.ev_win = ev.win
vim.g.ev_match = ev.match
vim.g.ev_curwin = vim.api.nvim_get_current_win()
end,
})
vim.api.nvim_win_call(scroll_win, function()
vim.cmd('normal! G')
end)
end, scrolled)
command('redraw')
eq(scrolled, api.nvim_get_var('ev_win'))
eq(tostring(scrolled), api.nvim_get_var('ev_match'))
neq(scrolled, api.nvim_get_var('ev_curwin'))
end)
it('is restored after a nested autocmd', function()
command('edit a')
command('rightbelow vsplit b')
command('wincmd w')
local trigger_win = api.nvim_get_current_win()
exec_lua(function()
-- Fires BufLeave/WinLeave/WinEnter/BufEnter, each with its own ev.win.
vim.api.nvim_create_autocmd('ColorScheme', {
callback = function()
vim.cmd('wincmd w')
end,
})
vim.api.nvim_create_autocmd('ColorScheme', {
callback = function(ev)
vim.g.ev_win = ev.win
end,
})
end)
command('colorscheme blue')
eq(trigger_win, api.nvim_get_var('ev_win'))
end)
end)
it('can receive arbitrary data', function()
local function test(data)
eq(