feat(api): mark nvim_create_autocmd as api-fast #40836

Problem:
nvim_create_autocmd() isn't |api-fast|, so modules that create autocmds
(e.g. vim.treesitter.query) can't be require()d in a fast event context.

Solution:
Mark it |api-fast|. Compile autocmd patterns with RE_NOBREAK so
aucmd_next() won't os_breakcheck() mid-iteration, where a fast
nvim_create_autocmd() could realloc the autocmds vector and dangle the
caller's AutoPat/AutoCmd.

RE_NOBREAK is low-risk because:
- aucmd_next()'s loop checks CTRL-C: `(for (… i < apc->ausize && !got_int; …)`.
- `line_breakcheck()` (`autocmd.c:1912`) runs once per matched autocmd.
- Each autocmd _execution_ runs through `do_cmdline`, which has its own
  breakchecks.

However this does admit risk of a pathological case:
a catastrophic-backtracking glob matched against a very long `User`
event-pattern.

Co-authored-by: Riley Bruins <ribru17@hotmail.com>
Co-authored-by: zeertzjq <zeertzjq@outlook.com>
This commit is contained in:
Justin M. Keyes
2026-07-19 11:38:14 -04:00
committed by GitHub
parent 746f5682a7
commit d1b3a9924d
5 changed files with 20 additions and 4 deletions

View File

@@ -2159,6 +2159,7 @@ nvim_create_autocmd({event}, {opts}) *nvim_create_autocmd()*
<
Attributes: ~
|api-fast|
Since: 0.7.0
Parameters: ~

View File

@@ -179,6 +179,8 @@ API
• |nvim_set_option_value()| accepts a new `operation` field to modify the
existing option value.
• |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).
BUILD

View File

@@ -391,6 +391,7 @@ cleanup:
Integer nvim_create_autocmd(uint64_t channel_id, Object event, Dict(create_autocmd) *opts,
Arena *arena, Error *err)
FUNC_API_SINCE(9)
FUNC_API_FAST
{
int64_t autocmd_id = -1;
char *desc = NULL;

View File

@@ -1022,7 +1022,8 @@ int autocmd_register(int64_t id, event_T event, const char *pat, int patlen, int
ap->buflocal_nr = 0;
char *reg_pat = file_pat_to_reg_pat(pat, pat + patlen, &ap->allow_dirs, true);
if (reg_pat != NULL) {
ap->reg_prog = vim_regcomp(reg_pat, RE_MAGIC);
// RE_NOBREAK: autocmd-pattern matching (aucmd_next()) must not os_breakcheck(). #35071
ap->reg_prog = vim_regcomp(reg_pat, RE_MAGIC | RE_NOBREAK);
}
xfree(reg_pat);
if (reg_pat == NULL || ap->reg_prog == NULL) {

View File

@@ -24,7 +24,11 @@ describe('|api-fast| functions', function()
keycode = vim.keycode('<C-a>'),
keytrans = vim.fn.keytrans(vim.keycode('<C-Home>')),
nr2char = vim.fn.nr2char(65),
replace_termcodes = vim.api.nvim_replace_termcodes('<Esc>', true, true, true),
nvim_create_autocmd = vim.api.nvim_create_autocmd('User', {
pattern = 'Fast',
callback = function() end,
}) > 0,
nvim_replace_termcodes = vim.api.nvim_replace_termcodes('<Esc>', true, true, true),
str2list = vim.fn.str2list('AB'),
strcharlen = vim.fn.strcharlen('abc'),
strcharpart = vim.fn.strcharpart('héllo', 1, 2),
@@ -53,7 +57,8 @@ describe('|api-fast| functions', function()
keycode = '\1', -- <C-a>
keytrans = '<C-Home>',
nr2char = 'A',
replace_termcodes = '\27', -- <Esc>
nvim_create_autocmd = true,
nvim_replace_termcodes = '\27', -- <Esc>
str2list = { 65, 66 },
strcharlen = 3,
strcharpart = 'él',
@@ -87,6 +92,9 @@ describe('|api-fast| functions', function()
-- for `veryfast_breakcheck`). Small input could pass (false negative).
local big = ('héllo wörld '):rep(100000) -- 1.2M chars.
local aupat = ('a'):rep(80000) -- Must stay under the "E339: Pattern too long" limit.
vim.api.nvim_create_autocmd('User', { pattern = aupat, callback = function() end })
local cases = {
byteidx = function()
vim.fn.byteidx(big, 500000)
@@ -106,7 +114,10 @@ describe('|api-fast| functions', function()
nr2char = function()
vim.fn.nr2char(65)
end,
replace_termcodes = function()
nvim_create_autocmd = function()
vim.api.nvim_exec_autocmds('User', { pattern = aupat })
end,
nvim_replace_termcodes = function()
vim.api.nvim_replace_termcodes('<Esc>', true, true, true)
end,
str2list = function()