From d1b3a9924d6d0e77cdd1196efbbf2b57e9946ea0 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Sun, 19 Jul 2026 11:38:14 -0400 Subject: [PATCH] feat(api): mark nvim_create_autocmd as api-fast #40836 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Co-authored-by: zeertzjq --- runtime/doc/api.txt | 1 + runtime/doc/news.txt | 2 ++ src/nvim/api/autocmd.c | 1 + src/nvim/autocmd.c | 3 ++- test/functional/api/fast_spec.lua | 17 ++++++++++++++--- 5 files changed, 20 insertions(+), 4 deletions(-) diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt index 45b82eb733..753d2c1823 100644 --- a/runtime/doc/api.txt +++ b/runtime/doc/api.txt @@ -2159,6 +2159,7 @@ nvim_create_autocmd({event}, {opts}) *nvim_create_autocmd()* < Attributes: ~ + |api-fast| Since: 0.7.0 Parameters: ~ diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 4e5ba1b508..ab2b6f27e4 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -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 diff --git a/src/nvim/api/autocmd.c b/src/nvim/api/autocmd.c index cc04a91c14..f193ae8594 100644 --- a/src/nvim/api/autocmd.c +++ b/src/nvim/api/autocmd.c @@ -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; diff --git a/src/nvim/autocmd.c b/src/nvim/autocmd.c index 1bfcb681da..2faa727147 100644 --- a/src/nvim/autocmd.c +++ b/src/nvim/autocmd.c @@ -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) { diff --git a/test/functional/api/fast_spec.lua b/test/functional/api/fast_spec.lua index 507fbc4c2c..d8f4ed3dfa 100644 --- a/test/functional/api/fast_spec.lua +++ b/test/functional/api/fast_spec.lua @@ -24,7 +24,11 @@ describe('|api-fast| functions', function() keycode = vim.keycode(''), keytrans = vim.fn.keytrans(vim.keycode('')), nr2char = vim.fn.nr2char(65), - replace_termcodes = vim.api.nvim_replace_termcodes('', 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('', 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', -- keytrans = '', nr2char = 'A', - replace_termcodes = '\27', -- + nvim_create_autocmd = true, + nvim_replace_termcodes = '\27', -- 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('', true, true, true) end, str2list = function()