Merge pull #40619 fix(lua): statusline expr leaks into msg area

This commit is contained in:
Justin M. Keyes
2026-07-06 17:21:03 -04:00
committed by GitHub
11 changed files with 83 additions and 93 deletions

View File

@@ -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`

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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"

View File

@@ -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)

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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.

View File

@@ -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);

View File

@@ -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<cr>')