fix(api): nvim_exec_autocmds({buf=x}) runs in buffer context #39061

Problem: `nvim_exec_autocmds({ buf = ... })` matches the target buffer, but callbacks and modelines run with the caller buffer current rather than the target buffer.

Solution: Execute the buffered path in prepared target-buffer context and restore the caller afterward.
This commit is contained in:
Barrett Ruth
2026-05-20 03:48:55 -04:00
committed by GitHub
parent ea8f1463dd
commit 5181984db9
9 changed files with 206 additions and 31 deletions

View File

@@ -2238,14 +2238,15 @@ nvim_exec_autocmds({event}, {opts}) *nvim_exec_autocmds()*
• {event} (`vim.api.keyset.events|vim.api.keyset.events[]`) Event(s) to
execute.
• {opts} (`vim.api.keyset.exec_autocmds`) Optional filters:
• buf (`integer?`) Buffer id |autocmd-buflocal|. Not allowed
with {pattern}.
• buf (`integer?`) Buffer where the event is applied.
|autocmd-buflocal| Not allowed with {pattern}.
• data (`any`): Arbitrary data passed to the callback. See
|nvim_create_autocmd()|.
• group (`string|integer?`) Group name or id to match
against. |autocmd-groups|.
• modeline (`boolean?`, default: true) Process the modeline
after the autocommands <nomodeline>.
after the autocommands <nomodeline>. Ignored if `buf` is
given.
• pattern (`string|array?`, default: current file name)
|autocmd-pattern|. Not allowed with {buf}.

View File

@@ -1235,11 +1235,11 @@ function vim.api.nvim_exec2(src, opts) end
--- @see `:help :doautocmd`
--- @param event vim.api.keyset.events|vim.api.keyset.events[] Event(s) to execute.
--- @param opts vim.api.keyset.exec_autocmds Optional filters:
--- - buf (`integer?`) Buffer id `autocmd-buflocal`. Not allowed with {pattern}.
--- - buf (`integer?`) Buffer where the event is applied. `autocmd-buflocal` Not allowed with {pattern}.
--- - data (`any`): Arbitrary data passed to the callback. See `nvim_create_autocmd()`.
--- - group (`string|integer?`) Group name or id to match against. `autocmd-groups`.
--- - modeline (`boolean?`, default: true) Process the modeline after the autocommands
--- [<nomodeline>].
--- [<nomodeline>]. Ignored if `buf` is given.
--- - pattern (`string|array?`, default: current file name) `autocmd-pattern`. Not allowed with {buf}.
function vim.api.nvim_exec_autocmds(event, opts) end

View File

@@ -190,13 +190,10 @@ function TSHighlighter:destroy()
vim.b[self.bufnr].ts_highlight = nil
api.nvim_buf_clear_namespace(self.bufnr, ns, 0, -1)
if vim.g.syntax_on == 1 then
-- FileType autocmds commonly assume curbuf is the target buffer, so nvim_buf_call.
api.nvim_buf_call(self.bufnr, function()
api.nvim_exec_autocmds(
'FileType',
{ group = 'syntaxset', buf = self.bufnr, modeline = false }
)
end)
api.nvim_exec_autocmds(
'FileType',
{ group = 'syntaxset', buf = self.bufnr, modeline = false }
)
end
end
end

View File

@@ -23,6 +23,7 @@
#include "nvim/lua/executor.h"
#include "nvim/memory.h"
#include "nvim/memory_defs.h"
#include "nvim/option.h"
#include "nvim/strings.h"
#include "nvim/types_defs.h"
#include "nvim/vim_defs.h"
@@ -680,11 +681,11 @@ void nvim_del_augroup_by_name(String name, Error *err)
/// Executes handlers for {event} that match the corresponding {opts} query. |autocmd-execute|
/// @param event Event(s) to execute.
/// @param opts Optional filters:
/// - buf (`integer?`) Buffer id |autocmd-buflocal|. Not allowed with {pattern}.
/// - buf (`integer?`) Buffer where the event is applied. |autocmd-buflocal| Not allowed with {pattern}.
/// - data (`any`): Arbitrary data passed to the callback. See |nvim_create_autocmd()|.
/// - group (`string|integer?`) Group name or id to match against. |autocmd-groups|.
/// - modeline (`boolean?`, default: true) Process the modeline after the autocommands
/// [<nomodeline>].
/// [<nomodeline>]. Ignored if `buf` is given.
/// - pattern (`string|array?`, default: current file name) |autocmd-pattern|. Not allowed with {buf}.
/// @see |:doautocmd|
void nvim_exec_autocmds(Object event, Dict(exec_autocmds) *opts, Arena *arena, Error *err)
@@ -759,11 +760,12 @@ 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);
did_aucmd |= apply_autocmds_group(event_nr, fname, NULL, true, au_group, b, NULL, data,
has_buf);
})
})
if (did_aucmd && modeline) {
if (did_aucmd && modeline && !has_buf) {
do_modelines(0);
}
}

View File

@@ -1160,7 +1160,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)) {
curbuf, NULL, NULL, false)) {
nothing_done = false;
}
}
@@ -1551,7 +1551,7 @@ static void deferred_event(void **argv)
aco_save_T aco = { 0 };
aucmd_prepbuf(&aco, buf);
apply_autocmds_group(event, fname, fname_io, false, group, buf, eap, data);
apply_autocmds_group(event, fname, fname_io, false, group, buf, eap, data, false);
aucmd_restbuf(&aco);
restore_v_event(v_event, &save_v_event);
@@ -1606,7 +1606,7 @@ 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);
return apply_autocmds_group(event, fname, fname_io, force, AUGROUP_ALL, buf, NULL, NULL, false);
}
/// Like apply_autocmds(), but with extra "eap" argument. This takes care of
@@ -1623,7 +1623,7 @@ 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);
return apply_autocmds_group(event, fname, fname_io, force, AUGROUP_ALL, buf, eap, NULL, false);
}
/// Like apply_autocmds(), but handles the caller's retval. If the script
@@ -1646,7 +1646,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);
bool did_cmd = apply_autocmds_group(event, fname, fname_io, force, AUGROUP_ALL, buf, NULL, NULL,
false);
if (did_cmd && aborting()) {
*retval = FAIL;
}
@@ -1691,10 +1692,11 @@ bool trigger_cursorhold(void) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
/// @param group autocmd group ID or AUGROUP_ALL
/// @param buf Buffer for <abuf>
/// @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)
buf_T *buf, exarg_T *eap, Object *data, bool with_buf)
{
char *sfname = NULL; // short file name
bool retval = false;
@@ -1706,6 +1708,9 @@ bool apply_autocmds_group(event_T event, char *fname, char *fname_io, bool force
save_redo_T save_redo;
const bool save_KeyTyped = KeyTyped;
ESTACK_CHECK_DECLARATION;
aco_save_T aco = { 0 };
bool save_changed = false;
buf_T *old_curbuf = NULL;
// Quickly return if there are no autocommands for this event or
// autocommands are blocked.
@@ -1775,9 +1780,6 @@ bool apply_autocmds_group(event_T event, char *fname, char *fname_io, bool force
char *save_autocmd_match = autocmd_match;
int save_autocmd_busy = autocmd_busy;
int save_autocmd_nested = autocmd_nested;
bool save_changed = curbuf->b_changed;
buf_T *old_curbuf = curbuf;
// Set the file name to be used for <afile>.
// Make a copy to avoid that changing a buffer name or directory makes it
// invalid.
@@ -1875,6 +1877,13 @@ bool apply_autocmds_group(event_T event, char *fname, char *fname_io, bool force
goto BYPASS_AU;
}
if (with_buf && buf != NULL && buf != curbuf) {
aucmd_prepbuf(&aco, buf);
}
save_changed = curbuf->b_changed;
old_curbuf = curbuf;
#ifdef BACKSLASH_IN_FILENAME
// Replace all backslashes with forward slashes. This makes the
// autocommand patterns portable between Unix and Windows.
@@ -2068,6 +2077,8 @@ BYPASS_AU:
curbuf->b_au_did_filetype = true;
}
aucmd_restbuf(&aco);
return retval;
}
@@ -2076,7 +2087,7 @@ void do_termresponse_autocmd(const String sequence)
MAXSIZE_TEMP_DICT(data, 1);
PUT_C(data, "sequence", STRING_OBJ(sequence));
apply_autocmds_group(EVENT_TERMRESPONSE, NULL, NULL, true, AUGROUP_ALL, NULL, NULL,
&DICT_OBJ(data));
&DICT_OBJ(data), false);
termresponse_changed = true;
}

View File

@@ -20,6 +20,7 @@
#include "nvim/api/private/defs.h"
#include "nvim/arabic.h"
#include "nvim/ascii_defs.h"
#include "nvim/autocmd.h"
#include "nvim/buffer_defs.h"
#include "nvim/decoration.h"
#include "nvim/globals.h"
@@ -951,8 +952,11 @@ void win_grid_alloc(win_T *wp)
if (want_allocation && (!has_allocation
|| grid_allocated->rows != total_rows
|| grid_allocated->cols != total_cols)) {
// aucmd_win is a transient host for autocmds against a hidden buffer;
// initialize its grid with valid attrs so a nested redraw from a callback
// doesn't composite through uninitialized cells.
grid_alloc(grid_allocated, total_rows, total_cols,
wp->w_grid_alloc.valid, false);
wp->w_grid_alloc.valid, is_aucmd_win(wp));
grid_allocated->valid = true;
if (wp->w_floating && wp->w_config.border) {
wp->w_redr_border = true;
@@ -965,7 +969,12 @@ void win_grid_alloc(win_T *wp)
grid_allocated->valid = false;
was_resized = true;
} else if (want_allocation && has_allocation && !wp->w_grid_alloc.valid) {
grid_invalidate(grid_allocated);
if (is_aucmd_win(wp)) {
size_t n = (size_t)grid_allocated->rows * (size_t)grid_allocated->cols;
memset(grid_allocated->attrs, 0, n * sizeof(*grid_allocated->attrs));
} else {
grid_invalidate(grid_allocated);
}
grid_allocated->valid = true;
}

View File

@@ -1180,7 +1180,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));
AUGROUP_ALL, NULL, NULL, &DICT_OBJ(data), false);
kv_destroy(messages);
}

View File

@@ -279,7 +279,7 @@ static void emit_termrequest(void **argv)
term->refcount++;
apply_autocmds_group(EVENT_TERMREQUEST, NULL, NULL, true, AUGROUP_ALL, buf, NULL,
&DICT_OBJ(data));
&DICT_OBJ(data), false);
term->refcount--;
xfree(sequence);
@@ -737,7 +737,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));
buf, NULL, &DICT_OBJ(data), false);
restore_v_event(dict, &save_v_event);
}

View File

@@ -1205,6 +1205,161 @@ describe('autocmd api', function()
eq(1, api.nvim_get_var('buffer_executed'))
end)
it('executes in target buf context for one visible window', function()
local caller = api.nvim_get_current_buf()
local caller_win = api.nvim_get_current_win()
command('split')
local target = api.nvim_create_buf(true, false)
api.nvim_set_current_buf(target)
local target_win = api.nvim_get_current_win()
api.nvim_set_current_win(caller_win)
api.nvim_create_autocmd('CursorHold', {
buffer = target,
command = 'let [g:abuf, g:cur, g:win] = [str2nr(expand("<abuf>")), bufnr("%"), win_getid()]',
})
api.nvim_exec_autocmds('CursorHold', { buf = target })
eq(target, api.nvim_get_var('abuf'))
eq(target, api.nvim_get_var('cur'))
eq(target_win, api.nvim_get_var('win'))
eq(caller, api.nvim_get_current_buf())
eq(caller_win, api.nvim_get_current_win())
end)
it('executes in target buf context for a hidden buffer', function()
local caller = api.nvim_get_current_buf()
local caller_win = api.nvim_get_current_win()
local target = api.nvim_create_buf(true, false)
api.nvim_create_autocmd('User', {
buffer = target,
command = 'let [g:abuf, g:cur] = [str2nr(expand("<abuf>")), bufnr("%")]',
})
api.nvim_exec_autocmds('User', { buf = target })
eq(target, api.nvim_get_var('abuf'))
eq(target, api.nvim_get_var('cur'))
eq(caller, api.nvim_get_current_buf())
eq(caller_win, api.nvim_get_current_win())
end)
it('ignores modelines when executing for a passed buffer', function()
local caller = api.nvim_get_current_buf()
local target = api.nvim_create_buf(true, false)
api.nvim_buf_set_lines(caller, 0, -1, false, { 'x', '/* vim: set textwidth=23: */' })
api.nvim_buf_set_lines(target, 0, -1, false, { 'x', '/* vim: set textwidth=17: */' })
api.nvim_set_option_value('textwidth', 0, { buf = caller })
api.nvim_set_option_value('textwidth', 0, { buf = target })
api.nvim_create_autocmd('User', { buffer = target, command = '' })
api.nvim_exec_autocmds('User', { buf = target, modeline = true })
eq(0, api.nvim_get_option_value('textwidth', { buf = caller }))
eq(0, api.nvim_get_option_value('textwidth', { buf = target }))
end)
it('restores caller if target buffer is wiped during execution', function()
local caller = api.nvim_get_current_buf()
local caller_win = api.nvim_get_current_win()
local target = api.nvim_create_buf(true, false)
api.nvim_create_autocmd('User', {
buffer = target,
command = 'let g:seen = bufnr("%") | execute "bwipeout!" expand("<abuf>")',
})
api.nvim_exec_autocmds('User', { buf = target })
eq(target, api.nvim_get_var('seen'))
eq(false, api.nvim_buf_is_valid(target))
eq(caller, api.nvim_get_current_buf())
eq(caller_win, api.nvim_get_current_win())
end)
it('restores caller if callback changes current buffer', function()
local caller = api.nvim_get_current_buf()
local caller_win = api.nvim_get_current_win()
local target = api.nvim_create_buf(true, false)
local other = api.nvim_create_buf(true, false)
command('split')
api.nvim_set_current_buf(target)
local target_win = api.nvim_get_current_win()
api.nvim_set_current_win(caller_win)
api.nvim_create_autocmd('User', {
buffer = target,
command = string.format(
'let g:seen = bufnr("%%") | buffer %d | let g:changed = bufnr("%%")',
other
),
})
api.nvim_exec_autocmds('User', { buf = target })
eq(target, api.nvim_get_var('seen'))
eq(other, api.nvim_get_var('changed'))
eq(caller, api.nvim_get_current_buf())
eq(caller_win, api.nvim_get_current_win())
eq(target, api.nvim_win_get_buf(target_win))
end)
it('restores nested target buffer contexts', function()
local caller = api.nvim_get_current_buf()
local caller_win = api.nvim_get_current_win()
local target = api.nvim_create_buf(true, false)
local other = api.nvim_create_buf(true, false)
api.nvim_create_autocmd('User', {
buffer = other,
command = 'let g:inner = bufnr("%")',
})
api.nvim_create_autocmd('User', {
buffer = target,
nested = true,
command = string.format(
'let g:outer = bufnr("%%")'
.. ' | call nvim_exec_autocmds("User", #{buf: %d})'
.. ' | let g:outer_after = bufnr("%%")',
other
),
})
api.nvim_exec_autocmds('User', { buf = target })
eq(target, api.nvim_get_var('outer'))
eq(other, api.nvim_get_var('inner'))
eq(target, api.nvim_get_var('outer_after'))
eq(caller, api.nvim_get_current_buf())
eq(caller_win, api.nvim_get_current_win())
end)
it('respects eventignorewin across multiple windows', function()
local caller_win = api.nvim_get_current_win()
local target = api.nvim_create_buf(false, false)
command('split')
local w_allows = api.nvim_get_current_win()
api.nvim_win_set_buf(w_allows, target)
command('vsplit')
local w_ignores = api.nvim_get_current_win()
api.nvim_win_set_buf(w_ignores, target)
api.nvim_set_current_win(caller_win)
api.nvim_create_autocmd('CursorHold', {
buffer = target,
command = 'let g:fired += 1',
})
api.nvim_set_option_value('eventignorewin', '', { win = w_allows })
api.nvim_set_option_value('eventignorewin', 'CursorHold', { win = w_ignores })
api.nvim_set_var('fired', 0)
api.nvim_exec_autocmds('CursorHold', { buf = target })
eq(1, api.nvim_get_var('fired'))
api.nvim_set_option_value('eventignorewin', 'CursorHold', { win = w_allows })
api.nvim_set_var('fired', 0)
api.nvim_exec_autocmds('CursorHold', { buf = target })
eq(0, api.nvim_get_var('fired'))
eq(caller_win, api.nvim_get_current_win())
end)
it('can pass the filename, pattern match', function()
api.nvim_set_var('filename_executed', 'none')
eq('none', api.nvim_get_var('filename_executed'))