mirror of
https://github.com/neovim/neovim.git
synced 2026-06-16 00:31:16 +00:00
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:
@@ -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}.
|
||||
|
||||
|
||||
4
runtime/lua/vim/_meta/api.gen.lua
generated
4
runtime/lua/vim/_meta/api.gen.lua
generated
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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'))
|
||||
|
||||
Reference in New Issue
Block a user