diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt index 34df0953f6..b647a9aabf 100644 --- a/runtime/doc/api.txt +++ b/runtime/doc/api.txt @@ -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 . + after the autocommands . Ignored if `buf` is + given. • pattern (`string|array?`, default: current file name) |autocmd-pattern|. Not allowed with {buf}. diff --git a/runtime/lua/vim/_meta/api.gen.lua b/runtime/lua/vim/_meta/api.gen.lua index 50ba118b6f..f463bf7928 100644 --- a/runtime/lua/vim/_meta/api.gen.lua +++ b/runtime/lua/vim/_meta/api.gen.lua @@ -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 ---- []. +--- []. 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 diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua index 8f0457a8ed..c12731fb3c 100644 --- a/runtime/lua/vim/treesitter/highlighter.lua +++ b/runtime/lua/vim/treesitter/highlighter.lua @@ -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 diff --git a/src/nvim/api/autocmd.c b/src/nvim/api/autocmd.c index 644c52f586..79dc839981 100644 --- a/src/nvim/api/autocmd.c +++ b/src/nvim/api/autocmd.c @@ -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 -/// []. +/// []. 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); } } diff --git a/src/nvim/autocmd.c b/src/nvim/autocmd.c index f7d19c5a28..fee7b4e96f 100644 --- a/src/nvim/autocmd.c +++ b/src/nvim/autocmd.c @@ -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 /// @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 . // 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; } diff --git a/src/nvim/grid.c b/src/nvim/grid.c index 3b9868265d..35d4bc1363 100644 --- a/src/nvim/grid.c +++ b/src/nvim/grid.c @@ -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; } diff --git a/src/nvim/message.c b/src/nvim/message.c index 984abf4b87..9a5c939998 100644 --- a/src/nvim/message.c +++ b/src/nvim/message.c @@ -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); } diff --git a/src/nvim/terminal.c b/src/nvim/terminal.c index ac97de8edc..76decc0900 100644 --- a/src/nvim/terminal.c +++ b/src/nvim/terminal.c @@ -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); } diff --git a/test/functional/api/autocmd_spec.lua b/test/functional/api/autocmd_spec.lua index f34facb71e..fed4488e5f 100644 --- a/test/functional/api/autocmd_spec.lua +++ b/test/functional/api/autocmd_spec.lua @@ -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("")), 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("")), 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("")', + }) + 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'))