From 359459dec6e1929c0afb56a3111dee7b5a924eea Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Mon, 27 Jul 2026 06:25:21 -0400 Subject: refactor(exmode): Ex-mode as cmdwin + Lua #40991 Problem: POSIX-compatible Ex-mode requires special-cases all over the codebase to match various quirks that don't actually matter to users. - The main utility of *interactive* Ex-mode is its REPL behavior, and that can be achieved with `cmdwin`, which also gains extra UX benefits. - The main utility of *non-interactive* `nvim -es` is for shell scripting, where Ex-mode quirks are mostly unhelpful (e.g. the "Entering Ex mode" message). Solution: - Reimplement *interactive* Ex-mode as a "persistent, insert-mode cmdwin" in Lua. - "nvim -e/-E" is simply an alias to "gQ". - Reframe *non-interactive* Ex-mode (`nvim -es`) as "script mode". - Drop POSIX Ex-mode quirks. Improvements: - "nvim -V1 -es" output ends with a final newline! - "nvim -V1 -es" no longer shows the "Entering Ex mode" msg. (This was pointless noise, unwanted for scripting purposes.) - stdin is no longer typeahead. Scripts (":lua io.read()") can read stdin as data. - Empty line is a no-op: a stray blank line no longer moves the cursor (deviates from POSIX ex "+1"), no longer exits 1 at EOF (E501). Preserved behavior: - cursor starts at "$" - mode()=="cv" (for non-interactive) - multiline commands (:append/:function/heredoc pull continuation lines) - bare-range print - :print=>stdout - -V1=>stderr - CRLF input - continue-after-error and exit codes Dropped (regressed) POSIX behavior (non-interactive): - Event loop only ticks while/between commands, not while blocked waiting for a stdin line. - ":g/pat/visual...Q" - input()/getchar()/":s/x/y/c" no longer consume stdin lines as answers: Nvim stops at end-of-input, skipping the rest of the script, exit 0. Use ":lua io.read()" instead. - If users care about this they should use interactive Ex-mode (`gQ`). - ":@r" stops at end of the register instead of continuing to read cmdline input from stdin. --- src/nvim/lua/executor.c | 49 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) (limited to 'src/nvim/lua/executor.c') diff --git a/src/nvim/lua/executor.c b/src/nvim/lua/executor.c index c6bf2fe586..e05da57b3c 100644 --- a/src/nvim/lua/executor.c +++ b/src/nvim/lua/executor.c @@ -99,6 +99,11 @@ typedef struct { size_t size; } ModuleDef; +typedef struct { + LuaRef getline; ///< Lua function returning the next input line, or nil at EOF. + bool got_line; ///< True if at least one line was read. +} ExDocmdCookie; + #include "lua/executor.c.generated.h" #include "lua/vim_module.generated.h" @@ -598,6 +603,44 @@ static int nlua_check_interrupt(lua_State *lstate) return 1; } +/// LineGetter for nlua_ex_docmd(): calls the Lua `getline` function. +static char *nlua_ex_getline(int c, void *cookie, int indent, bool do_concat) +{ + ExDocmdCookie *ecd = cookie; + lua_State *lstate = global_lstate; + nlua_pushref(lstate, ecd->getline); + if (nlua_pcall(lstate, 0, 1)) { + nlua_error(lstate, _("Error executing Ex-mode line getter: %.*s")); + return NULL; + } + if (lua_type(lstate, -1) != LUA_TSTRING) { // nil: EOF. + lua_pop(lstate, 1); + return NULL; + } + char *line = xstrdup(lua_tostring(lstate, -1)); + lua_pop(lstate, 1); + ecd->got_line = true; + return line; +} + +/// For exmode.lua: Executes one cmdline obtained from `getline`, which is also called for any +/// continuation lines (`:append` text, `:function` body, heredoc, …). Unlike `nvim_exec2()`, errors +/// are emitted (not thrown), so the caller can continue with the next command. #40966 +static int nlua_ex_docmd(lua_State *lstate) + FUNC_ATTR_NONNULL_ALL +{ + luaL_checktype(lstate, 1, LUA_TFUNCTION); + ExDocmdCookie cookie = { .getline = nlua_ref_global(lstate, 1) }; + msg_scroll = true; + need_wait_return = false; + no_wait_return++; // Don't wait for return, e.g. after "-V1" messages. + do_cmdline(NULL, nlua_ex_getline, &cookie, 0); + no_wait_return--; + nlua_unref_global(lstate, cookie.getline); + lua_pushboolean(lstate, cookie.got_line); + return 1; +} + static int nlua_os_exit(lua_State *lstate) { if (in_fast_callback > 0) { @@ -900,7 +943,7 @@ static bool nlua_state_init(lua_State *const lstate) FUNC_ATTR_NONNULL_ALL nlua_common_vim_init(lstate, false); - // vim._core wait helpers + // Bindings for `vim.wait()`. lua_getfield(lstate, -1, "_core"); lua_pushcfunction(lstate, &nlua_loop_poll); lua_setfield(lstate, -2, "loop_poll"); @@ -908,6 +951,10 @@ static bool nlua_state_init(lua_State *const lstate) FUNC_ATTR_NONNULL_ALL lua_setfield(lstate, -2, "ui_flush"); lua_pushcfunction(lstate, &nlua_check_interrupt); lua_setfield(lstate, -2, "check_interrupt"); + + // Misc vim._core bindings. + lua_pushcfunction(lstate, &nlua_ex_docmd); + lua_setfield(lstate, -2, "ex_docmd"); lua_pop(lstate, 1); // patch require() (only for --startuptime) -- cgit