From 0aa7d2f4d546647e0d6b5c338bd5517675e9842c Mon Sep 17 00:00:00 2001 From: bfredl Date: Sun, 17 May 2026 16:25:22 +0200 Subject: [PATCH] feat(api): nvim_buf_call, win_call can has multiple return values #39801 from the "because we can and it is not much code" department. (diffcount excluding tests is actually negative) fixes https://github.com/neovim/neovim/issues/39636#issuecomment-4397141270 --- runtime/doc/api.txt | 4 ++ runtime/doc/news.txt | 4 ++ runtime/lua/vim/_meta/api.gen.lua | 4 ++ src/gen/gen_api_dispatch.lua | 17 ++----- src/nvim/api/buffer.c | 16 +++---- src/nvim/api/window.c | 10 ++-- src/nvim/api/window.h | 2 + src/nvim/lua/executor.c | 7 ++- src/nvim/lua/executor.h | 1 + test/functional/api/buffer_spec.lua | 71 +++++++++++++++++++++++++++++ test/functional/api/window_spec.lua | 71 +++++++++++++++++++++++++++++ 11 files changed, 179 insertions(+), 28 deletions(-) diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt index 663d7cb4e3..b547c02ef9 100644 --- a/runtime/doc/api.txt +++ b/runtime/doc/api.txt @@ -2422,6 +2422,8 @@ nvim_buf_call({buf}, {fun}) *nvim_buf_call()* This is useful e.g. to call Vimscript functions that only work with the current buffer/window currently, like `jobstart(…, {'term': v:true})`. + This preserves any Lua return values, including multiple return values. + Attributes: ~ Lua |vim.api| only Since: 0.5.0 @@ -4060,6 +4062,8 @@ Window Functions *api-window* nvim_win_call({win}, {fun}) *nvim_win_call()* Calls a function with window as temporary current window. + This preserves any Lua return values, including multiple return values. + Attributes: ~ Lua |vim.api| only Since: 0.5.0 diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 9193eb801a..8a771637f9 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -274,6 +274,10 @@ These existing features changed their behavior. • |nvim_exec_autocmds()| • |nvim_get_autocmds()| +• API: + • |nvim_buf_call()| and |nvim_win_call()| now preserve multiple return + values + ============================================================================== REMOVED FEATURES *news-removed* diff --git a/runtime/lua/vim/_meta/api.gen.lua b/runtime/lua/vim/_meta/api.gen.lua index b9cc487448..b280ec4bd4 100644 --- a/runtime/lua/vim/_meta/api.gen.lua +++ b/runtime/lua/vim/_meta/api.gen.lua @@ -277,6 +277,8 @@ function vim.api.nvim_buf_attach(buf, send_buffer, opts) end --- This is useful e.g. to call Vimscript functions that only work with the --- current buffer/window currently, like `jobstart(…, {'term': v:true})`. --- +--- This preserves any Lua return values, including multiple return values. +--- --- @param buf integer Buffer id, or 0 for current buffer --- @param fun function Function to call inside the buffer (currently Lua callable --- only) @@ -2423,6 +2425,8 @@ function vim.api.nvim_ui_send(content) end --- Calls a function with window as temporary current window. --- --- +--- This preserves any Lua return values, including multiple return values. +--- --- @see `:help win_execute()` --- @see vim.api.nvim_buf_call --- @param win integer `window-ID`, or 0 for current window diff --git a/src/gen/gen_api_dispatch.lua b/src/gen/gen_api_dispatch.lua index c8b1de455e..ec491b13c2 100644 --- a/src/gen/gen_api_dispatch.lua +++ b/src/gen/gen_api_dispatch.lua @@ -882,16 +882,8 @@ exit_0: local ret_type = real_type(fn.return_type) local ret_mode = (ret_type == 'Object') and '&' or '' if fn.has_lua_imp then - -- only push onto the Lua stack if we haven't already - write_shifted_output( - [[ - if (lua_gettop(lstate) == 0) { - nlua_push_%s(lstate, %sret, kNluaPushSpecial | kNluaPushFreeRefs); - } - ]], - return_type, - ret_mode - ) + -- it is up to the function to push return values + write_shifted_output(' (void)ret;') elseif ret_type:match('^KeyDict_') then write_shifted_output(' nlua_push_keydict(lstate, &ret, %s_table);\n', return_type:sub(9)) else @@ -911,11 +903,12 @@ exit_0: %s %s %s - return 1; + return %s; ]], free_retval, free_at_exit_code, - err_throw_code + err_throw_code, + (fn.has_lua_imp and 'lua_gettop(lstate)' or '1') ) else write_shifted_output( diff --git a/src/nvim/api/buffer.c b/src/nvim/api/buffer.c index e62fd0e401..f1b445142b 100644 --- a/src/nvim/api/buffer.c +++ b/src/nvim/api/buffer.c @@ -289,12 +289,7 @@ ArrayOf(String) nvim_buf_get_lines(uint64_t channel_id, return rv; }); - if (start >= end) { - // Return 0-length array - return rv; - } - - size_t size = (size_t)(end - start); + size_t size = end >= start ? (size_t)(end - start) : 0; init_line_array(lstate, &rv, size, arena); @@ -1195,12 +1190,14 @@ ArrayOf(Integer, 2) nvim_buf_get_mark(Buffer buf, String name, Arena *arena, Err /// This is useful e.g. to call Vimscript functions that only work with the /// current buffer/window currently, like `jobstart(…, {'term': v:true})`. /// +/// This preserves any Lua return values, including multiple return values. +/// /// @param buf Buffer id, or 0 for current buffer /// @param fun Function to call inside the buffer (currently Lua callable /// only) /// @param[out] err Error details, if any /// @return Return value of function. -Object nvim_buf_call(Buffer buf, LuaRef fun, Error *err) +Object nvim_buf_call(Buffer buf, LuaRef fun, lua_State *lstate, Error *err) FUNC_API_SINCE(7) FUNC_API_LUA_ONLY { @@ -1209,18 +1206,17 @@ Object nvim_buf_call(Buffer buf, LuaRef fun, Error *err) return NIL; } - Object res = OBJECT_INIT; TRY_WRAP(err, { aco_save_T aco; aucmd_prepbuf(&aco, b); Array args = ARRAY_DICT_INIT; - res = nlua_call_ref(fun, NULL, args, kRetLuaref, NULL, err); + nlua_call_ref(fun, NULL, args, kRetMultiStack, NULL, err); aucmd_restbuf(&aco); }); - return res; + return NIL; // kRetMultiStack: values are already on the lua stack } /// @nodoc diff --git a/src/nvim/api/window.c b/src/nvim/api/window.c index 2fcb249b41..7fa5ebed73 100644 --- a/src/nvim/api/window.c +++ b/src/nvim/api/window.c @@ -2,6 +2,7 @@ #include #include +#include "lua.h" #include "nvim/api/keysets_defs.h" #include "nvim/api/private/defs.h" #include "nvim/api/private/dispatch.h" @@ -400,12 +401,14 @@ void nvim_win_close(Window win, Boolean force, Error *err) /// @see |win_execute()| /// @see |nvim_buf_call()| /// +/// This preserves any Lua return values, including multiple return values. +/// /// @param win |window-ID|, or 0 for current window /// @param fun Function to call inside the window (currently Lua callable /// only) /// @param[out] err Error details, if any /// @return Return value of function. -Object nvim_win_call(Window win, LuaRef fun, Error *err) +Object nvim_win_call(Window win, LuaRef fun, lua_State *lstate, Error *err) FUNC_API_SINCE(7) FUNC_API_LUA_ONLY { @@ -415,16 +418,15 @@ Object nvim_win_call(Window win, LuaRef fun, Error *err) } tabpage_T *tabpage = win_find_tabpage(w); - Object res = OBJECT_INIT; TRY_WRAP(err, { win_execute_T win_execute_args; if (win_execute_before(&win_execute_args, w, tabpage)) { Array args = ARRAY_DICT_INIT; - res = nlua_call_ref(fun, NULL, args, kRetLuaref, NULL, err); + nlua_call_ref(fun, NULL, args, kRetMultiStack, NULL, err); } win_execute_after(&win_execute_args); }); - return res; + return NIL; // kRetMultiStack: values are already on the lua stack } /// Set highlight namespace for a window. This will use highlights defined with diff --git a/src/nvim/api/window.h b/src/nvim/api/window.h index 757ec533aa..fe7d468822 100644 --- a/src/nvim/api/window.h +++ b/src/nvim/api/window.h @@ -1,5 +1,7 @@ #pragma once +#include // IWYU pragma: keep + #include "nvim/api/keysets_defs.h" // IWYU pragma: keep #include "nvim/api/private/defs.h" // IWYU pragma: keep diff --git a/src/nvim/lua/executor.c b/src/nvim/lua/executor.c index c2b1887d60..b785ed0c64 100644 --- a/src/nvim/lua/executor.c +++ b/src/nvim/lua/executor.c @@ -1801,7 +1801,7 @@ Object nlua_call_ref(LuaRef ref, const char *name, Array args, LuaRetMode mode, static int mode_ret(LuaRetMode mode) { - return mode == kRetMulti ? LUA_MULTRET : 1; + return (mode == kRetMulti || mode == kRetMultiStack) ? LUA_MULTRET : 1; } /// Like nlua_call_ref, but with an option to run in fast (api-fast) context. @@ -1844,7 +1844,7 @@ Object nlua_call_ref_ctx(bool fast, LuaRef ref, const char *name, Array args, Lu static Object nlua_call_pop_retval(lua_State *lstate, LuaRetMode mode, Arena *arena, int pretop, Error *err) { - if (mode != kRetMulti && lua_isnil(lstate, -1)) { + if (mode != kRetMulti && mode != kRetMultiStack && lua_isnil(lstate, -1)) { lua_pop(lstate, 1); return NIL; } @@ -1878,6 +1878,9 @@ static Object nlua_call_pop_retval(lua_State *lstate, LuaRetMode mode, Arena *ar } res.size = (size_t)nres; return ARRAY_OBJ(res); + case kRetMultiStack: + ; + return NIL; } UNREACHABLE; } diff --git a/src/nvim/lua/executor.h b/src/nvim/lua/executor.h index 88acf2dece..59082af6c9 100644 --- a/src/nvim/lua/executor.h +++ b/src/nvim/lua/executor.h @@ -43,6 +43,7 @@ typedef enum { ///< Should also be used when return value is ignored, as it is allocation-free kRetLuaref, ///< return value becomes a single Luaref, regardless of type (except NIL) kRetMulti, ///< like kRetObject but return multiple return values as an Array + kRetMultiStack, ///< like kRetMulti but leave values on the lua stack } LuaRetMode; /// Maximum number of errors in vim.ui_attach() and decor provider callbacks. diff --git a/test/functional/api/buffer_spec.lua b/test/functional/api/buffer_spec.lua index a167e7ad11..7b05206bb6 100644 --- a/test/functional/api/buffer_spec.lua +++ b/test/functional/api/buffer_spec.lua @@ -2427,4 +2427,75 @@ describe('api/buf', function() eq(false, pcall(api.nvim_buf_del_mark, 99, 'a')) end) end) + + describe('nvim_buf_call', function() + it('supports multiple returns', function() + local curbuf = api.nvim_get_current_buf() + local other = api.nvim_create_buf(false, true) + exec_lua(function() + function with_len(...) + return select('#', ...), { ... } + end + function test(fn) + local len, res = with_len(vim.api.nvim_buf_call(other, fn)) + -- convert to serializable vim.NIL + for i = 1, len do + if res[i] == nil then + res[i] = vim.NIL + end + end + return res + end + end) + + eq( + { other }, + exec_lua(function() + return test(function() + return vim.api.nvim_get_current_buf() + end) + end) + ) + eq(curbuf, api.nvim_get_current_buf()) + eq( + { other, vim.NIL }, + exec_lua(function() + return test(function() + return vim.api.nvim_get_current_buf(), nil + end) + end) + ) + + eq( + { 6, 7 }, + exec_lua(function() + return test(function() + return 6, 7 + end) + end) + ) + eq( + { 6, vim.NIL, 7 }, + exec_lua(function() + return test(function() + return 6, nil, 7 + end) + end) + ) + eq( + {}, + exec_lua(function() + return test(function() end) + end) + ) + eq( + { vim.NIL }, + exec_lua(function() + return test(function() + return nil + end) + end) + ) + end) + end) end) diff --git a/test/functional/api/window_spec.lua b/test/functional/api/window_spec.lua index 650a4bf5e3..7b0f9e2d56 100644 --- a/test/functional/api/window_spec.lua +++ b/test/functional/api/window_spec.lua @@ -3885,4 +3885,75 @@ describe('API/win', function() command('tabclose') eq(tab2, api.nvim_get_current_tabpage()) end) + + describe('nvim_win_call', function() + it('supports multiple returns', function() + local cur = api.nvim_get_current_win() + local other = api.nvim_open_win(api.nvim_create_buf(false, true), false, { split = 'left' }) + exec_lua(function() + function with_len(...) + return select('#', ...), { ... } + end + function test(fn) + local len, res = with_len(vim.api.nvim_win_call(other, fn)) + -- convert to serializable vim.NIL + for i = 1, len do + if res[i] == nil then + res[i] = vim.NIL + end + end + return res + end + end) + + eq( + { other }, + exec_lua(function() + return test(function() + return vim.api.nvim_get_current_win() + end) + end) + ) + eq(cur, api.nvim_get_current_win()) + eq( + { other, vim.NIL }, + exec_lua(function() + return test(function() + return vim.api.nvim_get_current_win(), nil + end) + end) + ) + + eq( + { 6, 7 }, + exec_lua(function() + return test(function() + return 6, 7 + end) + end) + ) + eq( + { 6, vim.NIL, 7 }, + exec_lua(function() + return test(function() + return 6, nil, 7 + end) + end) + ) + eq( + {}, + exec_lua(function() + return test(function() end) + end) + ) + eq( + { vim.NIL }, + exec_lua(function() + return test(function() + return nil + end) + end) + ) + end) + end) end)