From e02755cd9ff29277d14421c9df627e5dc48e4f67 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Sat, 1 Aug 2026 10:07:26 -0400 Subject: [PATCH] fix(options): ":set foo?" for Lua callbacks #41100 Problem: `:set foo=` and `:set foo?` for func/expr options set to a Lua function, always displays "v:lua". The existing "" form was avoided because the ref id N changes on every read of the same option. Solution: - Don't attempt to tab-complete Lua functions. - Show "" (or "" if source file is unknown). Example: :set operatorfunc? operatorfunc= --- src/nvim/eval/typval.c | 2 +- src/nvim/eval/vars.c | 2 +- src/nvim/lua/executor.c | 7 ++-- src/nvim/mapping.c | 4 +-- src/nvim/msgpack_rpc/packer.c | 2 +- src/nvim/option.c | 41 +++++++++++++++--------- src/nvim/usercmd.c | 2 +- test/functional/options/options_spec.lua | 16 ++++++++- 8 files changed, 50 insertions(+), 26 deletions(-) diff --git a/src/nvim/eval/typval.c b/src/nvim/eval/typval.c index 15b07e5ed9..9ceb0514b6 100644 --- a/src/nvim/eval/typval.c +++ b/src/nvim/eval/typval.c @@ -1898,7 +1898,7 @@ void callback_copy(Callback *dest, Callback *src) char *callback_to_string(Callback *cb, Arena *arena) { if (cb->type == kCallbackLua) { - return nlua_funcref_str(cb->data.luaref, arena); + return nlua_funcref_str(cb->data.luaref, arena, true); } const size_t msglen = 100; diff --git a/src/nvim/eval/vars.c b/src/nvim/eval/vars.c index c913d79f99..fc993150a5 100644 --- a/src/nvim/eval/vars.c +++ b/src/nvim/eval/vars.c @@ -3303,7 +3303,7 @@ typval_T opt_to_tv(Object value, bool numbool) case kObjectTypeLuaRef: // Lua callback option (e.g. 'operatorfunc'): show a human-readable hint (same as `:map` does). rettv.v_type = VAR_STRING; - rettv.vval.v_string = nlua_funcref_str(value.data.luaref, NULL); + rettv.vval.v_string = nlua_funcref_str(value.data.luaref, NULL, true); break; default: abort(); // Should never happen. diff --git a/src/nvim/lua/executor.c b/src/nvim/lua/executor.c index e05da57b3c..bffa924d8f 100644 --- a/src/nvim/lua/executor.c +++ b/src/nvim/lua/executor.c @@ -2453,7 +2453,7 @@ int nlua_do_ucmd(ucmd_T *cmd, exarg_T *eap, bool preview) /// String representation of a Lua function reference /// /// @return Allocated string -char *nlua_funcref_str(LuaRef ref, Arena *arena) +char *nlua_funcref_str(LuaRef ref, Arena *arena, bool show_ref) { lua_State *const lstate = global_lstate; @@ -2469,13 +2469,14 @@ char *nlua_funcref_str(LuaRef ref, Arena *arena) lua_Debug ar; if (lua_getinfo(lstate, ">S", &ar) && *ar.source == '@' && ar.linedefined >= 0) { char *src = home_replace_save(NULL, ar.source + 1); - String str = arena_printf(arena, "", ref, src, ar.linedefined); + String str = show_ref ? arena_printf(arena, "", ref, src, ar.linedefined) + : arena_printf(arena, "", src, ar.linedefined); xfree(src); return str.data; } plain: {} - return arena_printf(arena, "", ref).data; + return show_ref ? arena_printf(arena, "", ref).data : arena_printf(arena, "").data; } /// Execute the vim._core.defaults module to set up default mappings and autocommands diff --git a/src/nvim/mapping.c b/src/nvim/mapping.c index c62d30efc5..b034c0be46 100644 --- a/src/nvim/mapping.c +++ b/src/nvim/mapping.c @@ -255,7 +255,7 @@ static void showmap(mapblock_T *mp, bool local) // Use false below if we only want things like to show up as such on // the rhs, and not M-x etc, true gets both -- webb if (mp->m_luaref != LUA_NOREF) { - char *str = nlua_funcref_str(mp->m_luaref, NULL); + char *str = nlua_funcref_str(mp->m_luaref, NULL, true); msg_puts_hl(str, HLF_8, false); xfree(str); } else if (mp->m_str[0] == NUL) { @@ -2206,7 +2206,7 @@ static void get_maparg(typval_T *argvars, typval_T *rettv, int exact) rettv->vval.v_string = str2special_save(rhs, false, false); } } else if (rhs_lua != LUA_NOREF) { - rettv->vval.v_string = nlua_funcref_str(mp->m_luaref, NULL); + rettv->vval.v_string = nlua_funcref_str(mp->m_luaref, NULL, true); } } else { // Return a dictionary. diff --git a/src/nvim/msgpack_rpc/packer.c b/src/nvim/msgpack_rpc/packer.c index 2c3aa6b2cf..c837324c74 100644 --- a/src/nvim/msgpack_rpc/packer.c +++ b/src/nvim/msgpack_rpc/packer.c @@ -212,7 +212,7 @@ void mpack_object_inner(Object *current, Object *container, size_t container_idx // see nlua_pop_Object. // Human-readable hint string (`:map`-style "" string). - char *repr = nlua_funcref_str(current->data.luaref, NULL); + char *repr = nlua_funcref_str(current->data.luaref, NULL, true); api_free_luaref(current->data.luaref); current->data.luaref = LUA_NOREF; mpack_str(cstr_as_string(repr), packer); diff --git a/src/nvim/option.c b/src/nvim/option.c index 19ed457e16..5e549e1236 100644 --- a/src/nvim/option.c +++ b/src/nvim/option.c @@ -3448,8 +3448,8 @@ static ObjectType optval_type(Object o) } } -/// True for a "callback option" (varp stores `Callback`): func option (kOptFlagFunc, e.g. -/// 'operatorfunc') or expr option (kOptFlagExpr, e.g. 'foldexpr'). +/// True for a "callback option" (varp stores `Callback`): func option (e.g. 'operatorfunc') or expr +/// option (e.g. 'foldexpr'). static bool is_callback_option(OptIndex opt_idx) { return (options[opt_idx].flags & (kOptFlagFunc | kOptFlagExpr)) != 0; @@ -3472,7 +3472,7 @@ Object optval_own(OptIndex opt_idx, Object value) static Object optval_snapshot(Object value) { if (value.type == kObjectTypeLuaRef) { - return CSTR_AS_OBJ(nlua_funcref_str(value.data.luaref, NULL)); + return CSTR_AS_OBJ(nlua_funcref_str(value.data.luaref, NULL, true)); } return copy_object(value, NULL); } @@ -3486,14 +3486,14 @@ static Object optval_snapshot_varp(OptIndex opt_idx, void *varp) return snapshot; } -/// Converts a callback option's stored Callback to an option value Object. -/// Always OWNED (a copied name/expr string, or a fresh LuaRef): free with optval_free(). +/// Converts a callback option's stored Callback to an optval Object. +/// Always OWNED (a copied name/expr string, or a new LuaRef): free with optval_free(). /// -/// - Lua callback => `LuaRef` -/// - named funcref => name -/// - partial => name -/// - expr option => expression string -/// - an unset callback => empty string +/// - Lua callback => `LuaRef` +/// - named funcref => name +/// - partial => name +/// - expr option => expression string +/// - unset callback => empty string static Object opt_from_callback(Callback *cb) { switch (cb->type) { @@ -3520,10 +3520,10 @@ static Object opt_from_callback(Callback *cb) /// Builds a callback option's Callback from an option value Object. Borrows `value` (copies its /// string/ref; the caller retains ownership). /// -/// - `LuaRef` ("func" or "expr" option) => Lua callback -/// - func option with `is_expr=false` => string parsed as a function name or lambda -/// - expr option with non-empty string => expression string (``/`s:` resolves to ``) -/// - anything else (empty string, `Unset`) => `CALLBACK_NONE` +/// - `LuaRef` ("func"/"expr" options) => Lua callback +/// - func option with `is_expr=false` => string parsed as a function name or lambda +/// - expr option with non-empty string => expression string (``/`s:` resolves to ``) +/// - other (empty string, `Unset`) => `CALLBACK_NONE` static Callback opt_to_callback(Object value, bool is_expr) { Callback cb = CALLBACK_NONE; @@ -3653,7 +3653,7 @@ static char *optval_to_cstr(Object o, bool quote) return buf; } case kObjectTypeLuaRef: - return xstrdup("v:lua"); // A Lua callback has no name; show a stable handle. + return nlua_funcref_str(o.data.luaref, NULL, false); // show_ref=false: ref changes every time. default: abort(); // Should not happen. } @@ -6467,7 +6467,7 @@ int ExpandOldSetting(int *numMatches, char ***matches) char *var = NULL; *numMatches = 0; - *matches = xmalloc(sizeof(char *)); + *matches = NULL; // For a terminal key code expand_option_idx is kOptInvalid. if (expand_option_idx == kOptInvalid) { @@ -6475,6 +6475,14 @@ int ExpandOldSetting(int *numMatches, char ***matches) } if (expand_option_idx != kOptInvalid) { + Object o = opt_from_varp(expand_option_idx, + get_varp_scope(&options[expand_option_idx], expand_option_flags)); + bool is_luafn = o.type == kObjectTypeLuaRef; + optval_free_read(expand_option_idx, o); + if (is_luafn) { + // Special case: ":set x=" cannot meaningfully complete a Lua function. + return OK; + } // Put string of option value in NameBuff. optval_fmt(expand_option_idx, expand_option_flags); var = NameBuff; @@ -6484,6 +6492,7 @@ int ExpandOldSetting(int *numMatches, char ***matches) char *buf = escape_option_str_cmdline(var); + *matches = xmalloc(sizeof(char *)); (*matches)[0] = buf; *numMatches = 1; return OK; diff --git a/src/nvim/usercmd.c b/src/nvim/usercmd.c index 43fcaed5b0..5af0bdccc6 100644 --- a/src/nvim/usercmd.c +++ b/src/nvim/usercmd.c @@ -613,7 +613,7 @@ static void uc_list(char *name, size_t name_len) msg_outtrans(IObuff, 0, false); if (cmd->uc_luaref != LUA_NOREF) { - char *fn = nlua_funcref_str(cmd->uc_luaref, NULL); + char *fn = nlua_funcref_str(cmd->uc_luaref, NULL, true); msg_puts_hl(fn, HLF_8, false); xfree(fn); // put the description on a new line diff --git a/test/functional/options/options_spec.lua b/test/functional/options/options_spec.lua index 09cec55672..86bbca1f3f 100644 --- a/test/functional/options/options_spec.lua +++ b/test/functional/options/options_spec.lua @@ -218,7 +218,7 @@ describe('callback (func/expr) options', function() eq('GoodFunc', eval('&operatorfunc')) end) - it('with ":set all"', function() + it(':set', function() command('set operatorfunc=SomeFunc') command('set quickfixtextfunc={\\ d\\ ->\\ []\\ }') exec_lua(function() @@ -227,6 +227,20 @@ describe('callback (func/expr) options', function() end) command('set all') n.assert_alive() + + -- ":set opt?" shows funcref/lambda name, or "". + local function set_q(opt) + return n.api.nvim_exec2(('set %s?'):format(opt), { output = true }).output + end + eq(' operatorfunc=SomeFunc', set_q('operatorfunc')) + matches("^ quickfixtextfunc=function%('%d+'%)$", set_q('quickfixtextfunc')) + matches('^ omnifunc=$', set_q('omnifunc')) + matches('^ foldexpr=$', set_q('foldexpr')) + eq(set_q('omnifunc'), set_q('omnifunc')) -- Stable: no per-read ref id. + + -- ":set opt=" completes a funcref value, but not a Lua function. + eq({ 'SomeFunc' }, n.fn.getcompletion('set operatorfunc=', 'cmdline')) + eq({}, n.fn.getcompletion('set omnifunc=', 'cmdline')) end) it("expr option ('foldexpr') accepts string, reads back string", function()