diff options
| author | Justin M. Keyes <justinkz@gmail.com> | 2026-07-27 06:25:21 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-27 06:25:21 -0400 |
| commit | 359459dec6e1929c0afb56a3111dee7b5a924eea (patch) | |
| tree | 90ffd5f3205a2a1f9e879c88171514686675cee6 /src/nvim/lua/executor.c | |
| parent | faaa4d57f16d96f529fc08ef5752f4dbcb5665cb (diff) | |
| download | rneovim-359459dec6e1929c0afb56a3111dee7b5a924eea.tar.gz rneovim-359459dec6e1929c0afb56a3111dee7b5a924eea.tar.bz2 rneovim-359459dec6e1929c0afb56a3111dee7b5a924eea.zip | |
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.
Diffstat (limited to 'src/nvim/lua/executor.c')
| -rw-r--r-- | src/nvim/lua/executor.c | 49 |
1 files changed, 48 insertions, 1 deletions
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) |