mirror of
https://github.com/neovim/neovim.git
synced 2026-09-17 19:24:55 +00:00
perf(vim.fn): call Lua-implemented vim.fn.xx() directly #39166
Problem:
- Builtin "Vimscript" functions (f_xx) are mostly implemented in C.
Partly that's because there is some boilerplate required to call out
to Lua.
- Calls to `vim.fn.foo()` always marshall over the Lua <=> Vimscript
("typval") bridge, even if `fn.foo()` is implemented entirely in Lua:
```
Lua => typval => Object => Lua => Object => typval => Lua.
```
Solution:
Functions declared in eval.lua with `func_lua` are implemented in
entirely in Lua (`_core/vimfn.lua`).
- `gen_eval.lua` wires `func_lua` entries to `lua_wrapper`, which handles
the typval conversion for Vimscript callers (slow path).
- `nlua_call()` detects `func_lua` functions and calls the Lua
implementation directly. This eliminates all conversion overhead for
Lua callers (fast path).
- Validate at build-time that `func`, `func_float`, and `func_lua` are
mutually exclusive.
- Migrate `hostname()` as a toy example, to show the idea.
This commit is contained in:
@@ -112,6 +112,12 @@ Many of the editor concepts are defined as Lua data files:
|
||||
- Ex (cmdline) commands: src/nvim/ex_cmds.lua
|
||||
- Options: src/nvim/options.lua
|
||||
- Vimscript functions: src/nvim/eval.lua
|
||||
- Functions can be implemented in C (`f_foo` in eval/funcs.c), or in Lua
|
||||
(set `func_lua` in eval.lua, implement in _core/vimfn.lua). Lua impl is
|
||||
preferred, for performance and maintainability.
|
||||
- VimScript callers go through `lua_wrapper` (typval <=> Object conversion).
|
||||
- Lua callers (`vim.fn.foo()`) take a fast path in `nlua_call()` that calls
|
||||
the Lua function directly, avoiding any conversion.
|
||||
- v: variables: src/nvim/vvars.lua
|
||||
|
||||
==============================================================================
|
||||
|
||||
@@ -158,7 +158,10 @@ OPTIONS
|
||||
|
||||
PERFORMANCE
|
||||
|
||||
• todo
|
||||
• Nvim architecture allows pure-Lua implementations of some `vim.fn`
|
||||
functions, which skips the Vimscript <=> Lua "bridge" (no data
|
||||
conversion/marshalling) entirely, if the `vim.fn` function is called from
|
||||
Lua.
|
||||
|
||||
PLUGINS
|
||||
|
||||
|
||||
@@ -4943,8 +4943,7 @@ hlexists({name}) *hlexists()*
|
||||
|
||||
hostname() *hostname()*
|
||||
Returns the hostname of the machine on which the Nvim server
|
||||
(not the UI client) is currently running. Names greater than
|
||||
256 characters long are truncated.
|
||||
(not the UI client) is currently running.
|
||||
|
||||
Return: ~
|
||||
(`string`)
|
||||
|
||||
14
runtime/lua/vim/_core/vimfn.lua
Normal file
14
runtime/lua/vim/_core/vimfn.lua
Normal file
@@ -0,0 +1,14 @@
|
||||
-- Lua implementations of "vimfn" builtin functions (via `func_lua`).
|
||||
--
|
||||
-- Functions defined here are pure Lua, they don't have any explicit C impl, so they are named with
|
||||
-- the "f_xx" convention, for discoverability.
|
||||
|
||||
local M = {}
|
||||
|
||||
--- Returns the hostname of the machine.
|
||||
--- @return string
|
||||
function M.f_hostname()
|
||||
return vim.uv.os_gethostname()
|
||||
end
|
||||
|
||||
return M
|
||||
3
runtime/lua/vim/_meta/vimfn.gen.lua
generated
3
runtime/lua/vim/_meta/vimfn.gen.lua
generated
@@ -4475,8 +4475,7 @@ function vim.fn.hlID(name) end
|
||||
function vim.fn.hlexists(name) end
|
||||
|
||||
--- Returns the hostname of the machine on which the Nvim server
|
||||
--- (not the UI client) is currently running. Names greater than
|
||||
--- 256 characters long are truncated.
|
||||
--- (not the UI client) is currently running.
|
||||
---
|
||||
--- @return string
|
||||
function vim.fn.hostname() end
|
||||
|
||||
@@ -54,10 +54,16 @@ hashpipe:write([[
|
||||
]])
|
||||
|
||||
local funcs = loadfile(eval_file)().funcs
|
||||
for _, func in pairs(funcs) do
|
||||
if func.float_func then
|
||||
for name, func in pairs(funcs) do
|
||||
local n = (func.func_float and 1 or 0) + (func.func_lua and 1 or 0) + (func.func and 1 or 0)
|
||||
assert(n <= 1, name .. ': only one of func, func_float, func_lua can be set')
|
||||
|
||||
if func.func_float then
|
||||
func.func = 'float_op_wrapper'
|
||||
func.data = '{ .float_func = &' .. func.float_func .. ' }'
|
||||
func.data = '{ .func_float = &' .. func.func_float .. ' }'
|
||||
elseif func.func_lua then
|
||||
func.func = 'lua_wrapper'
|
||||
func.data = '{ .func_lua = "' .. func.func_lua .. '" }'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -67,7 +73,7 @@ for _, fun in ipairs(metadata) do
|
||||
funcs[fun.name] = {
|
||||
args = #fun.parameters,
|
||||
func = 'api_wrapper',
|
||||
data = '{ .api_handler = &method_handlers[' .. fun.handler_id .. '] }',
|
||||
data = '{ .func_api = &method_handlers[' .. fun.handler_id .. '] }',
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
-- File containing table with all functions.
|
||||
--
|
||||
-- Keys:
|
||||
-- Defines all "vimfn" (builtin/"eval"/"Vimscript") functions.
|
||||
--
|
||||
--- @class vim.EvalFn
|
||||
--- @field name? string
|
||||
--- @field args? integer|integer[] Number of arguments, list with maximum and minimum number of arguments
|
||||
--- or list with a minimum number of arguments only. Defaults to zero
|
||||
--- arguments.
|
||||
--- @field base? integer For methods: the argument to use as the base argument (1-indexed):
|
||||
--- base->method()
|
||||
--- Defaults to BASE_NONE (function cannot be used as a method).
|
||||
--- @field func? string Name of the C function which implements the Vimscript function. Defaults to
|
||||
--- `f_{funcname}`.
|
||||
--- @field float_func? string
|
||||
--- @field args? integer|integer[] (default: 0) Number of arguments, list with maximum and minimum number of arguments or list with a minimum number of arguments only.
|
||||
--- @field base? integer For methods: the argument to use as the base argument (1-indexed): base->method(). Defaults to BASE_NONE (function cannot be used as a method).
|
||||
--- @field func? string (default: "f_{funcname}") C function which implements the vimfn.
|
||||
--- @field func_float? string Floating-point C function. Sets func="float_op_wrapper".
|
||||
--- @field func_lua? string Function name in `vim._core.vimfn.*` (e.g. "f_hostname"). Sets func="lua_wrapper".
|
||||
--- @field fast? boolean Function can run in |api-fast| events. Defaults to false.
|
||||
--- @field deprecated? true
|
||||
--- @field returns? string|false
|
||||
@@ -76,7 +70,7 @@ M.funcs = {
|
||||
< 2.094395
|
||||
|
||||
]=],
|
||||
float_func = 'acos',
|
||||
func_float = 'acos',
|
||||
name = 'acos',
|
||||
params = { { 'expr', 'number' } },
|
||||
returns = 'number',
|
||||
@@ -276,7 +270,7 @@ M.funcs = {
|
||||
< -0.523599
|
||||
|
||||
]=],
|
||||
float_func = 'asin',
|
||||
func_float = 'asin',
|
||||
name = 'asin',
|
||||
params = { { 'expr', 'any' } },
|
||||
returns = 'number',
|
||||
@@ -567,7 +561,7 @@ M.funcs = {
|
||||
< -1.326405
|
||||
|
||||
]=],
|
||||
float_func = 'atan',
|
||||
func_float = 'atan',
|
||||
name = 'atan',
|
||||
params = { { 'expr', 'number' } },
|
||||
returns = 'number',
|
||||
@@ -1022,7 +1016,7 @@ M.funcs = {
|
||||
Returns 0.0 if {expr} is not a |Float| or a |Number|.
|
||||
|
||||
]=],
|
||||
float_func = 'ceil',
|
||||
func_float = 'ceil',
|
||||
name = 'ceil',
|
||||
params = { { 'expr', 'number' } },
|
||||
returns = 'number',
|
||||
@@ -1597,7 +1591,7 @@ M.funcs = {
|
||||
< -0.646043
|
||||
|
||||
]=],
|
||||
float_func = 'cos',
|
||||
func_float = 'cos',
|
||||
name = 'cos',
|
||||
params = { { 'expr', 'number' } },
|
||||
returns = 'number',
|
||||
@@ -1618,7 +1612,7 @@ M.funcs = {
|
||||
< -1.127626
|
||||
|
||||
]=],
|
||||
float_func = 'cosh',
|
||||
func_float = 'cosh',
|
||||
name = 'cosh',
|
||||
params = { { 'expr', 'number' } },
|
||||
returns = 'number',
|
||||
@@ -2357,7 +2351,7 @@ M.funcs = {
|
||||
< 0.367879
|
||||
|
||||
]=],
|
||||
float_func = 'exp',
|
||||
func_float = 'exp',
|
||||
name = 'exp',
|
||||
params = { { 'expr', 'number' } },
|
||||
signature = 'exp({expr})',
|
||||
@@ -2882,7 +2876,7 @@ M.funcs = {
|
||||
< 4.0
|
||||
|
||||
]=],
|
||||
float_func = 'floor',
|
||||
func_float = 'floor',
|
||||
name = 'floor',
|
||||
params = { { 'expr', 'number' } },
|
||||
signature = 'floor({expr})',
|
||||
@@ -5515,10 +5509,10 @@ M.funcs = {
|
||||
hostname = {
|
||||
desc = [=[
|
||||
Returns the hostname of the machine on which the Nvim server
|
||||
(not the UI client) is currently running. Names greater than
|
||||
256 characters long are truncated.
|
||||
(not the UI client) is currently running.
|
||||
]=],
|
||||
fast = true,
|
||||
func_lua = 'f_hostname',
|
||||
name = 'hostname',
|
||||
params = {},
|
||||
returns = 'string',
|
||||
@@ -6576,7 +6570,7 @@ M.funcs = {
|
||||
< 5.0
|
||||
|
||||
]=],
|
||||
float_func = 'log',
|
||||
func_float = 'log',
|
||||
name = 'log',
|
||||
params = { { 'expr', 'number' } },
|
||||
returns = 'number',
|
||||
@@ -6596,7 +6590,7 @@ M.funcs = {
|
||||
< -2.0
|
||||
|
||||
]=],
|
||||
float_func = 'log10',
|
||||
func_float = 'log10',
|
||||
name = 'log10',
|
||||
params = { { 'expr', 'number' } },
|
||||
returns = 'number',
|
||||
@@ -9058,7 +9052,7 @@ M.funcs = {
|
||||
< -5.0
|
||||
|
||||
]=],
|
||||
float_func = 'round',
|
||||
func_float = 'round',
|
||||
name = 'round',
|
||||
params = { { 'expr', 'number' } },
|
||||
returns = 'number',
|
||||
@@ -11047,7 +11041,7 @@ M.funcs = {
|
||||
< 0.763301
|
||||
|
||||
]=],
|
||||
float_func = 'sin',
|
||||
func_float = 'sin',
|
||||
name = 'sin',
|
||||
params = { { 'expr', 'number' } },
|
||||
returns = 'number',
|
||||
@@ -11068,7 +11062,7 @@ M.funcs = {
|
||||
< -1.026517
|
||||
|
||||
]=],
|
||||
float_func = 'sinh',
|
||||
func_float = 'sinh',
|
||||
name = 'sinh',
|
||||
params = { { 'expr', 'number' } },
|
||||
signature = 'sinh({expr})',
|
||||
@@ -11335,7 +11329,7 @@ M.funcs = {
|
||||
NaN may be different, it depends on system libraries.
|
||||
|
||||
]=],
|
||||
float_func = 'sqrt',
|
||||
func_float = 'sqrt',
|
||||
name = 'sqrt',
|
||||
params = { { 'expr', 'number' } },
|
||||
signature = 'sqrt({expr})',
|
||||
@@ -12517,7 +12511,7 @@ M.funcs = {
|
||||
< -1.181502
|
||||
|
||||
]=],
|
||||
float_func = 'tan',
|
||||
func_float = 'tan',
|
||||
name = 'tan',
|
||||
params = { { 'expr', 'number' } },
|
||||
returns = 'number',
|
||||
@@ -12538,7 +12532,7 @@ M.funcs = {
|
||||
< -0.761594
|
||||
|
||||
]=],
|
||||
float_func = 'tanh',
|
||||
func_float = 'tanh',
|
||||
name = 'tanh',
|
||||
params = { { 'expr', 'number' } },
|
||||
returns = 'number',
|
||||
@@ -12798,7 +12792,7 @@ M.funcs = {
|
||||
< 4.0
|
||||
|
||||
]=],
|
||||
float_func = 'trunc',
|
||||
func_float = 'trunc',
|
||||
name = 'trunc',
|
||||
params = { { 'expr', 'number' } },
|
||||
returns = 'integer',
|
||||
|
||||
@@ -227,7 +227,7 @@ char *get_expr_name(expand_T *xp, int idx)
|
||||
return get_user_var_name(xp, ++intidx);
|
||||
}
|
||||
|
||||
/// Find internal function in hash functions
|
||||
/// Gets a builtin (aka "vimfn", "eval") function from the generated hash table.
|
||||
///
|
||||
/// @param[in] name Name of the function.
|
||||
///
|
||||
@@ -240,6 +240,17 @@ const EvalFuncDef *find_internal_func(const char *const name)
|
||||
return index >= 0 ? &functions[index] : NULL;
|
||||
}
|
||||
|
||||
/// Gets the Lua name of a Lua-implemented "vimfn" function, or NULL if not found.
|
||||
const char *find_internal_func_lua(const char *const name)
|
||||
FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_PURE FUNC_ATTR_NONNULL_ALL
|
||||
{
|
||||
const EvalFuncDef *const fdef = find_internal_func(name);
|
||||
if (fdef && fdef->func == &lua_wrapper) {
|
||||
return fdef->data.func_lua;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/// Check the argument count to use for internal function "fdef".
|
||||
/// @return -1 for failure, 0 if no method base accepted, 1 if method base is
|
||||
/// first argument, 2 if method base is second argument, etc.
|
||||
@@ -325,7 +336,7 @@ static bool non_zero_arg(typval_T *argvars)
|
||||
&& *argvars[0].vval.v_string != NUL));
|
||||
}
|
||||
|
||||
/// Apply a floating point C function on a typval with one float_T.
|
||||
/// Apply a floating point C function on a typval with one float_T (`func_float` in eval.lua).
|
||||
///
|
||||
/// Some versions of glibc on i386 have an optimization that makes it harder to
|
||||
/// call math functions indirectly from inside an inlined function, causing
|
||||
@@ -336,19 +347,23 @@ static void float_op_wrapper(typval_T *argvars, typval_T *rettv, EvalFuncData fp
|
||||
|
||||
rettv->v_type = VAR_FLOAT;
|
||||
if (tv_get_float_chk(argvars, &f)) {
|
||||
rettv->vval.v_float = fptr.float_func(f);
|
||||
rettv->vval.v_float = fptr.func_float(f);
|
||||
} else {
|
||||
rettv->vval.v_float = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Invokes an API (nvim_) function from Vimscript.
|
||||
///
|
||||
/// Converts `argvars` to API Objects, calls the API handler, and converts the result back.
|
||||
/// Used by `gen_eval.lua` for `eval=true` API functions.
|
||||
static void api_wrapper(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
||||
{
|
||||
if (check_secure()) {
|
||||
return;
|
||||
}
|
||||
|
||||
MsgpackRpcRequestHandler handler = *fptr.api_handler;
|
||||
MsgpackRpcRequestHandler handler = *fptr.func_api;
|
||||
|
||||
MAXSIZE_TEMP_ARRAY(args, MAX_FUNC_ARGS);
|
||||
Arena arena = ARENA_EMPTY;
|
||||
@@ -375,11 +390,43 @@ end:
|
||||
api_clear_error(&err);
|
||||
}
|
||||
|
||||
/// Invokes a Lua-implemented vimfn/"f_xx" function from Vimscript (`func_lua` in eval.lua).
|
||||
///
|
||||
/// - Converts argvars to API Objects, calls the Lua function, converts the result back.
|
||||
/// - NOT used when called from Lua; `nlua_call()` calls the function directly.
|
||||
static void lua_wrapper(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
||||
{
|
||||
MAXSIZE_TEMP_ARRAY(args, MAX_FUNC_ARGS);
|
||||
Arena arena = ARENA_EMPTY;
|
||||
|
||||
for (typval_T *tv = argvars; tv->v_type != VAR_UNKNOWN; tv++) {
|
||||
ADD_C(args, vim_to_object(tv, &arena, false));
|
||||
}
|
||||
|
||||
// `func_lua` is the function name (e.g. "f_hostname") in `_core/vimfn.lua`.
|
||||
char buf[256];
|
||||
snprintf(buf, sizeof(buf), "return require('vim._core.vimfn').%s(...)", fptr.func_lua);
|
||||
|
||||
Error err = ERROR_INIT;
|
||||
Object result = nlua_exec(cstr_as_string(buf), NULL, args, kRetObject, &arena, &err);
|
||||
|
||||
if (ERROR_SET(&err)) {
|
||||
semsg_multiline("emsg", e_api_error, err.msg);
|
||||
goto end;
|
||||
}
|
||||
|
||||
object_to_vim_take_luaref(&result, rettv, true, &err);
|
||||
|
||||
end:
|
||||
arena_mem_free(arena_finish(&arena));
|
||||
api_clear_error(&err);
|
||||
}
|
||||
|
||||
/// "abs(expr)" function
|
||||
static void f_abs(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
||||
{
|
||||
if (argvars[0].v_type == VAR_FLOAT) {
|
||||
float_op_wrapper(argvars, rettv, (EvalFuncData){ .float_func = &fabs });
|
||||
float_op_wrapper(argvars, rettv, (EvalFuncData){ .func_float = &fabs });
|
||||
} else {
|
||||
bool error = false;
|
||||
|
||||
@@ -2901,16 +2948,6 @@ static void f_hlexists(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
||||
rettv->vval.v_number = highlight_exists(tv_get_string(&argvars[0]));
|
||||
}
|
||||
|
||||
/// "hostname()" function
|
||||
static void f_hostname(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
||||
{
|
||||
char hostname[256];
|
||||
|
||||
os_get_hostname(hostname, 256);
|
||||
rettv->v_type = VAR_STRING;
|
||||
rettv->vval.v_string = xstrdup(hostname);
|
||||
}
|
||||
|
||||
/// "index()" function
|
||||
static void f_index(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
||||
{
|
||||
|
||||
@@ -1207,6 +1207,7 @@ static bool viml_func_is_fast(const char *name)
|
||||
return false;
|
||||
}
|
||||
|
||||
/// "vim.call()", aka "vim.fn".
|
||||
int nlua_call(lua_State *lstate)
|
||||
{
|
||||
Error err = ERROR_INIT;
|
||||
@@ -1224,6 +1225,27 @@ int nlua_call(lua_State *lstate)
|
||||
return luaL_error(lstate, "Function called with too many arguments");
|
||||
}
|
||||
|
||||
// Fast path: if the vimfn is defined with `func_lua`, call the Lua function
|
||||
// directly without Lua→typval→Object→Lua round-trip.
|
||||
const char *func_lua = find_internal_func_lua(name);
|
||||
if (func_lua) {
|
||||
lua_getglobal(lstate, "require");
|
||||
lua_pushliteral(lstate, "vim._core.vimfn");
|
||||
if (lua_pcall(lstate, 1, 1, 0) != 0) {
|
||||
return lua_error(lstate);
|
||||
}
|
||||
lua_getfield(lstate, -1, func_lua);
|
||||
lua_remove(lstate, -2); // remove module table
|
||||
// Push args (still on the stack as Lua values).
|
||||
for (int j = 0; j < nargs; j++) {
|
||||
lua_pushvalue(lstate, j + 2);
|
||||
}
|
||||
if (lua_pcall(lstate, nargs, 1, 0) != 0) {
|
||||
return lua_error(lstate);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
typval_T vim_args[MAX_FUNC_ARGS + 1];
|
||||
int i = 0; // also used for freeing the variables
|
||||
for (; i < nargs; i++) {
|
||||
|
||||
@@ -32,8 +32,9 @@ typedef double float_T;
|
||||
typedef struct MsgpackRpcRequestHandler MsgpackRpcRequestHandler;
|
||||
|
||||
typedef union {
|
||||
float_T (*float_func)(float_T);
|
||||
const MsgpackRpcRequestHandler *api_handler;
|
||||
float_T (*func_float)(float_T);
|
||||
const MsgpackRpcRequestHandler *func_api; // Vimscript bridge to API fn.
|
||||
const char *func_lua; ///< Lua-implemented vimfn.
|
||||
void *null;
|
||||
} EvalFuncData;
|
||||
|
||||
|
||||
@@ -231,6 +231,7 @@ describe('vim._core', function()
|
||||
'vim._core.system',
|
||||
'vim._core.ui2',
|
||||
'vim._core.util',
|
||||
'vim._core.vimfn',
|
||||
'vim._init_packages',
|
||||
'vim.filetype',
|
||||
'vim.fs',
|
||||
|
||||
@@ -1391,6 +1391,15 @@ describe('lua stdlib', function()
|
||||
)
|
||||
end)
|
||||
|
||||
it('vim.fn `func_lua` (fast path for Lua-implemented builtins)', function()
|
||||
-- hostname() is implemented via func_lua, calling Lua directly when invoked from Lua.
|
||||
local lua_result = exec_lua([[return vim.fn.hostname()]])
|
||||
eq(type(lua_result), 'string')
|
||||
assert(#lua_result > 0, 'hostname() should return a non-empty string')
|
||||
-- VimScript path (lua_wrapper) should return the same result.
|
||||
eq(lua_result, eval('hostname()'))
|
||||
end)
|
||||
|
||||
it('vim.call fails in fast context', function()
|
||||
local screen = Screen.new(120, 10)
|
||||
exec_lua([[
|
||||
@@ -2003,14 +2012,14 @@ describe('lua stdlib', function()
|
||||
|
||||
local errmsg = api.nvim_get_vvar('errmsg')
|
||||
matches(
|
||||
[[
|
||||
^vim%.on%_key%(%) callbacks:.*
|
||||
With ns%_id %d+: .*: Dumb Error
|
||||
stack traceback:
|
||||
.*: in function 'error'
|
||||
.*: in function 'ErrF2'
|
||||
.*: in function 'ErrF1'
|
||||
.*]],
|
||||
t.dedent [[
|
||||
^vim%.on%_key%(%) callbacks:.*
|
||||
With ns%_id %d+: .*: Dumb Error
|
||||
stack traceback:
|
||||
.*: in function 'error'
|
||||
.*: in function 'ErrF2'
|
||||
.*: in function 'ErrF1'
|
||||
.*]],
|
||||
errmsg
|
||||
)
|
||||
end)
|
||||
@@ -3268,8 +3277,8 @@ describe('vim.keymap', function()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('Vimscript function exists()', function()
|
||||
it('can check a lua function', function()
|
||||
describe('vim.fn.exists()', function()
|
||||
it('can check a Lua function', function()
|
||||
eq(
|
||||
1,
|
||||
exec_lua [[
|
||||
|
||||
@@ -215,10 +215,10 @@ describe('listing functions using :function', function()
|
||||
command('let A = {-> 1}')
|
||||
local num = exec_capture('echo A'):match("function%('<lambda>(%d+)'%)")
|
||||
eq(
|
||||
([[
|
||||
function <lambda>%s(...)
|
||||
1 return 1
|
||||
endfunction]]):format(num),
|
||||
t.dedent([[
|
||||
function <lambda>%s(...)
|
||||
1 return 1
|
||||
endfunction]]):format(num),
|
||||
exec_capture(('function <lambda>%s'):format(num))
|
||||
)
|
||||
end)
|
||||
|
||||
Reference in New Issue
Block a user