aboutsummaryrefslogtreecommitdiff
path: root/src/nvim/lua/executor.c
diff options
context:
space:
mode:
authorJustin M. Keyes <justinkz@gmail.com>2026-07-27 06:25:21 -0400
committerGitHub <noreply@github.com>2026-07-27 06:25:21 -0400
commit359459dec6e1929c0afb56a3111dee7b5a924eea (patch)
tree90ffd5f3205a2a1f9e879c88171514686675cee6 /src/nvim/lua/executor.c
parentfaaa4d57f16d96f529fc08ef5752f4dbcb5665cb (diff)
downloadrneovim-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.c49
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)