diff --git a/runtime/doc/dev_arch.txt b/runtime/doc/dev_arch.txt index 68778dcdd9..8363e22d28 100644 --- a/runtime/doc/dev_arch.txt +++ b/runtime/doc/dev_arch.txt @@ -216,7 +216,7 @@ order of preference): a performance benefit: the Vimscript <=> Lua bridge is skipped when the `vim.fn.xx()` function is called from Lua. Examples: - `f_hostname` -2. Partial Lua: the C implementation calls `nlua_call_vimfn` or +2. Partial Lua: the C implementation calls `nlua_call_typval` or `nlua_call_excmd`. (Or in rare cases: `nlua_exec` / `NLUA_EXEC_STATIC`). Examples: - `ex_log` diff --git a/src/nvim/eval/funcs.c b/src/nvim/eval/funcs.c index e51c9b082a..d8bf95105c 100644 --- a/src/nvim/eval/funcs.c +++ b/src/nvim/eval/funcs.c @@ -4059,7 +4059,7 @@ static void f_luaeval(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) return; } - nlua_typval_eval(cstr_as_string(str), &argvars[1], rettv); + nlua_call_luaeval(cstr_as_string(str), &argvars[1], rettv); } static void find_some_match(typval_T *const argvars, typval_T *const rettv, @@ -6373,7 +6373,7 @@ static void f_serverlist(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) .vval.v_special = kSpecialVarNull }; typval_T lua_args[] = { opts, addrs_tv, { .v_type = VAR_UNKNOWN } }; - nlua_call_vimfn("vim._core.server", "serverlist", lua_args, rettv); + nlua_call_typval("vim._core.server", "serverlist", lua_args, rettv); tv_clear(&addrs_tv); } diff --git a/src/nvim/eval/userfunc.c b/src/nvim/eval/userfunc.c index c74e132820..5326f5e938 100644 --- a/src/nvim/eval/userfunc.c +++ b/src/nvim/eval/userfunc.c @@ -1407,7 +1407,7 @@ static int call_user_func_check(ufunc_T *fp, int argcount, typval_T *argvars, ty FUNC_ATTR_NONNULL_ARG(1, 3, 4, 5) { if (fp->uf_flags & FC_LUAREF) { - return typval_exec_lua_callable(fp->uf_luaref, argcount, argvars, rettv); + return nlua_exec_typval_callable(fp->uf_luaref, argcount, argvars, rettv); } if ((fp->uf_flags & FC_RANGE) && funcexe->fe_doesrange != NULL) { @@ -1741,7 +1741,7 @@ int call_func(const char *funcname, int len, typval_T *rettv, int argcount_in, t if (len > 0) { error = FCERR_NONE; argv_add_base(funcexe->fe_basetv, &argvars, &argcount, argv, &argv_base); - nlua_call_typval(funcname, (size_t)len, argvars, argcount, rettv); + nlua_call_vlua(funcname, (size_t)len, argvars, argcount, rettv); } else { // v:lua was called directly; show its name in the emsg XFREE_CLEAR(name); @@ -1827,7 +1827,7 @@ int call_simple_luafunc(const char *funcname, size_t len, typval_T *rettv) typval_T argvars[1]; argvars[0].v_type = VAR_UNKNOWN; - nlua_call_typval(funcname, len, argvars, 0, rettv); + nlua_call_vlua(funcname, len, argvars, 0, rettv); return OK; } diff --git a/src/nvim/ex_getln.c b/src/nvim/ex_getln.c index ccd0186ed0..04eb0ccbf5 100644 --- a/src/nvim/ex_getln.c +++ b/src/nvim/ex_getln.c @@ -4587,7 +4587,7 @@ static void cmdwin_invoke(const char *action, int firstc, char *content, int pos { .v_type = VAR_NUMBER, .vval.v_number = pos + 1 }, { .v_type = VAR_UNKNOWN }, }; - nlua_call_vimfn("vim._core.cmdwin", action, firstc ? tv_args : tv_args + 3, NULL); + nlua_call_typval("vim._core.cmdwin", action, firstc ? tv_args : tv_args + 3, NULL); xfree(content); } diff --git a/src/nvim/help.c b/src/nvim/help.c index e728fac88a..546591ad13 100644 --- a/src/nvim/help.c +++ b/src/nvim/help.c @@ -108,7 +108,7 @@ void ex_help(exarg_T *eap) if (helpbang) { typval_T no_args[] = { { .v_type = VAR_UNKNOWN } }; typval_T rettv; - nlua_call_vimfn("vim._core.help", "resolve_tag", no_args, &rettv); + nlua_call_typval("vim._core.help", "resolve_tag", no_args, &rettv); if (rettv.v_type == VAR_STRING && rettv.vval.v_string != NULL && *rettv.vval.v_string != NUL) { allocated_arg = rettv.vval.v_string; // takes ownership arg = allocated_arg; @@ -339,7 +339,7 @@ int find_help_tags(const char *arg, int *num_matches, char ***matches, bool keep { .v_type = VAR_UNKNOWN }, }; typval_T rettv; - nlua_call_vimfn("vim._core.help", "escape_subject", tv_args, &rettv); + nlua_call_typval("vim._core.help", "escape_subject", tv_args, &rettv); if (rettv.v_type != VAR_STRING || rettv.vval.v_string == NULL) { tv_clear(&rettv); return FAIL; @@ -461,7 +461,7 @@ void prepare_help_buffer(void) void get_local_additions(void) { typval_T no_args[] = { { .v_type = VAR_UNKNOWN } }; - nlua_call_vimfn("vim._core.help", "local_additions", no_args, NULL); + nlua_call_typval("vim._core.help", "local_additions", no_args, NULL); } /// ":exusage" diff --git a/src/nvim/lua/executor.c b/src/nvim/lua/executor.c index c9d40107c7..8a1487fe5a 100644 --- a/src/nvim/lua/executor.c +++ b/src/nvim/lua/executor.c @@ -1497,72 +1497,44 @@ LuaRef api_new_luaref(LuaRef original_ref) return new_ref; } -/// Evaluate lua string -/// -/// Used for luaeval(). -/// -/// @param[in] str String to execute. -/// @param[in] arg Second argument to `luaeval()`. -/// @param[out] ret_tv Location where result will be saved. -/// -/// @return Result of the execution. -void nlua_typval_eval(const String str, typval_T *const arg, typval_T *const ret_tv) - FUNC_ATTR_NONNULL_ALL +/// Formats `fmt` (must contain a single "%.*s" for `bodylen`-byte `body`) into a Lua chunk and runs +/// it via nlua_exec_typval(). +static void nlua_exec_typval_fmt(const char *fmt, const char *body, size_t bodylen, + const char *name, typval_T *args, int argcount, bool special, + typval_T *ret_tv) + FUNC_ATTR_PRINTF(1, 0) { -#define EVALHEADER "local _A=select(1,...) return (" - const size_t lcmd_len = sizeof(EVALHEADER) - 1 + str.size + 1; - char *lcmd; - if (lcmd_len < IOSIZE) { - lcmd = IObuff; - } else { - lcmd = xmalloc(lcmd_len); + // Don't use shared IObuff; may reenter Lua (e.g. 'statusline' eval during a redraw). #40616 + // This local buf is safe across (re)entrant calls since luaL_loadbuffer() copies before exec. + static char lcmd_buf[IOSIZE]; + char *lcmd = lcmd_buf; + int lcmd_len = vim_snprintf(lcmd_buf, sizeof(lcmd_buf), fmt, (int)bodylen, body); + if (lcmd_len >= IOSIZE) { + lcmd = xmalloc((size_t)lcmd_len + 1); + vim_snprintf(lcmd, (size_t)lcmd_len + 1, fmt, (int)bodylen, body); } - memcpy(lcmd, EVALHEADER, sizeof(EVALHEADER) - 1); - memcpy(lcmd + sizeof(EVALHEADER) - 1, str.data, str.size); - lcmd[lcmd_len - 1] = ')'; -#undef EVALHEADER - nlua_typval_exec(lcmd, lcmd_len, "luaeval()", arg, 1, true, ret_tv); - if (lcmd != IObuff) { + nlua_exec_typval(lcmd, (size_t)lcmd_len, name, args, argcount, special, ret_tv); + + if (lcmd != lcmd_buf) { xfree(lcmd); } } -/// Calls a Lua function by name with typval args. Used by v:lua.func(). -/// -/// Builds "return func(...)" and executes it via luaL_loadbuffer. -/// Converts args via nlua_push_typval, result via nlua_pop_typval. -/// -/// @param str Lua expression (function name), e.g. "my_func" or "mod.func". -/// @param len Length of str. -/// @param args typval_T arguments. -/// @param argcount Number of arguments. -/// @param ret_tv Return value (always set). -void nlua_call_typval(const char *str, size_t len, typval_T *const args, int argcount, - typval_T *ret_tv) +/// Implements vimfn: luaeval(). +void nlua_call_luaeval(const String str, typval_T *const arg, typval_T *const ret_tv) FUNC_ATTR_NONNULL_ALL { -#define CALLHEADER "return " -#define CALLSUFFIX "(...)" - const size_t lcmd_len = sizeof(CALLHEADER) - 1 + len + sizeof(CALLSUFFIX) - 1; - char *lcmd; - if (lcmd_len < IOSIZE) { - lcmd = IObuff; - } else { - lcmd = xmalloc(lcmd_len); - } - memcpy(lcmd, CALLHEADER, sizeof(CALLHEADER) - 1); - memcpy(lcmd + sizeof(CALLHEADER) - 1, str, len); - memcpy(lcmd + sizeof(CALLHEADER) - 1 + len, CALLSUFFIX, - sizeof(CALLSUFFIX) - 1); -#undef CALLHEADER -#undef CALLSUFFIX + nlua_exec_typval_fmt("local _A=select(1,...) return (%.*s)", str.data, str.size, "luaeval()", arg, + 1, true, ret_tv); +} - nlua_typval_exec(lcmd, lcmd_len, "v:lua", args, argcount, false, ret_tv); - - if (lcmd != IObuff) { - xfree(lcmd); - } +/// Calls v:lua.func() with typval args. +void nlua_call_vlua(const char *str, size_t len, typval_T *const args, int argcount, + typval_T *ret_tv) + FUNC_ATTR_NONNULL_ALL +{ + nlua_exec_typval_fmt("return %.*s(...)", str, len, "v:lua", args, argcount, false, ret_tv); } /// Calls a Lua completion function (stored as LuaRef) for cmdline expansion. @@ -1585,7 +1557,7 @@ void nlua_call_user_expand_func(expand_T *xp, typval_T *ret_tv) } /// Executes a Lua chunk with typval arguments and optional typval return. -static void nlua_typval_exec(const char *lcmd, size_t lcmd_len, const char *name, +static void nlua_exec_typval(const char *lcmd, size_t lcmd_len, const char *name, typval_T *const args, int argcount, bool special, typval_T *ret_tv) { if (check_secure()) { @@ -1626,20 +1598,18 @@ void nlua_exec_ga(garray_T *ga, char *name) { char *code = ga_concat_strings(ga, "\n"); size_t len = strlen(code); - nlua_typval_exec(code, len, name, NULL, 0, false, NULL); + nlua_exec_typval(code, len, name, NULL, 0, false, NULL); xfree(code); } -/// Call a LuaCallable given some typvals -/// -/// Used to call any Lua callable passed from Lua into Vimscript. +/// Calls a Lua callable from Vimscript. /// /// @param[in] lstate Lua State /// @param[in] lua_cb Lua Callable /// @param[in] argcount Count of typval arguments /// @param[in] argvars Typval Arguments /// @param[out] rettv The return value from the called function. -int typval_exec_lua_callable(LuaRef lua_cb, int argcount, typval_T *argvars, typval_T *rettv) +int nlua_exec_typval_callable(LuaRef lua_cb, int argcount, typval_T *argvars, typval_T *rettv) { lua_State *lstate = global_lstate; @@ -1697,7 +1667,7 @@ Object nlua_exec(const String str, const char *chunkname, const Array args, LuaR return nlua_call_pop_retval(lstate, mode, arena, top, err); } -/// Calls Lua to implement a "vimfn" ("f_xx"/"eval"/"builtin") function. +/// Calls a Lua function with typval args. /// /// Converts argvars directly to Lua values (no Object intermediate), calls the Lua function, and /// converts the result back to typval_T. @@ -1706,7 +1676,7 @@ Object nlua_exec(const String str, const char *chunkname, const Array args, LuaR /// @param func Function name in the module, e.g. "serverlist". /// @param argvars typval args (VAR_UNKNOWN-terminated). /// @param rettv Return value (caller must tv_clear), or NULL to discard. -void nlua_call_vimfn(const char *module, const char *func, typval_T *argvars, typval_T *rettv) +void nlua_call_typval(const char *module, const char *func, typval_T *argvars, typval_T *rettv) { int argcount = 0; for (typval_T *tv = argvars; tv->v_type != VAR_UNKNOWN; tv++) { @@ -1716,7 +1686,7 @@ void nlua_call_vimfn(const char *module, const char *func, typval_T *argvars, ty char buf[256]; snprintf(buf, sizeof(buf), "return require('%s').%s(...)", module, func); - nlua_typval_exec(buf, strlen(buf), module, argvars, argcount, false, rettv); + nlua_exec_typval(buf, strlen(buf), module, argvars, argcount, false, rettv); } /// Calls Lua to implement an excmd. Passes `eap` + `cmdmod` to Lua as a dict arg, which is arranged @@ -1885,8 +1855,6 @@ bool nlua_is_deferred_safe(void) return in_fast_callback == 0; } -/// Executes Lua code. -/// /// Implements `:lua` and `:lua ={expr}`. /// /// @param eap Vimscript `:lua {code}`, `:{range}lua`, or `:lua ={expr}` command. @@ -1916,21 +1884,19 @@ void ex_lua(exarg_T *const eap) if (eap->cmdidx == CMD_equal || code[0] == '=') { size_t off = (eap->cmdidx == CMD_equal) ? 0 : 1; len += sizeof("vim._print(true, )") - 1 - off; - // `nlua_typval_exec` doesn't expect NUL-terminated string so `len` must end before NUL byte. + // `nlua_exec_typval` doesn't expect NUL-terminated string so `len` must end before NUL byte. char *code_buf = xmallocz(len); vim_snprintf(code_buf, len + 1, "vim._print(true, %s)", code + off); xfree(code); code = code_buf; } - nlua_typval_exec(code, len, ":lua", NULL, 0, false, NULL); + nlua_exec_typval(code, len, ":lua", NULL, 0, false, NULL); xfree(code); } -/// Executes Lua code for-each line in a buffer range. -/// -/// Implements `:luado`. +/// Implements `:luado`: executes Lua code for-each line in a buffer range. /// /// @param eap Vimscript `:luado {code}` command. void ex_luado(exarg_T *const eap) @@ -1950,9 +1916,12 @@ void ex_luado(exarg_T *const eap) const size_t lcmd_len = (cmd_len + (sizeof(DOSTART) - 1) + (sizeof(DOEND) - 1)); + // Don't use shared IObuff; may reenter Lua (e.g. 'statusline' eval during a redraw). #40616 + // This local buf is safe across (re)entrant calls since luaL_loadbuffer() copies before exec. + static char lcmd_buf[IOSIZE]; char *lcmd; if (lcmd_len < IOSIZE) { - lcmd = IObuff; + lcmd = lcmd_buf; } else { lcmd = xmalloc(lcmd_len + 1); } @@ -1964,12 +1933,12 @@ void ex_luado(exarg_T *const eap) if (luaL_loadbuffer(lstate, lcmd, lcmd_len, ":luado")) { nlua_error(lstate, _("E5109: Lua: %.*s")); - if (lcmd_len >= IOSIZE) { + if (lcmd != lcmd_buf) { xfree(lcmd); } return; } - if (lcmd_len >= IOSIZE) { + if (lcmd != lcmd_buf) { xfree(lcmd); } if (nlua_pcall(lstate, 0, 1)) { @@ -2022,9 +1991,7 @@ void ex_luado(exarg_T *const eap) redraw_curbuf_later(UPD_NOT_VALID); } -/// Executes Lua code from a file location. -/// -/// Implements `:luafile`. +/// Implements `:luafile`: executes Lua code from a file. /// /// @param eap Vimscript `:luafile {file}` command. void ex_luafile(exarg_T *const eap) diff --git a/src/nvim/main.c b/src/nvim/main.c index 39f986d86c..ae67da64d6 100644 --- a/src/nvim/main.c +++ b/src/nvim/main.c @@ -503,7 +503,7 @@ int main(int argc, char **argv) tv_list_alloc_ret(&items_tv, 0); recover_names(NULL, false, items_tv.vval.v_list); typval_T lua_args[] = { items_tv, { .v_type = VAR_UNKNOWN } }; - nlua_call_vimfn("vim._core.swapfile", "list_swaps", lua_args, NULL); + nlua_call_typval("vim._core.swapfile", "list_swaps", lua_args, NULL); tv_clear(&items_tv); os_exit(0); } diff --git a/src/nvim/memline.c b/src/nvim/memline.c index 84d9d23323..28f8e4565c 100644 --- a/src/nvim/memline.c +++ b/src/nvim/memline.c @@ -803,7 +803,7 @@ void ml_recover(bool checkext) if (n_swaps > 1) { // Several swapfiles found: prompt (async) via vim.ui.select(). typval_T lua_args[] = { items_tv, { .v_type = VAR_UNKNOWN } }; - nlua_call_vimfn("vim._core.swapfile", "select_swap", lua_args, NULL); + nlua_call_typval("vim._core.swapfile", "select_swap", lua_args, NULL); tv_clear(&items_tv); goto theend; } diff --git a/src/nvim/normal.c b/src/nvim/normal.c index 1e21114970..9ba80c88a8 100644 --- a/src/nvim/normal.c +++ b/src/nvim/normal.c @@ -6419,7 +6419,7 @@ static void nv_record(cmdarg_T *cap) { .v_type = VAR_STRING, .vval.v_string = fc }, { .v_type = VAR_UNKNOWN }, }; - nlua_call_vimfn("vim._core.cmdwin", "open", tv_args, NULL); + nlua_call_typval("vim._core.cmdwin", "open", tv_args, NULL); } else { // (stop) recording into a named register, unless executing a // register. diff --git a/src/nvim/spellsuggest.c b/src/nvim/spellsuggest.c index 7cf2e05810..34dd085891 100644 --- a/src/nvim/spellsuggest.c +++ b/src/nvim/spellsuggest.c @@ -479,7 +479,7 @@ static void select_spell_suggestion(suginfo_T *sug) typval_T bad_tv = { .v_type = VAR_STRING, .vval.v_string = xstrnsave(sug->su_badptr, (size_t)sug->su_badlen) }; typval_T lua_args[] = { items_tv, bad_tv, { .v_type = VAR_UNKNOWN } }; - nlua_call_vimfn("vim._core.spell", "select_suggest", lua_args, NULL); + nlua_call_typval("vim._core.spell", "select_suggest", lua_args, NULL); tv_clear(&items_tv); tv_clear(&bad_tv); diff --git a/test/functional/ui/messages_spec.lua b/test/functional/ui/messages_spec.lua index e79decbc3f..89062b9291 100644 --- a/test/functional/ui/messages_spec.lua +++ b/test/functional/ui/messages_spec.lua @@ -5,7 +5,6 @@ local Screen = require('test.functional.ui.screen') local clear, feed = n.clear, n.feed local eval = n.eval local eq = t.eq -local pcall_err = t.pcall_err local neq = t.neq local command = n.command local set_method_error = n.set_method_error @@ -1751,6 +1750,30 @@ describe('ui/builtin messages', function() eq(busy_start, busy_stop) -- balanced: cursor restored afterwards end) + it(':write message not clobbered by v:lua in statusline redraw #40616', function() + local fname = 'Xtest_write_progress' + finally(function() + os.remove(fname) + end) + exec_lua(function() + -- The Progress event fired by the ":write" message (since ff68fd6b8a84) + -- evaluates statusline mid-message. + _G.Statusline = function() + return 'STL' + end + vim.o.laststatus = 2 + vim.o.statusline = '%{v:lua.Statusline()}' + vim.api.nvim_create_autocmd('Progress', { + callback = function() + vim.cmd('redrawstatus!') + end, + }) + end) + command('write ' .. fname) + -- Should not be overwritten by "return Statusline(...)"). + screen:expect({ any = ('"%s".*written'):format(fname) }) + end) + it(':hi Group output', function() screen:try_resize(70, 7) feed(':hi ErrorMsg')