From 359459dec6e1929c0afb56a3111dee7b5a924eea Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Mon, 27 Jul 2026 06:25:21 -0400 Subject: [PATCH] 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. --- runtime/doc/if_pyth.txt | 2 +- runtime/doc/insert.txt | 2 +- runtime/doc/intro.txt | 24 +- runtime/doc/news.txt | 17 +- runtime/doc/quickref.txt | 4 +- runtime/doc/repeat.txt | 6 - runtime/doc/starting.txt | 7 +- runtime/doc/various.txt | 4 +- runtime/doc/vim_diff.txt | 8 +- runtime/doc/vimfn.txt | 5 +- runtime/lua/vim/_core/defaults.lua | 2 +- runtime/lua/vim/_core/exmode.lua | 223 +++++++++++++++++ runtime/lua/vim/_core/server.lua | 5 +- runtime/lua/vim/_core/util.lua | 12 + runtime/lua/vim/_meta/builtin.lua | 7 + runtime/lua/vim/_meta/vimfn.gen.lua | 5 +- src/nvim/autocmd.c | 2 - src/nvim/bufwrite.c | 4 - src/nvim/drawscreen.c | 3 +- src/nvim/eval.c | 2 - src/nvim/eval.lua | 5 +- src/nvim/ex_cmds.c | 165 +++++------- src/nvim/ex_docmd.c | 202 +-------------- src/nvim/ex_getln.c | 58 ++--- src/nvim/fileio.c | 13 +- src/nvim/globals.h | 10 +- src/nvim/grid.c | 1 - src/nvim/input.c | 7 +- src/nvim/lua/executor.c | 49 +++- src/nvim/main.c | 62 ++--- src/nvim/main.h | 1 + src/nvim/message.c | 22 +- src/nvim/normal.c | 62 ++--- src/nvim/os/input.c | 85 ++----- src/nvim/os/input.h | 3 - test/functional/api/vim_spec.lua | 22 +- test/functional/core/main_spec.lua | 1 + test/functional/core/startup_spec.lua | 128 +++++----- test/functional/ex_cmds/normal_spec.lua | 1 + test/functional/legacy/ex_mode_spec.lua | 253 ++++++++----------- test/functional/legacy/global_spec.lua | 14 +- test/functional/terminal/cursor_spec.lua | 304 +++++++++-------------- test/functional/terminal/tui_spec.lua | 2 +- test/functional/ui/cmdline_spec.lua | 73 +++--- test/functional/ui/messages_spec.lua | 29 +-- test/old/testdir/test_ex_mode.vim | 11 +- test/old/testdir/test_excmd.vim | 8 +- test/old/testdir/test_functions.vim | 18 +- test/old/testdir/test_startup.vim | 3 +- 49 files changed, 900 insertions(+), 1056 deletions(-) create mode 100644 runtime/lua/vim/_core/exmode.lua diff --git a/runtime/doc/if_pyth.txt b/runtime/doc/if_pyth.txt index 609a80ef50..d8c8aeb8b1 100644 --- a/runtime/doc/if_pyth.txt +++ b/runtime/doc/if_pyth.txt @@ -153,7 +153,7 @@ Overview >vim Methods of the "vim" module vim.command(str) *python-command* - Executes the vim (ex-mode) command str. Returns None. + Executes the Ex command str. Returns None. Examples: >vim :py vim.command("set tw=72") :py vim.command("%s/aaa/bbb/g") diff --git a/runtime/doc/insert.txt b/runtime/doc/insert.txt index 977710024e..cef97fb1bc 100644 --- a/runtime/doc/insert.txt +++ b/runtime/doc/insert.txt @@ -2026,7 +2026,7 @@ containing only a ".". Watch out for lines starting with a backslash, see |line-continuation|. Text typed after a "|" command separator is used first. So the following -command in ex mode: > +command in Ex mode: > :a|one two . diff --git a/runtime/doc/intro.txt b/runtime/doc/intro.txt index cb28d1501e..53ecd1c51b 100644 --- a/runtime/doc/intro.txt +++ b/runtime/doc/intro.txt @@ -276,8 +276,7 @@ Vim has seven BASIC modes: "!". |Cmdline-mode| - Ex mode: Like Command-line mode, but after entering a command - you remain in Ex mode. Very limited editing of the - command line. |Ex-mode| + you remain in |Ex-mode|. *Terminal-mode* - Terminal mode: In Terminal mode all input (except CTRL-\) is sent to @@ -380,19 +379,26 @@ CTRL-O in Insert mode you get a beep but you are still in Insert mode, type *v_CTRL-\_CTRL-N* *t_CTRL-\_CTRL-N* Additionally the command CTRL-\ CTRL-N or can be used to go to Normal mode from any other mode. This can be used to make sure Vim is in -Normal mode, without causing a beep like would. However, this does not -work in Ex mode. When used after a command that takes an argument, such as -|f| or |m|, the timeout set with 'ttimeoutlen' applies. +Normal mode, without causing a beep like would. When used after +a command that takes an argument, such as |f| or |m|, the timeout set with +'ttimeoutlen' applies. *CTRL-\_CTRL-G* *i_CTRL-\_CTRL-G* *c_CTRL-\_CTRL-G* *v_CTRL-\_CTRL-G* CTRL-\ CTRL-G works the same as |CTRL-\_CTRL-N| for backward compatibility. *gQ* *mode-Ex* *Ex-mode* *Ex* *EX* *E501* gQ Switch to Ex mode. This is like typing ":" commands - one after another, except: - - You don't have to keep pressing ":". - - The screen doesn't get updated after each command. - Use the `:vi` command (|:visual|) to exit this mode. + one after another, except you don't have to keep + pressing ":". Use |:visual| to exit this mode. + + Implementation details: a persistent |cmdwin| where + executes the current line as a command + (against the window from which Ex mode was entered) + and stays open, in insert-mode. Command output is + presented as comment (") lines. After a command that + moves the cursor or changes the buffer, the current + line is printed. An empty line moves to (and prints) + the next line. ============================================================================== Window contents *window-contents* diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index da41073887..4e08457c14 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -93,6 +93,21 @@ DIAGNOSTICS EDITOR +• Interactive |Ex-mode| (|gQ|, `nvim -e`) is a persistent |cmdwin|. This is much + more flexible and makes "Ex mode" more useful as a kind of "cmdline REPL", + with the tradeoff of dropping some POSIX Ex-mode quirks. + • |mode()| returns "n" or "i"; "cv" indicates |-es| (script mode) only. + • ":global/pat/visual" no longer switches to Normal mode per matching line. +• Non-interactive Ex-mode (|-es|, "script mode") executes stdin as Ex commands + directly. It is a "script host", not "POSIX Ex mode". + • Output ends with a final newline. + • |:visual| is |:edit|: it does not switch to reading stdin as Normal-mode + commands, and ":global/pat/visual" does not enter Normal mode per + matching line. + • Commands that wait for typed input (|input()|, |getchar()|, …) stop Nvim + at end-of-input instead of consuming the next stdin line. + • An empty line is a no-op instead of advancing the cursor (or failing + with |E501| at end-of-file). • On Windows, the |trust| db now stores paths with "/" slashes. This means the trust store will be reset (one time). • `stdpath("log")` moved to `stdpath("state")/logs`. @@ -242,7 +257,7 @@ EDITOR real-time, instead of only on |FocusGained| or |:checktime|. • During |complete()|-triggered completion, CTRL-N and CTRL-P are now subject to insert-mode mappings. -• Multi-byte characters, translated by 'langmap', now invoke correct +• Multibyte characters, translated by 'langmap', now invoke correct mappings. Example: >vim set langmap=õ] diff --git a/runtime/doc/quickref.txt b/runtime/doc/quickref.txt index 92ce9c2a93..3de268c57c 100644 --- a/runtime/doc/quickref.txt +++ b/runtime/doc/quickref.txt @@ -1019,7 +1019,7 @@ Short explanation of each option: *option-list* |:version| :ve[rsion] show version information |:normal| :norm[al][!] {commands} execute Normal mode commands -|gQ| gQ switch to "Ex" mode +|gQ| gQ switch to |Ex-mode| |:redir| :redir >{file} redirect messages to {file} |:silent| :silent[!] {command} execute {command} silently @@ -1144,7 +1144,7 @@ Context-sensitive completion on the command-line: |-+| +[num] put the cursor at line [num] (default: last line) |-+c| +{command} execute {command} after loading the file |-+/| +/{pat} {file} .. put the cursor at the first occurrence of {pat} -|-e| -e Ex mode, start vim in Ex mode +|-e| -e |Ex-mode| |-R| -R Read-only mode, implies -n |-m| -m modifications not allowed (resets 'write' option) |-d| -d |diff-mode| diff --git a/runtime/doc/repeat.txt b/runtime/doc/repeat.txt index e478973f62..370c158d63 100644 --- a/runtime/doc/repeat.txt +++ b/runtime/doc/repeat.txt @@ -102,12 +102,6 @@ This replaces all occurrences of "pat" with "PAT". The same can be done with: > :%s/pat/PAT/g Which is two characters shorter! -When using "global" in Ex mode, a special case is using ":visual" as a -command. This will move to a matching line, go to Normal mode to let you -execute commands there until you use |gQ| to return to Ex mode. This will be -repeated for each matching line. While doing this you cannot use ":global". -To abort this type CTRL-C twice. - ============================================================================== Complex repeats *complex-repeat* diff --git a/runtime/doc/starting.txt b/runtime/doc/starting.txt index ba2e422bf8..0db5c37d5a 100644 --- a/runtime/doc/starting.txt +++ b/runtime/doc/starting.txt @@ -182,7 +182,7 @@ argument. changes and writing. -e *-e* *-E* --E Start Nvim in Ex mode |gQ|, see |Ex-mode|. +-E Start Nvim in |Ex-mode|. If stdin is not a TTY: • -e reads/executes stdin as Ex commands. @@ -209,6 +209,11 @@ argument. With |:verbose| or 'verbose', other commands display on stderr: > nvim -es +"verbose echo 'foo'" nvim -V1 -es +"echo 'foo'" +< + Commands that wait for typed input (|input()|, |getchar()|, …) + stop Nvim on EOF; they do not consume stdin. Lua can read + stdin as data: > + printf 'lua vim.g.b = io.read()\nfoo\nput =g:b\np' | nvim -es < Skips user |config| unless |-u| was given. Disables |shada| unless |-i| was given. diff --git a/runtime/doc/various.txt b/runtime/doc/various.txt index 8282e98f84..11fdb53f3e 100644 --- a/runtime/doc/various.txt +++ b/runtime/doc/various.txt @@ -232,8 +232,8 @@ gx Opens the current filepath, URL (decided by will be aborted as if or was typed. This implies that an insert command must be completed (to start Insert mode, see |:startinsert|). A ":" - command must be completed as well. And you can't use - "gQ" to start Ex mode. + command must be completed as well. And you can't + start |Ex-mode|. The display is not updated while ":normal" is busy. diff --git a/runtime/doc/vim_diff.txt b/runtime/doc/vim_diff.txt index 626683ff8f..b10f7bedc5 100644 --- a/runtime/doc/vim_diff.txt +++ b/runtime/doc/vim_diff.txt @@ -334,6 +334,7 @@ Commands: - |:uptime| Editor: +- Interactive |Ex-mode| is implemented as persistent, insert-mode |cmdwin|. - |prompt-buffer| supports multiline input/paste, undo/redo, and o/O normal commands. - |i_CTRL-R| inserts named/clipboard registers (A-Z,a-z,0-9+) literally, like @@ -482,10 +483,11 @@ Signs: Startup: - |-e| and |-es| invoke the same "improved Ex mode" as -E and -Es. - |-E| and |-Es| read stdin as text (into buffer 1). -- |-es| and |-Es| have improved behavior: - - Quits automatically, don't need "-c qa!". +- |-es| and |-Es| act as a generic "script mode": + - Exits automatically, don't need "-c qa!". - Skips swap-file dialog. - - Optimized for non-interactive scripts: disables swapfile, shada. + - Disables swapfile, shada. + - Various "POSIX Ex" quirks were dropped. - |-l| Executes Lua scripts non-interactively. - |-s| reads Normal commands from stdin if the script name is "-". - Reading text (instead of commands) from stdin |--|: diff --git a/runtime/doc/vimfn.txt b/runtime/doc/vimfn.txt index cc5e21c08c..781b44185a 100644 --- a/runtime/doc/vimfn.txt +++ b/runtime/doc/vimfn.txt @@ -4126,7 +4126,7 @@ getpos({expr}) *getpos()* 'x Position of mark x (if the mark is not set, 0 is returned for all values). w0 First line visible in current window (one if the - display isn't updated, e.g. in silent Ex mode). + display isn't updated, e.g. in |silent-mode|). w$ Last line visible in current window (this is one less than "w0" if no lines are visible). v End of the current Visual selection (unlike |'<| @@ -7329,8 +7329,7 @@ mode([{expr}]) *mode()* Rvx Virtual Replace mode |i_CTRL-X| completion c Command-line editing cr Command-line editing overstrike mode |c_| - cv Vim Ex mode |gQ| - cvr Vim Ex mode while in overstrike mode |c_| + cv Non-interactive Ex mode |-es| r Hit-enter prompt rm The -- more -- prompt r? A |:confirm| query of some sort diff --git a/runtime/lua/vim/_core/defaults.lua b/runtime/lua/vim/_core/defaults.lua index 720739f7c4..2bb5acba82 100644 --- a/runtime/lua/vim/_core/defaults.lua +++ b/runtime/lua/vim/_core/defaults.lua @@ -302,7 +302,7 @@ do local function cmd(opts) local ok, err = pcall(vim.api.nvim_cmd, opts, {}) if not ok then - vim.api.nvim_echo({ { err:sub(#'Vim:' + 1) } }, true, { err = true }) + vim.api.nvim_echo({ { require('vim._core.util').cmd_errmsg(err) } }, true, { err = true }) end end diff --git a/runtime/lua/vim/_core/exmode.lua b/runtime/lua/vim/_core/exmode.lua new file mode 100644 index 0000000000..c13a91ba9f --- /dev/null +++ b/runtime/lua/vim/_core/exmode.lua @@ -0,0 +1,223 @@ +--- Ex mode. +--- - `run()`: non-interactive (`nvim -es`): executes stdin as Ex commands. +--- - `open()`: interactive (`gQ`, `nvim -e`), a keep-open "cmdwin" REPL. + +local api = vim.api +local N_ = vim.fn.gettext +local util = require('vim._core.util') + +local M = {} + +--- Commands that end Ex mode before executing (in the caller window). +local exit_cmds = { + cquit = true, + exit = true, + quit = true, + quitall = true, + restart = true, + view = true, + visual = true, + wq = true, + wqall = true, + xall = true, + xit = true, +} + +--- Continuation lines (of a multiline cmd), for the statuscolumn. +--- @type table +local cont_lines = {} + +--- Non-interactive "script mode" ("nvim -es", fka "Ex-mode", but POSIX behavior was dropped): +--- Executes stdin as Ex commands, until EOF. +--- +--- Stdin is not consumed as typeahead: fd 0 is read directly (`io.lines()`), so `:lua` script lines +--- can also read stdin as data via `io.read()`. +function M.run() + local lines = io.lines() + local getline = function() + local line = lines() + -- Strip trailing CR so CRLF input (e.g. Windows pipes) works like LF. + return line and (line:gsub('\r$', '')) or nil + end + -- Instead of per-line `nvim_exec2`, this runs one `do_cmdline` per logical command with + -- `getline` as the line-getter, in order to preserve these behaviors: + -- - :append/:function/:if/heredocs pull continuation lines from stdin. #7679 + -- - Errors are emitted, not thrown (nvim_exec2 turns the first error into an exception): + -- execution continues with the next command, `ex_exitval` still sets the exit code, and + -- "-V1" shows errors on stderr. + -- - Output (:print, :set, …) to stdout as each command executes. + -- + -- TODO: could enhance nvim_exec2 instead? + while vim._core.ex_docmd(getline) do + vim.wait(0) -- Run pending events (vim.schedule() callbacks, RPC). + end +end + +--- Whether `lines` form a complete command block: no `:append` text, `:function` body, heredoc, or +--- :if/:while/:for/:try block is awaiting continuation lines. Feeds the lines to do_cmdline(), +--- which groups multiline constructs even when skipping (false `:if`), without executing anything. +--- If the construct is still open it consumes the sentinel "endif" and asks the getter for more. +--- @param lines string[] +--- @return boolean +local function block_complete(lines) + -- :append/etc. not grouped by the skipped-:if below: collect until the "." terminator. + local ok, p = pcall(api.nvim_parse_cmd, lines[1], {}) + if ok and (p.cmd == 'append' or p.cmd == 'insert' or p.cmd == 'change') then + return vim.list_contains({ unpack(lines, 2) }, '.') + end + + local feed = { 'if v:false' } + vim.list_extend(feed, lines) + feed[#feed + 1] = 'endif' + local i = 0 + local complete = true + -- Silenced: probing an incomplete block emits e.g. E126/E990 when the reader hits EOF. + vim._with({ silent = true, emsg_silent = true }, function() + vim._core.ex_docmd(function() + i = i + 1 + if i > #feed then + complete = false + return nil + end + return feed[i] + end) + end) + return complete +end + +--- Ex-mode 'statuscolumn': ":", or blank for continuation lines (`:func` body, `:append` text, …). +function M._statuscolumn() + return cont_lines[vim.v.lnum] and ' ' or ':' +end + +--- Interactive Ex-mode: keep-open cmdwin REPL. `` executes the current line against the caller +--- window and keeps the cmdwin open; command output is presented as comment ('"') lines. Multiline +--- commands (`:append`, etc.) are collected until the block is complete. +function M.open() + local cmdwin = require('vim._core.cmdwin') + local caller = api.nvim_get_current_win() + cmdwin.open(':') + if api.nvim_get_current_win() == caller then + return -- cmdwin failed to open (E1292 already echoed). + end + local buf = api.nvim_get_current_buf() + local block_start = api.nvim_buf_line_count(buf) + cont_lines = {} + vim.wo[0][0].statuscolumn = '%#NonText#%{v:lua.require("vim._core.exmode")._statuscolumn()}' + + --- Formats `text` (possibly multiline) as transcript comment lines. + --- @param out string[] + --- @param text string + local function put(out, text) + for _, l in ipairs(vim.split(text, '\n', { trimempty = true })) do + out[#out + 1] = '" ' .. l + end + end + + api.nvim_echo({ { N_('Entering Ex mode. Type "visual" to go to Normal mode.') } }, true, {}) + + local function run() + if not api.nvim_win_is_valid(caller) then + cmdwin._cleanup() -- Caller window is gone: end Ex mode. + return + end + local line = api.nvim_get_current_line() + local lnum = api.nvim_win_get_cursor(0)[1] + if lnum >= block_start then + -- Multiline commands: collect continuation lines until the block is complete. + local block = api.nvim_buf_get_lines(buf, block_start - 1, lnum, false) + if not block_complete(block) then + cont_lines[lnum + 1] = true -- Mark the continuation line. + api.nvim_buf_set_lines(buf, lnum, lnum, false, { '' }) + api.nvim_win_set_cursor(0, { lnum + 1, 0 }) + return + end + line = table.concat(block, '\n') + end + local parse_ok, parsed = pcall(api.nvim_parse_cmd, line:match('^[^\n]*'), {}) + if parse_ok and not line:find('\n') and exit_cmds[parsed.cmd] then + vim.fn.histadd('cmd', line) + cmdwin._cleanup() -- Focuses the caller window. + vim.cmd.stopinsert() + if parsed.cmd ~= 'visual' and parsed.cmd ~= 'view' or #parsed.args > 0 then + --- @type boolean, string? + local ok, err = pcall(vim.cmd --[[@as function]], line) -- May exit Nvim. + if not ok then + util.echo_err(util.cmd_errmsg(tostring(err))) + end + end + return + end + + local out = {} --- @type string[] + local prev_lnum = api.nvim_win_get_cursor(caller)[1] + local prev_tick = vim.b[api.nvim_win_get_buf(caller)].changedtick + if line == '' then + -- Empty line: advance one line (POSIX ex "+"); auto-print below shows it. + if prev_lnum >= api.nvim_buf_line_count(api.nvim_win_get_buf(caller)) then + put(out, N_('E501: At end-of-file')) + else + api.nvim_win_set_cursor(caller, { prev_lnum + 1, 0 }) + end + else + local ok, res = pcall(api.nvim_win_call, caller, function() + return api.nvim_exec2(line, { output = true }) + end) + if ok then + put(out, res.output) + else + put(out, util.cmd_errmsg(tostring(res))) + end + vim.fn.histadd('cmd', line) + end + + if not api.nvim_win_is_valid(caller) then + -- The command closed the caller window (e.g. ":close"): continue Ex mode against a + -- remaining window, or end it. + local wins = vim.tbl_filter( + --- @param w integer + function(w) + return api.nvim_win_get_buf(w) ~= buf + end, + api.nvim_list_wins() + ) + if #wins == 0 then + vim.cmd.quit() -- Only the cmdwin is left: exit. + return + end + caller = wins[1] + end + + -- Ex-mode feature: auto-print the current line after a cursor move or buffer change, unless the + -- cmd already printed something (e.g. ":5,6print" should not redundantly auto-print). + local typed_text = parse_ok + and (parsed.cmd == 'append' or parsed.cmd == 'insert' or parsed.cmd == 'change') + local cbuf = api.nvim_win_get_buf(caller) + local cur = api.nvim_win_get_cursor(caller)[1] + if + #out == 0 + and not typed_text + -- Bare range (":1") prints even when the cursor is already on line 1. + and ( + (parse_ok and parsed.cmd == '') + or cur ~= prev_lnum + or vim.b[cbuf].changedtick ~= prev_tick + ) + then + put(out, api.nvim_buf_get_lines(cbuf, cur - 1, cur, false)[1] or '') + end + + -- Keep-open: record the transcript and park the cursor on a fresh last line. + out[#out + 1] = '' + api.nvim_buf_set_lines(buf, -1, -1, false, out) + block_start = api.nvim_buf_line_count(buf) + api.nvim_win_set_cursor(0, { block_start, 0 }) + end + + vim.keymap.set({ 'n', 'i' }, '', run, { buffer = buf }) + vim.keymap.set({ 'n', 'i' }, '', run, { buffer = buf }) + -- Enter Insert mode before any already-typed keys, so `gQcmd` types "cmd" into the REPL. + api.nvim_feedkeys('i', 'ni', false) +end + +return M diff --git a/runtime/lua/vim/_core/server.lua b/runtime/lua/vim/_core/server.lua index e5e2737a7f..b94fb441f3 100644 --- a/runtime/lua/vim/_core/server.lua +++ b/runtime/lua/vim/_core/server.lua @@ -210,9 +210,8 @@ function M.ex_session_restart(eap, extra) vim.fs.rm(session, { force = true }) -- Trim error message to be equivalent to `:restart!` - local trimmed_msg = msg:match('Vim:.*$') - if trimmed_msg then - util.echo_err(trimmed_msg:sub(5)) + if msg:find('Vim:') then + util.echo_err(util.cmd_errmsg(msg)) else error(msg) end diff --git a/runtime/lua/vim/_core/util.lua b/runtime/lua/vim/_core/util.lua index 7bf4f53de7..adc5ce9253 100644 --- a/runtime/lua/vim/_core/util.lua +++ b/runtime/lua/vim/_core/util.lua @@ -166,6 +166,18 @@ function M.get_forge_url(repo, target, target_type) return ('%s/%s/%s'):format(repo, middle, target) end +--- Gets a scrubbed message from a pcall'd command error (drops Lua context/traceback): +--- "…/editor.lua:123: Vim(put):E484: xx" => "E484: xx" +--- +--- @param err string +--- @return string +function M.cmd_errmsg(err) + err = err:match('^[^\n]*') or err + --- @type string + err = err:match('Vim%b():%s*(.*)') or err:match('Vim:%s*(.*)') or (err:gsub('^.-:%d+:%s*', '')) + return (err:gsub('^Lua:%s*', '')) +end + --- Utility function for displaying vim error codes (EXX) --- @param msg string function M.echo_err(msg) diff --git a/runtime/lua/vim/_meta/builtin.lua b/runtime/lua/vim/_meta/builtin.lua index 999bc57dcf..fe45546acd 100644 --- a/runtime/lua/vim/_meta/builtin.lua +++ b/runtime/lua/vim/_meta/builtin.lua @@ -202,6 +202,13 @@ function vim._core.ui_flush() end --- @return boolean function vim._core.check_interrupt() end +--- @nodoc +--- Executes one Ex command line obtained from `getline`, which is also called for any +--- continuation lines (`:append` text, `:function` body, heredoc, …). See `vim._core.exmode`. +--- @param getline fun(): string? +--- @return boolean # false if {getline} returned nil before any line was read (EOF). +function vim._core.ex_docmd(getline) end + --- @nodoc --- Parses `keys` (internal representation) into a list of key chords. See |vim.keycode()|. --- @param keys string diff --git a/runtime/lua/vim/_meta/vimfn.gen.lua b/runtime/lua/vim/_meta/vimfn.gen.lua index 5bc52d2219..2f6305dcdc 100644 --- a/runtime/lua/vim/_meta/vimfn.gen.lua +++ b/runtime/lua/vim/_meta/vimfn.gen.lua @@ -3644,7 +3644,7 @@ function vim.fn.getpid() end --- 'x Position of mark x (if the mark is not set, 0 is --- returned for all values). --- w0 First line visible in current window (one if the ---- display isn't updated, e.g. in silent Ex mode). +--- display isn't updated, e.g. in |silent-mode|). --- w$ Last line visible in current window (this is one --- less than "w0" if no lines are visible). --- v End of the current Visual selection (unlike |'<| @@ -6573,8 +6573,7 @@ function vim.fn.mkdir(name, flags, prot) end --- Rvx Virtual Replace mode |i_CTRL-X| completion --- c Command-line editing --- cr Command-line editing overstrike mode |c_| ---- cv Vim Ex mode |gQ| ---- cvr Vim Ex mode while in overstrike mode |c_| +--- cv Non-interactive Ex mode |-es| --- r Hit-enter prompt --- rm The -- more -- prompt --- r? A |:confirm| query of some sort diff --git a/src/nvim/autocmd.c b/src/nvim/autocmd.c index 2faa727147..e2921f6978 100644 --- a/src/nvim/autocmd.c +++ b/src/nvim/autocmd.c @@ -1717,13 +1717,11 @@ bool apply_autocmds_group(event_T event, char *fname, char *fname_io, bool force } const int save_did_emsg = did_emsg; - const bool save_ex_pressedreturn = get_pressedreturn(); // Execute the autocmd. The `getnextac` callback handles iteration. do_cmdline(NULL, getnextac, &patcmd, DOCMD_NOWAIT | DOCMD_VERBOSE | DOCMD_REPEAT); did_emsg += save_did_emsg; - set_pressedreturn(save_ex_pressedreturn); if (nesting == 1) { // restore cursor and topline, unless they were changed diff --git a/src/nvim/bufwrite.c b/src/nvim/bufwrite.c index f646850416..5a5af1e9a0 100644 --- a/src/nvim/bufwrite.c +++ b/src/nvim/bufwrite.c @@ -1013,10 +1013,6 @@ int buf_write(buf_T *buf, char *fname, char *sfname, linenr_T start, linenr_T en write_info.bw_conv_error_lnum = 0; write_info.bw_iconv_fd = (iconv_t)-1; - // After writing a file changedtick changes but we don't want to display - // the line. - ex_no_reprint = true; - // If there is no file name yet, use the one for the written file. // BF_NOTEDITED is set to reflect this (in case the write fails). // Don't do this when the write is for a filter command. diff --git a/src/nvim/drawscreen.c b/src/nvim/drawscreen.c index 3e4fe575cf..0027ec0868 100644 --- a/src/nvim/drawscreen.c +++ b/src/nvim/drawscreen.c @@ -370,9 +370,8 @@ void screen_resize(int width, int height) // - While at the more prompt or executing an external command, don't // redraw, but position the cursor. // - While editing the command line, only redraw that. TODO: lies - // - in Ex mode, don't redraw anything. // - Otherwise, redraw right now, and position the cursor. - if (State == MODE_ASKMORE || State == MODE_EXTERNCMD || exmode_active + if (State == MODE_ASKMORE || State == MODE_EXTERNCMD || ((State & MODE_CMDLINE) && get_cmdline_info()->one_key)) { if (State & MODE_CMDLINE) { update_screen(); diff --git a/src/nvim/eval.c b/src/nvim/eval.c index c8df429c72..cd60391725 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -5022,7 +5022,6 @@ void timer_due_cb(TimeWatcher *tw, void *data) timer_T *timer = (timer_T *)data; int save_did_emsg = did_emsg; const int called_emsg_before = called_emsg; - const bool save_ex_pressedreturn = get_pressedreturn(); if (timer->stopped || timer->paused) { return; @@ -5049,7 +5048,6 @@ void timer_due_cb(TimeWatcher *tw, void *data) } } did_emsg = save_did_emsg; - set_pressedreturn(save_ex_pressedreturn); if (timer->emsg_count >= 3) { timer_stop(timer); diff --git a/src/nvim/eval.lua b/src/nvim/eval.lua index f95dc53190..a84928ab13 100644 --- a/src/nvim/eval.lua +++ b/src/nvim/eval.lua @@ -4487,7 +4487,7 @@ M.funcs = { 'x Position of mark x (if the mark is not set, 0 is returned for all values). w0 First line visible in current window (one if the - display isn't updated, e.g. in silent Ex mode). + display isn't updated, e.g. in |silent-mode|). w$ Last line visible in current window (this is one less than "w0" if no lines are visible). v End of the current Visual selection (unlike |'<| @@ -7943,8 +7943,7 @@ M.funcs = { Rvx Virtual Replace mode |i_CTRL-X| completion c Command-line editing cr Command-line editing overstrike mode |c_| - cv Vim Ex mode |gQ| - cvr Vim Ex mode while in overstrike mode |c_| + cv Non-interactive Ex mode |-es| r Hit-enter prompt rm The -- more -- prompt r? A |:confirm| query of some sort diff --git a/src/nvim/ex_cmds.c b/src/nvim/ex_cmds.c index 17455c5cdd..7bbe4f223c 100644 --- a/src/nvim/ex_cmds.c +++ b/src/nvim/ex_cmds.c @@ -2881,10 +2881,7 @@ int do_ecmd(int fnum, char *ffname, char *sfname, exarg_T *eap, linenr_T newlnum } else { beginline(BL_SOL | BL_FIX); } - } else { // no line number, go to last line in Ex mode - if (exmode_active) { - curwin->w_cursor.lnum = curbuf->b_ml.ml_line_count; - } + } else { beginline(BL_WHITE | BL_FIX); } } @@ -3001,6 +2998,7 @@ void ex_append(exarg_T *eap) lnum = 0; } + const int save_State = State; State = MODE_INSERT; // behave like in Insert mode if (curbuf->b_p_iminsert == B_IMODE_LMAP) { State |= MODE_LANGMAP; @@ -3039,13 +3037,13 @@ void ex_append(exarg_T *eap) } eap->nextcmd = p; } else { - int save_State = State; + int getline_State = State; // Set State to avoid the cursor shape to be set to MODE_INSERT // state when getline() returns. State = MODE_CMDLINE; theline = eap->ea_getline(eap->cstack->cs_looplevel > 0 ? -1 : NUL, eap->cookie, indent, true); - State = save_State; + State = getline_State; } lines_left = Rows - 1; if (theline == NULL) { @@ -3092,7 +3090,7 @@ void ex_append(exarg_T *eap) empty = false; } } - State = MODE_NORMAL; + State = save_State; ui_cursor_shape(); if (eap->forceit) { @@ -3117,7 +3115,6 @@ void ex_append(exarg_T *eap) beginline(BL_SOL | BL_FIX); need_wait_return = false; // don't use wait_return() now - ex_no_reprint = true; } /// ":change" @@ -3268,7 +3265,6 @@ void ex_z(exarg_T *eap) curwin->w_cursor.lnum = curs; curwin->w_cursor.col = 0; } - ex_no_reprint = true; } /// @return true if the secure flag is set and also give an error message. @@ -3946,111 +3942,72 @@ static int do_sub(exarg_T *eap, proftime_T tm, const int cmdpreview_ns, // Loop until 'y', 'n', 'q', CTRL-E or CTRL-Y typed. while (subflags.do_ask) { - if (exmode_active) { - print_line_no_prefix(lnum, subflags.do_number, subflags.do_list); + String orig_line = STRING_INIT; + int len_change = 0; + const bool save_p_lz = p_lz; + int save_p_fen = curwin->w_p_fen; - colnr_T sc, ec; - getvcol(curwin, &curwin->w_cursor, &sc, NULL, NULL, 0); - curwin->w_cursor.col = MAX(regmatch.endpos[0].col - 1, 0); + curwin->w_p_fen = false; + // Invert the matched string. + // Remove the inversion afterwards. + int temp = RedrawingDisabled; + RedrawingDisabled = 0; - getvcol(curwin, &curwin->w_cursor, NULL, NULL, &ec, 0); - curwin->w_cursor.col = regmatch.startpos[0].col; - if (subflags.do_number || curwin->w_p_nu) { - int numw = number_width(curwin) + 1; - sc += numw; - ec += numw; - } + // avoid calling update_screen() in vgetorpeek() + p_lz = false; - char *prompt = xmallocz((size_t)ec + 1); - memset(prompt, ' ', (size_t)sc); - memset(prompt + sc, '^', (size_t)(ec - sc) + 1); - char *resp = getcmdline_prompt(-1, prompt, 0, EXPAND_NOTHING, NULL, - CALLBACK_NONE, false, NULL); - if (!ui_has(kUIMessages)) { - msg_putchar('\n'); - } - xfree(prompt); - if (resp != NULL) { - typed = (uint8_t)(*resp); - xfree(resp); - } else { - // getcmdline_prompt() returns NULL if there is no command line to return. - typed = NUL; - } - // When ":normal" runs out of characters we get - // an empty line. Use "q" to get out of the - // loop. - if (ex_normal_busy && typed == NUL) { - typed = 'q'; - } - } else { - String orig_line = STRING_INIT; - int len_change = 0; - const bool save_p_lz = p_lz; - int save_p_fen = curwin->w_p_fen; + if (new_start.data != NULL) { + // There already was a substitution, we would + // like to show this to the user. We cannot + // really update the line, it would change + // what matches. Temporarily replace the line + // and change it back afterwards. + orig_line = cbuf_to_string(ml_get(lnum), (size_t)ml_get_len(lnum)); + String new_line = { + .data = concat_str(new_start.data, sub_firstline.data + copycol), + .size = new_start.size + (sub_firstline.size - (size_t)copycol), + }; - curwin->w_p_fen = false; - // Invert the matched string. - // Remove the inversion afterwards. - int temp = RedrawingDisabled; - RedrawingDisabled = 0; + // Position the cursor relative to the end of the line, the + // previous substitute may have inserted or deleted characters + // before the cursor. + len_change = (int)new_line.size - (int)orig_line.size; + curwin->w_cursor.col += len_change; + ml_replace(lnum, new_line.data, false); + } - // avoid calling update_screen() in vgetorpeek() - p_lz = false; + Search.match_lines = regmatch.endpos[0].lnum - regmatch.startpos[0].lnum; + Search.match_endcol = regmatch.endpos[0].col + len_change; + if (Search.match_lines == 0 && Search.match_endcol == 0) { + // highlight at least one character for /^/ + Search.match_endcol = 1; + } + Search.hl_match = true; - if (new_start.data != NULL) { - // There already was a substitution, we would - // like to show this to the user. We cannot - // really update the line, it would change - // what matches. Temporarily replace the line - // and change it back afterwards. - orig_line = cbuf_to_string(ml_get(lnum), (size_t)ml_get_len(lnum)); - String new_line = { - .data = concat_str(new_start.data, sub_firstline.data + copycol), - .size = new_start.size + (sub_firstline.size - (size_t)copycol), - }; + update_topline(curwin); + validate_cursor(curwin); + redraw_later(curwin, UPD_SOME_VALID); + show_cursor_info_later(true); + update_screen(); + redraw_later(curwin, UPD_SOME_VALID); - // Position the cursor relative to the end of the line, the - // previous substitute may have inserted or deleted characters - // before the cursor. - len_change = (int)new_line.size - (int)orig_line.size; - curwin->w_cursor.col += len_change; - ml_replace(lnum, new_line.data, false); - } + curwin->w_p_fen = save_p_fen; - Search.match_lines = regmatch.endpos[0].lnum - regmatch.startpos[0].lnum; - Search.match_endcol = regmatch.endpos[0].col + len_change; - if (Search.match_lines == 0 && Search.match_endcol == 0) { - // highlight at least one character for /^/ - Search.match_endcol = 1; - } - Search.hl_match = true; + char *p = _("replace with %s? (y)es/(n)o/(a)ll/(q)uit/(l)ast/scroll up(^E)/down(^Y)"); + snprintf(IObuff, IOSIZE, p, sub); + p = xstrdup(IObuff); + typed = prompt_for_input(p, HLF_R, true, NULL); + Search.hl_match = false; + xfree(p); - update_topline(curwin); - validate_cursor(curwin); - redraw_later(curwin, UPD_SOME_VALID); - show_cursor_info_later(true); - update_screen(); - redraw_later(curwin, UPD_SOME_VALID); + msg_didout = false; // don't scroll up + gotocmdline(true); + p_lz = save_p_lz; + RedrawingDisabled = temp; - curwin->w_p_fen = save_p_fen; - - char *p = _("replace with %s? (y)es/(n)o/(a)ll/(q)uit/(l)ast/scroll up(^E)/down(^Y)"); - snprintf(IObuff, IOSIZE, p, sub); - p = xstrdup(IObuff); - typed = prompt_for_input(p, HLF_R, true, NULL); - Search.hl_match = false; - xfree(p); - - msg_didout = false; // don't scroll up - gotocmdline(true); - p_lz = save_p_lz; - RedrawingDisabled = temp; - - // restore the line - if (orig_line.data != NULL) { - ml_replace(lnum, orig_line.data, false); - } + // restore the line + if (orig_line.data != NULL) { + ml_replace(lnum, orig_line.data, false); } need_wait_return = false; // no hit-return prompt diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index aa0a8caf3b..816614f26c 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -145,7 +145,6 @@ static const char e_no_script_file_name_to_substitute_for_script[] = N_("E1274: No script file name to substitute for \"