fix(options): ":set foo?" for Lua callbacks #41100

Problem:
`:set foo=<tab>` and `:set foo?` for func/expr options set to a Lua
function, always displays "v:lua".

The existing "<Lua N: file:line>" 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 "<Lua file:line>" (or "<Lua>" if source file is unknown).

Example:

    :set operatorfunc?
      operatorfunc=<Lua ~/.config/nvim/init.lua:42>
This commit is contained in:
Justin M. Keyes
2026-08-01 10:07:26 -04:00
committed by GitHub
parent b11f1b1f76
commit e02755cd9f
8 changed files with 50 additions and 26 deletions

View File

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

View File

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

View File

@@ -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, "<Lua %d: %s:%d>", ref, src, ar.linedefined);
String str = show_ref ? arena_printf(arena, "<Lua %d: %s:%d>", ref, src, ar.linedefined)
: arena_printf(arena, "<Lua %s:%d>", src, ar.linedefined);
xfree(src);
return str.data;
}
plain: {}
return arena_printf(arena, "<Lua %d>", ref).data;
return show_ref ? arena_printf(arena, "<Lua %d>", ref).data : arena_printf(arena, "<Lua>").data;
}
/// Execute the vim._core.defaults module to set up default mappings and autocommands

View File

@@ -255,7 +255,7 @@ static void showmap(mapblock_T *mp, bool local)
// Use false below if we only want things like <Up> 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.

View File

@@ -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 "<Lua …>" 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);

View File

@@ -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 (`<SID>`/`s:` resolves to `<SNR>`)
/// - 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 (`<SID>`/`s:` resolves to `<SNR>`)
/// - 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=<tab>" 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;

View File

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

View File

@@ -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 "<Lua …>".
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%('<lambda>%d+'%)$", set_q('quickfixtextfunc'))
matches('^ omnifunc=<Lua .+:%d+>$', set_q('omnifunc'))
matches('^ foldexpr=<Lua .+:%d+>$', set_q('foldexpr'))
eq(set_q('omnifunc'), set_q('omnifunc')) -- Stable: no per-read ref id.
-- ":set opt=<Tab>" 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()