From fd1b193d518b8ff45f47e76b21afae7b902e8a55 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Wed, 22 Apr 2026 14:58:47 -0400 Subject: [PATCH] feat(:restart): v:starttime, v:exitreason #39319 --- runtime/doc/gui.txt | 18 ++--- runtime/doc/news.txt | 2 + runtime/doc/starting.txt | 38 ++++++----- runtime/doc/vvars.txt | 37 ++++++++-- runtime/lua/vim/_core/server.lua | 4 +- runtime/lua/vim/_meta/vvars.gen.lua | 43 ++++++++++-- src/nvim/eval/userfunc.c | 2 +- src/nvim/eval/vars.c | 2 + src/nvim/eval_defs.h | 2 + src/nvim/ex_docmd.c | 48 +++++++------ src/nvim/main.c | 8 ++- src/nvim/vvars.lua | 41 +++++++++-- test/client/uv_stream.lua | 6 +- test/functional/core/exit_spec.lua | 32 ++++++--- test/functional/core/main_spec.lua | 2 +- test/functional/terminal/tui_spec.lua | 97 +++++++++++++++++---------- 16 files changed, 265 insertions(+), 117 deletions(-) diff --git a/runtime/doc/gui.txt b/runtime/doc/gui.txt index 63da98d72f..2453fe605f 100644 --- a/runtime/doc/gui.txt +++ b/runtime/doc/gui.txt @@ -59,17 +59,16 @@ Stop or detach the current UI >vim :echo v:servername < - Note: The server closes the UI RPC channel, so :detach - inherently "works" for all UIs. But if a UI isn't expecting - the channel to be closed, it may be (incorrectly) reported as - an error. + Note: The server closes the UI RPC channel, so :detach works + for any UI client. But if the client isn't expecting this, it + may (incorrectly) report an error. ------------------------------------------------------------------------------ Restart Nvim *:restart* :restart [+cmd] [command] - Restarts Nvim. See also |ZR|. + Restarts Nvim. Sets |v:exitreason|. See also |ZR|. 1. Stops Nvim using `:qall` (or |+cmd|, if given). 2. Starts a new Nvim server using the same |v:argv| (except @@ -84,9 +83,12 @@ Restart Nvim < Example: restart and update plugins: > :restart lua vim.pack.update() < - Note: Only works if the UI and server are on the same system. - Note: If no attached UI implements the "restart" UI event, - this command will lead to a dangling server process. + Note: + • Only works if the UI and server are on the same system. + • Windows limitation: when +cmd is executed, |v:servername| + refers to a temporary address. + • If no UI handles the "restart" event, this command will lead + to a dangling server process. ------------------------------------------------------------------------------ Connect UI to a different server diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index c1082516e8..c8353cde0b 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -440,6 +440,8 @@ VIMSCRIPT • |wildtrigger()| triggers command-line expansion. • |v:vim_did_init| is set after sourcing |init.vim| but before |load-plugins|. • |prompt_appendbuf()| appends text to prompt-buffer. +• |v:exitreason| is set before |QuitPre|. +• |v:starttime| is the process start time (monotonic nanoseconds). ============================================================================== CHANGED FEATURES *news-changed* diff --git a/runtime/doc/starting.txt b/runtime/doc/starting.txt index 19db9c9069..5139b67cd5 100644 --- a/runtime/doc/starting.txt +++ b/runtime/doc/starting.txt @@ -434,30 +434,32 @@ Initialization *initialization* *startup* At startup, Nvim checks environment variables and files and sets values accordingly, proceeding as follows: -1. Set the 'shell' option. *SHELL* *COMSPEC* +1. Set |v:starttime|. + +2. Set the 'shell' option. *SHELL* *COMSPEC* The environment variable SHELL, if it exists, is used to set the 'shell' option. On Win32, the COMSPEC variable is used if SHELL is not set. -2. Process the arguments. +3. Process the arguments. The options and file names from the command that start Vim are inspected. The |-V| argument can be used to display or log what happens next, useful for debugging the initializations. The |--cmd| arguments are executed. Buffers are created for all files (but not loaded yet). -3. Start a server (unless |--listen| was given) and set |v:servername|. +4. Start a server (unless |--listen| was given) and set |v:servername|. -4. Wait for UI to connect. +5. Wait for UI to connect. Nvim started with |--embed| waits for the UI to connect before proceeding to load user configuration. -5. Setup |default-mappings| and |default-autocmds|. Create |popup-menu|. +6. Setup |default-mappings| and |default-autocmds|. Create |popup-menu|. -6. Enable filetype and indent plugins. +7. Enable filetype and indent plugins. Skipped if the "-u NONE" command line argument was given. This does the same as: > :runtime! ftplugin.vim indent.vim -7. Load user config (execute Ex commands from files, environment, …). +8. Load user config (execute Ex commands from files, environment, …). $VIMINIT environment variable is read as one Ex command line (separate multiple commands with '|' or ). *config* *init.vim* *init.lua* *vimrc* *exrc* @@ -499,19 +501,19 @@ accordingly, proceeding as follows: - ".nvimrc" - ".exrc" -8. Enable filetype detection. +9. Enable filetype detection. Skipped if ":filetype off" was called or if the "-u NONE" command line argument was given. This does the same as: > :runtime! filetype.lua -9. Enable syntax highlighting. +10. Enable syntax highlighting. Skipped if ":syntax off" was called or if the "-u NONE" command line argument was given. This does the same as: > :runtime! syntax/syntax.vim -10. Set the |v:vim_did_init| variable to 1. +11. Set the |v:vim_did_init| variable to 1. -11. Load the plugin scripts. *load-plugins* +12. Load the plugin scripts. *load-plugins* This does the same as: `:runtime! plugin/**/*.{vim,lua}` - The result is that all directories in 'runtimepath' will be searched for @@ -538,26 +540,26 @@ accordingly, proceeding as follows: if packages have been found, but that should not add a directory ending in "after". -12. Set 'shellpipe' and 'shellredir', according to the value of the 'shell' +13. Set 'shellpipe' and 'shellredir', according to the value of the 'shell' option, unless they were already set. Nvim will figure out the values of 'shellpipe' and 'shellredir' for you, unless you have set them yourself. -13. Set 'updatecount' to zero, if |-n| was given. +14. Set 'updatecount' to zero, if |-n| was given. -14. Set binary options if |-b| was given. +15. Set binary options if |-b| was given. -15. Read the |shada-file|. +16. Read the |shada-file|. -16. Read the quickfix file if |-q| was given, or exit on failure. +17. Read the quickfix file if |-q| was given, or exit on failure. -17. Open all windows. +18. Open all windows. - When |-o| was given, windows will be opened (but not displayed yet). - When |-p| was given, tab pages will be created (but not displayed yet). - When switching screens, it happens now. Redrawing starts. - If |-q| was given, the first error is jumped to. - Buffers for all windows will be loaded, without triggering |BufAdd|. -18. Execute startup commands. +19. Execute startup commands. - If |-t| was given, the tag is jumped to. - Commands given with |-c| and |+cmd| are executed. - The |vim_starting| flag is reset, `has("vim_starting")` will now return zero. diff --git a/runtime/doc/vvars.txt b/runtime/doc/vvars.txt index d598a00414..6a885f6a4f 100644 --- a/runtime/doc/vvars.txt +++ b/runtime/doc/vvars.txt @@ -230,9 +230,24 @@ v:exiting Exit code, or |v:null| before invoking the |VimLeavePre| and |VimLeave| autocmds. See |:q|, |:x| and |:cquit|. Example: >vim - :au VimLeave * echo "Exit value is " .. v:exiting + :au VimLeave * echo "Exit code is " .. v:exiting < + *v:exitreason* *exitreason-variable* +v:exitreason + Reason for the current exit. Set before |QuitPre|. Reset if + exit was canceled. + + Possible values: + - "" Not exiting, or exit was canceled. + - "quit" |:quit|, |:qall|, |:wq|, |ZZ|, |ZQ|, etc. + - "restart" |:restart|, |ZR|. + + Example: >vim + autocmd ExitPre * if v:exitreason ==# 'restart' | echomsg 'restarting' | endif +< + Read-only. + *v:false* *false-variable* v:false Special value used to put "false" in JSON and msgpack. See @@ -267,12 +282,12 @@ v:fcs_reason The reason why the |FileChangedShell| event was triggered. Can be used in an autocommand to decide what to do and/or what to set v:fcs_choice to. Possible values: - deleted file no longer exists - conflict file contents, mode or timestamp was + - deleted file no longer exists + - conflict file contents, mode or timestamp was changed and buffer is modified - changed file contents has changed - mode mode of file changed - time only file timestamp changed + - changed file contents has changed + - mode mode of file changed + - time only file timestamp changed *v:fname* *fname-variable* v:fname @@ -618,6 +633,16 @@ v:stacktrace stack trace. See also |v:exception|, |v:throwpoint|, and |throw-variables|. + *v:starttime* *starttime-variable* +v:starttime + Timestamp (monotonic nanoseconds) when the Nvim process + started. + + To see the current "uptime": >lua + vim.print(('uptime: %d seconds'):format((vim.uv.hrtime() - vim.v.starttime) / 1e9)) +< + Read-only. + *v:statusmsg* *statusmsg-variable* v:statusmsg Last given status message. diff --git a/runtime/lua/vim/_core/server.lua b/runtime/lua/vim/_core/server.lua index 77575dd255..3b05142a33 100644 --- a/runtime/lua/vim/_core/server.lua +++ b/runtime/lua/vim/_core/server.lua @@ -42,10 +42,10 @@ M.restart_canonical_addr = nil ---@type string? --- Windows named pipes can't be rebound immediately, so the new server starts on a --- temporary bootstrap address and polls until the canonical address is reclaimable. --- @param canonical_addr string The original --listen address to reclaim. ---- @param bootstrap_addr string Temporary address the new server started on. --- @param expected_uis integer Number of UIs expected to reattach (0 = don't wait). -function M.rebind_old_addr_after_restart(canonical_addr, bootstrap_addr, expected_uis) +function M.rebind_after_restart(canonical_addr, expected_uis) M.restart_canonical_addr = canonical_addr + local bootstrap_addr = vim.v.servername -- Temporary autogenerated address. local poll_ms = 50 local max_wait_ms = 30000 local timer = assert(vim.uv.new_timer()) diff --git a/runtime/lua/vim/_meta/vvars.gen.lua b/runtime/lua/vim/_meta/vvars.gen.lua index 90cece6b8c..7b5ca0e417 100644 --- a/runtime/lua/vim/_meta/vvars.gen.lua +++ b/runtime/lua/vim/_meta/vvars.gen.lua @@ -244,11 +244,29 @@ vim.v.exception = ... --- Example: --- --- ```vim ---- :au VimLeave * echo "Exit value is " .. v:exiting +--- :au VimLeave * echo "Exit code is " .. v:exiting --- ``` --- @type integer? vim.v.exiting = ... +--- Reason for the current exit. Set before `QuitPre`. Reset if +--- exit was canceled. +--- +--- Possible values: +--- - "" Not exiting, or exit was canceled. +--- - "quit" `:quit`, `:qall`, `:wq`, `ZZ`, `ZQ`, etc. +--- - "restart" `:restart`, `ZR`. +--- +--- Example: +--- +--- ```vim +--- autocmd ExitPre * if v:exitreason ==# 'restart' | echomsg 'restarting' | endif +--- ``` +--- +--- Read-only. +--- @type string +vim.v.exitreason = ... + --- Special value used to put "false" in JSON and msgpack. See --- `json_encode()`. This value is converted to "v:false" when used --- as a String (e.g. in `expr5` with string concatenation @@ -281,12 +299,12 @@ vim.v.fcs_choice = ... --- The reason why the `FileChangedShell` event was triggered. --- Can be used in an autocommand to decide what to do and/or what --- to set v:fcs_choice to. Possible values: ---- deleted file no longer exists ---- conflict file contents, mode or timestamp was +--- - deleted file no longer exists +--- - conflict file contents, mode or timestamp was --- changed and buffer is modified ---- changed file contents has changed ---- mode mode of file changed ---- time only file timestamp changed +--- - changed file contents has changed +--- - mode mode of file changed +--- - time only file timestamp changed --- @type string vim.v.fcs_reason = ... @@ -649,6 +667,19 @@ vim.v.shell_error = ... --- @type table[] vim.v.stacktrace = ... +--- Timestamp (monotonic nanoseconds) when the Nvim process +--- started. +--- +--- To see the current "uptime": +--- +--- ```lua +--- vim.print(('uptime: %d seconds'):format((vim.uv.hrtime() - vim.v.starttime) / 1e9)) +--- ``` +--- +--- Read-only. +--- @type integer +vim.v.starttime = ... + --- Last given status message. --- Modifiable (can be set). --- @type string diff --git a/src/nvim/eval/userfunc.c b/src/nvim/eval/userfunc.c index 842abee93c..cf174cb8d9 100644 --- a/src/nvim/eval/userfunc.c +++ b/src/nvim/eval/userfunc.c @@ -3520,7 +3520,7 @@ static void handle_defer_one(funccall_T *funccal) ga_clear(&funccal->fc_defer); } -/// Called when exiting: call all defer functions. +/// When exiting: call all ":defer" functions. void invoke_all_defer(void) { for (funccall_T *fc = current_funccal; fc != NULL; fc = fc->fc_caller) { diff --git a/src/nvim/eval/vars.c b/src/nvim/eval/vars.c index d4b7becb46..803b121133 100644 --- a/src/nvim/eval/vars.c +++ b/src/nvim/eval/vars.c @@ -215,6 +215,8 @@ static struct vimvar { VV(VV_LUA, "lua", VAR_PARTIAL, VV_RO), VV(VV_RELNUM, "relnum", VAR_NUMBER, VV_RO), VV(VV_VIRTNUM, "virtnum", VAR_NUMBER, VV_RO), + VV(VV_STARTTIME, "starttime", VAR_NUMBER, VV_RO), + VV(VV_EXITREASON, "exitreason", VAR_STRING, VV_RO), }; #undef VV diff --git a/src/nvim/eval_defs.h b/src/nvim/eval_defs.h index 365bddb00b..7cec3d6167 100644 --- a/src/nvim/eval_defs.h +++ b/src/nvim/eval_defs.h @@ -135,4 +135,6 @@ typedef enum { VV_LUA, VV_RELNUM, VV_VIRTNUM, + VV_STARTTIME, + VV_EXITREASON, } VimVarIndex; diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index cae697f74e..d3f3288246 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -4818,10 +4818,15 @@ static void ex_highlight(exarg_T *eap) void not_exiting(bool save_exiting) { exiting = save_exiting; + set_vim_var_string(VV_EXITREASON, NULL, -1); } bool before_quit_autocmds(win_T *wp, bool quit_all, bool forceit) { + // Set v:exitreason if not already set (e.g. by :restart). + if (*get_vim_var_str(VV_EXITREASON) == NUL) { + set_vim_var_string(VV_EXITREASON, S_LEN("quit")); + } apply_autocmds(EVENT_QUITPRE, NULL, NULL, false, wp->w_buffer); // Bail out when autocommands closed the window. @@ -4830,6 +4835,7 @@ bool before_quit_autocmds(win_T *wp, bool quit_all, bool forceit) if (!win_valid(wp) || curbuf_locked() || (wp->w_buffer->b_nwindows == 1 && wp->w_buffer->b_locked > 0)) { + set_vim_var_string(VV_EXITREASON, NULL, -1); return true; } @@ -4842,6 +4848,7 @@ bool before_quit_autocmds(win_T *wp, bool quit_all, bool forceit) if (!win_valid(wp) || curbuf_locked() || (curbuf->b_nwindows == 1 && curbuf->b_locked > 0)) { + set_vim_var_string(VV_EXITREASON, NULL, -1); return true; } } @@ -4976,15 +4983,10 @@ static void ex_restart(exarg_T *eap) char **argv = xcalloc((size_t)argc + 3, sizeof(char *)); size_t i = 0; - const char *listen_arg = NULL; -#ifdef MSWIN - // On Windows, don't pass --listen to new server (named pipe can't be reused immediately). - // Instead pass the address via RPC, and new server will rebind to it after startup. -# define HANDLE_LISTEN_ADDR listen_arg = addr; li = next_li; continue -#else -# define HANDLE_LISTEN_ADDR listen_arg = addr -#endif - TV_LIST_ITER_CONST(l, li, { + const char *listen_arg = NULL; // --listen arg given by user, if any. + + // Build args to start the new Nvim, based on the current v:argv. + for (const listitem_T *li = l->lv_first; li != NULL; li = li->li_next) { const char *arg = tv_get_string(TV_LIST_ITEM_TV(li)); // Drop "-- [files…]". Usually isn't wanted. User can :mksession instead. if (i > 0 && strequal(arg, "--")) { @@ -4992,16 +4994,22 @@ static void ex_restart(exarg_T *eap) } // Drop "-s ": skip the scriptfile arg too. if (i > 0 && strequal(arg, "-s")) { - li = TV_LIST_ITEM_NEXT(l, li); + li = li->li_next; continue; } // The address after --listen may be in use by the current server. if (i > 0 && strequal(arg, "--listen")) { - listitem_T *next_li = TV_LIST_ITEM_NEXT(l, li); + const listitem_T *next_li = li->li_next; if (next_li != NULL) { const char *addr = tv_get_string(TV_LIST_ITEM_TV(next_li)); if (strstr(addr, ":") || strstr(addr, "/") || strstr(addr, "\\")) { - HANDLE_LISTEN_ADDR; + listen_arg = addr; +#ifdef MSWIN + // On Windows, don't pass --listen to new server (named pipe can't be reused immediately). + // Instead pass the address via RPC; new server rebinds after startup. + li = next_li; + continue; +#endif } } } @@ -5019,12 +5027,11 @@ static void ex_restart(exarg_T *eap) } } } - }); -#undef HANDLE_LISTEN_ADDR + } #ifdef MSWIN // On Windows, --listen is omitted from child argv because the named pipe can't be reused immediately. - // Recover the canonical address from the Lua module state (set by the previous rebind_old_addr_after_restart() call), + // Recover the canonical address from the Lua module state (set by the previous rebind_after_restart() call), // and keep the current listener alive (new server reclaims it). char *listen_arg_alloc = NULL; if (listen_arg == NULL) { @@ -5104,8 +5111,7 @@ static void ex_restart(exarg_T *eap) result_mem = NULL; } - // Get the new server's initial listen address. On Windows this is the - // temporary bootstrap address that UIs should reconnect to first. + // Get the new server's initial address. On Windows this is the temporary self-generated address. MAXSIZE_TEMP_ARRAY(servername_args, 1); ADD_C(servername_args, CSTR_AS_OBJ("servername")); Object result = rpc_send_call(channel->id, "nvim_get_vvar", servername_args, &result_mem, &err); @@ -5116,6 +5122,7 @@ static void ex_restart(exarg_T *eap) emsg("restart failed: could not get listen address from new server"); goto fail_2; } + // New server's self-generated address. char *listen_addr = xmemdupz(result.data.string.data, result.data.string.size); arena_mem_free(result_mem); result_mem = NULL; @@ -5126,10 +5133,9 @@ static void ex_restart(exarg_T *eap) // then retire the bootstrap address after all UIs have reattached (or timeout). MAXSIZE_TEMP_ARRAY(lua_args, 2); ADD_C(lua_args, - CSTR_AS_OBJ("return require('vim._core.server').rebind_old_addr_after_restart(...)")); + CSTR_AS_OBJ("return require('vim._core.server').rebind_after_restart(...)")); MAXSIZE_TEMP_ARRAY(handoff_params, 3); ADD_C(handoff_params, CSTR_AS_OBJ(listen_arg)); - ADD_C(handoff_params, CSTR_AS_OBJ(listen_addr)); ADD_C(handoff_params, INTEGER_OBJ((Integer)ui_active())); ADD_C(lua_args, ARRAY_OBJ(handoff_params)); rpc_send_call(channel->id, "nvim_exec_lua", lua_args, &result_mem, &err); @@ -5146,6 +5152,8 @@ static void ex_restart(exarg_T *eap) ui_flush(); xfree(listen_addr); + set_vim_var_string(VV_EXITREASON, S_LEN("restart")); + char *quit_cmd = (eap->do_ecmd_cmd) ? eap->do_ecmd_cmd : "qall"; char *quit_cmd_copy = NULL; @@ -5154,6 +5162,7 @@ static void ex_restart(exarg_T *eap) quit_cmd_copy = concat_str("confirm ", quit_cmd); quit_cmd = quit_cmd_copy; } + // Try to quit. nvim_command(cstr_as_string(quit_cmd), &err); xfree(quit_cmd_copy); @@ -5165,6 +5174,7 @@ static void ex_restart(exarg_T *eap) } fail_2: + set_vim_var_string(VV_EXITREASON, NULL, -1); if (ERROR_SET(&err)) { emsg(err.msg); api_clear_error(&err); diff --git a/src/nvim/main.c b/src/nvim/main.c index 5e64681cb5..02cfc59849 100644 --- a/src/nvim/main.c +++ b/src/nvim/main.c @@ -197,6 +197,7 @@ void early_init(mparm_T *paramp) estack_init(); cmdline_init(); eval_init(); // init global variables + set_vim_var_nr(VV_STARTTIME, (varnumber_T)os_hrtime()); init_path(argv0 ? argv0 : "nvim"); init_normal_cmds(); // Init the table of Normal mode commands. runtime_init(); @@ -763,7 +764,12 @@ void getout(int exitval) set_vim_var_type(VV_EXITING, VAR_NUMBER); set_vim_var_nr(VV_EXITING, exitval); - // Invoked all deferred functions in the function stack. + // Set v:exitreason if not already set (e.g. by :restart). + if (*get_vim_var_str(VV_EXITREASON) == NUL) { + set_vim_var_string(VV_EXITREASON, S_LEN("quit")); + } + + // Invoked all ":defer" functions in the function stack. invoke_all_defer(); // Optionally print hashtable efficiency. diff --git a/src/nvim/vvars.lua b/src/nvim/vvars.lua index 26a62830d1..648e5f1310 100644 --- a/src/nvim/vvars.lua +++ b/src/nvim/vvars.lua @@ -268,10 +268,27 @@ M.vars = { Exit code, or |v:null| before invoking the |VimLeavePre| and |VimLeave| autocmds. See |:q|, |:x| and |:cquit|. Example: >vim - :au VimLeave * echo "Exit value is " .. v:exiting + :au VimLeave * echo "Exit code is " .. v:exiting < ]=], }, + exitreason = { + type = 'string', + desc = [=[ + Reason for the current exit. Set before |QuitPre|. Reset if + exit was canceled. + + Possible values: + - "" Not exiting, or exit was canceled. + - "quit" |:quit|, |:qall|, |:wq|, |ZZ|, |ZQ|, etc. + - "restart" |:restart|, |ZR|. + + Example: >vim + autocmd ExitPre * if v:exitreason ==# 'restart' | echomsg 'restarting' | endif + < + Read-only. + ]=], + }, fcs_choice = { type = 'string', desc = [=[ @@ -301,12 +318,12 @@ M.vars = { The reason why the |FileChangedShell| event was triggered. Can be used in an autocommand to decide what to do and/or what to set v:fcs_choice to. Possible values: - deleted file no longer exists - conflict file contents, mode or timestamp was + - deleted file no longer exists + - conflict file contents, mode or timestamp was changed and buffer is modified - changed file contents has changed - mode mode of file changed - time only file timestamp changed + - changed file contents has changed + - mode mode of file changed + - time only file timestamp changed ]=], }, fname = { @@ -736,6 +753,18 @@ M.vars = { |throw-variables|. ]=], }, + starttime = { + type = 'integer', + desc = [=[ + Timestamp (monotonic nanoseconds) when the Nvim process + started. + + To see the current "uptime": >lua + vim.print(('uptime: %d seconds'):format((vim.uv.hrtime() - vim.v.starttime) / 1e9)) + < + Read-only. + ]=], + }, statusmsg = { type = 'string', desc = [=[ diff --git a/test/client/uv_stream.lua b/test/client/uv_stream.lua index 464f52a609..a8f83b4eeb 100644 --- a/test/client/uv_stream.lua +++ b/test/client/uv_stream.lua @@ -97,7 +97,7 @@ function SocketStream:write(data) end uv.write(self._socket, data, function(err) if err then - error(self._stream_error or err) + self._stream_error = self._stream_error or err end end) end @@ -108,7 +108,9 @@ function SocketStream:read_start(cb) end uv.read_start(self._socket, function(err, chunk) if err then - error(err) + self._stream_error = self._stream_error or err + cb(nil) -- Signal EOF so the session layer stops. + return end cb(chunk) end) diff --git a/test/functional/core/exit_spec.lua b/test/functional/core/exit_spec.lua index 3500cd2919..852d9fd1a2 100644 --- a/test/functional/core/exit_spec.lua +++ b/test/functional/core/exit_spec.lua @@ -12,7 +12,7 @@ local pcall_err = t.pcall_err local exec_capture = n.exec_capture local poke_eventloop = n.poke_eventloop -describe('v:exiting', function() +describe('exit:', function() local cid before_each(function() @@ -20,39 +20,51 @@ describe('v:exiting', function() cid = n.api.nvim_get_chan_info(0).id end) - it('defaults to v:null', function() + it('v:exiting defaults to v:null', function() eq(1, eval('v:exiting is v:null')) + eq('', eval('v:exitreason')) end) local function test_exiting(setup_fn) local function on_setup() - command('autocmd VimLeavePre * call rpcrequest(' .. cid .. ', "exit", "VimLeavePre")') - command('autocmd VimLeave * call rpcrequest(' .. cid .. ', "exit", "VimLeave")') + command(('autocmd QuitPre * call rpcrequest(%d, "exit", "QuitPre")'):format(cid)) + command(('autocmd ExitPre * call rpcrequest(%d, "exit", "ExitPre")'):format(cid)) + command(('autocmd VimLeavePre * call rpcrequest(%d, "exit", "VimLeavePre")'):format(cid)) + command(('autocmd VimLeave * call rpcrequest(%d, "exit", "VimLeave")'):format(cid)) setup_fn() end - local requests_args = {} + local received = {} local function on_request(name, args) eq('exit', name) - table.insert(requests_args, args) - eq(0, eval('v:exiting')) + table.insert(received, args) + eq('quit', eval('v:exitreason')) + if args[1] == 'VimLeavePre' or args[1] == 'VimLeave' then + eq(0, eval('v:exiting')) + end return '' end run(on_request, nil, on_setup) - eq({ { 'VimLeavePre' }, { 'VimLeave' } }, requests_args) + eq({ { 'QuitPre' }, { 'ExitPre' }, { 'VimLeavePre' }, { 'VimLeave' } }, received) end - it('is 0 on normal exit', function() + it('v:exiting=0, v:exitreason=quit on normal exit', function() test_exiting(function() command('quit') end) end) - it('is 0 on exit from Ex mode involving try-catch vim-patch:8.0.0184', function() + it('v:exiting=0, v:exitreason=quit on exit from Ex mode try-catch vim-patch:8.0.0184', function() test_exiting(function() feed('gQ') feed_command('try', 'call NoFunction()', 'catch', 'echo "bye"', 'endtry', 'quit') end) end) + + it('resets v:exitreason if quit is cancelled', function() + n.api.nvim_buf_set_lines(0, 0, -1, true, { 'modified' }) + pcall_err(command, 'quit') + eq('', eval('v:exitreason')) + end) end) describe(':cquit', function() diff --git a/test/functional/core/main_spec.lua b/test/functional/core/main_spec.lua index cbacad5835..904beb1444 100644 --- a/test/functional/core/main_spec.lua +++ b/test/functional/core/main_spec.lua @@ -217,7 +217,7 @@ describe('vim._core', function() -- All `vim._core.*` modules are builtin. t.eq( - { 'rebind_old_addr_after_restart', 'serverlist' }, + { 'rebind_after_restart', 'serverlist' }, n.exec_lua([[local k = vim.tbl_keys(require('vim._core.server')); table.sort(k); return k]]) ) local expected = { diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua index b1e22ef191..e97597f521 100644 --- a/test/functional/terminal/tui_spec.lua +++ b/test/functional/terminal/tui_spec.lua @@ -207,22 +207,24 @@ describe('TUI :restart', function() before_each(n.clear) after_each(n.check_close) - ---@param exp boolean Restart expected + --- Asserts that the server at `addr` has (or has not) restarted since `starttime`. + ---@param starttime integer v:starttime of the server before restart ---@param sess table Session ---@param addr string - local function assert_restarted(exp, sess, addr) - local s = sess + ---@return integer starttime + ---@return table session + local function assert_restarted(starttime, sess, addr) + sess:close() + local new_starttime + -- Retry: old server may still be alive (connect succeeds but yields stale starttime), + -- or on Windows the --listen address is restored async. retry(nil, 5000, function() - if exp then - s:close() - s = n.connect(addr) - end - - -- Cheesy but reliable: :restart drops "-- [files…]", so empty v:argf means restart happened. - -- TODO(justinmk): add `v:startreason`, `v:starttime` - local _, argf = s:request('nvim_eval', 'v:argf') - ok(vim.tbl_count(argf) == (exp and 0 or 1), exp and 'empty v:argf' or 'nonempty v:argf', argf) + sess = n.connect(addr) + local _, t = sess:request('nvim_eval', 'v:starttime') + ok(t > starttime, ('v:starttime (%d) > old starttime (%d)'):format(t, starttime), t) + new_starttime = t end) + return new_starttime, sess end it('validation', function() @@ -231,25 +233,19 @@ describe('TUI :restart', function() it('ZR', function() -- Just exercise ZR, don't need to test all :restart functionality here. - t.skip(is_os('win'), 'FIXME: --listen not preserved by :restart on Windows #38539') local server_pipe = new_pipename() local server_session finally(function() - if server_session and not server_session.closed then + if server_session then server_session:close() end end) local screen = tt.setup_child_nvim({ - '-u', - 'NONE', - '-i', - 'NONE', + '--clean', '--listen', server_pipe, '--cmd', 'set notermguicolors', - '--', - 'file1.txt', -- XXX: see comment in assert_restarted() }, { env = vim.tbl_extend('force', env_notermguicolors, { -- Ignore logs, because assert_restarted may log "connection refused" while it retries. @@ -259,10 +255,10 @@ describe('TUI :restart', function() finally(function() os.remove(testlog) end) - screen:expect({ any = 'file1%.txt' }) + screen:expect({ any = '%[No Name%]' }) server_session = n.connect(server_pipe) - assert_restarted(false, server_session, server_pipe) + local _, starttime = server_session:request('nvim_eval', 'v:starttime') -- ZR on modified buffer fails with E37. tt.feed_data('ifoo\027') @@ -272,26 +268,47 @@ describe('TUI :restart', function() -- [count]ZR discards unsaved changes. tt.feed_data('1ZR') screen:expect({ any = vim.pesc('[No Name]') }) - assert_restarted(true, server_session, server_pipe) + starttime, server_session = assert_restarted(starttime, server_session, server_pipe) + + -- The server is now detached and needs to be quit explicitly. + tt.feed_data(':qall!\r') end) it('works', function() + -- Log exit events + v:exitreason. + local eventlog = t.tmpname() + local initfile = t.tmpname() .. '.lua' + local init = [[ + local f = %q + vim.api.nvim_create_autocmd({ 'QuitPre', 'ExitPre', 'VimLeavePre', 'VimLeave' }, { + callback = function(ev) + vim.fn.writefile({ ('%%s:%%s'):format(ev.event, vim.v.exitreason) }, f, 'a') + end, + }) + ]] + write_file(initfile, init:format(eventlog)) + finally(function() + os.remove(eventlog) + os.remove(initfile) + end) + + local function assert_exitreason(expected) + local default = 'QuitPre:restart\nExitPre:restart\nVimLeavePre:restart\nVimLeave:restart\n' + eq(expected or default, t.read_file(eventlog)) + os.remove(eventlog) + end + local server_pipe = new_pipename() local screen = tt.setup_child_nvim({ + '--clean', '-u', - 'NONE', - '-i', - 'NONE', + initfile, '--listen', server_pipe, '--cmd', 'colorscheme vim', '--cmd', 'set laststatus=2 background=dark noruler noshowcmd', - -- XXX: New server starts before the UI connects to it. - -- So checking screen state for this pid is not possible. - -- '--cmd', - -- 'echo getpid()', }, { env = { COLORTERM = 'truecolor' } }) screen:set_option('rgb', true) @@ -331,13 +348,6 @@ describe('TUI :restart', function() server_pid = new_pid end - --- XXX: No longer using -c during new server startup. - --- Gets the last `argn` items in v:argv as a joined string. - -- local function get_argv(argn) - -- local argv = ({ server_session:request('nvim_eval', 'v:argv') })[2] --[[@type table]] - -- return table.concat(argv, ' ', #argv - argn, #argv) - -- end - local s1 = [[ | ^Hello1 | @@ -352,6 +362,7 @@ describe('TUI :restart', function() tt.feed_data(":restart put ='Hello1'\013") screen:expect(s1) assert_new_pid() + assert_exitreason() assert_termguicolors_and_no_gui_running() -- Complex command following +cmd. @@ -366,6 +377,7 @@ describe('TUI :restart', function() {5:-- TERMINAL --} | ]]) assert_new_pid() + assert_exitreason() assert_termguicolors_and_no_gui_running() -- Check ":restart" on an unmodified buffer. @@ -373,12 +385,14 @@ describe('TUI :restart', function() tt.feed_data(':restart\013') screen:expect(s0) assert_new_pid() + assert_exitreason() assert_termguicolors_and_no_gui_running() -- Check ":restart +qall!" on an unmodified buffer. tt.feed_data(':restart +qall!\013') screen:expect(s0) assert_new_pid() + assert_exitreason() assert_termguicolors_and_no_gui_running() -- Check ":restart +echo" cannot restart server. @@ -402,6 +416,8 @@ describe('TUI :restart', function() -- Cancel the operation (abandons restart). tt.feed_data('C\013') screen:expect({ any = vim.pesc('[No Name]') }) + -- Failed/cancelled restarts still fire QuitPre/ExitPre (but not VimLeave[Pre]). + assert_exitreason('QuitPre:restart\nExitPre:restart\n') -- Check :restart respects 'confirm' option. tt.feed_data(':set confirm\013') @@ -410,6 +426,8 @@ describe('TUI :restart', function() tt.feed_data('C\013') screen:expect({ any = vim.pesc('[No Name]') }) tt.feed_data(':set noconfirm\013') + -- Failed/cancelled restarts still fire QuitPre/ExitPre (but not VimLeave[Pre]). + assert_exitreason('QuitPre:restart\nExitPre:restart\n') -- Check ":confirm restart " on a modified buffer. tt.feed_data(":confirm restart put ='Hello3'\013") @@ -417,6 +435,7 @@ describe('TUI :restart', function() tt.feed_data('N\013') screen:expect({ any = '%^Hello3' }) assert_new_pid() + assert_exitreason() assert_termguicolors_and_no_gui_running() -- Check ":confirm restart +echo" correctly ignores ":confirm" @@ -427,12 +446,14 @@ describe('TUI :restart', function() tt.feed_data('ithis will be removed\027') tt.feed_data(':restart\013') screen:expect({ any = vim.pesc('Vim(qall):E37: No write since last change') }) + assert_exitreason('QuitPre:restart\nExitPre:restart\n') -- Check ":restart +qall!" on a modified buffer. tt.feed_data('ithis will be removed\027') tt.feed_data(':restart +qall!\013') screen:expect(s0) assert_new_pid() + assert_exitreason() assert_termguicolors_and_no_gui_running() if not is_os('win') then @@ -440,6 +461,7 @@ describe('TUI :restart', function() feed_data(':lua vim.schedule(function() vim.wait(100) end); vim.cmd.restart()\n') screen:expect(s0) assert_new_pid() + assert_exitreason() assert_termguicolors_and_no_gui_running() end @@ -462,6 +484,7 @@ describe('TUI :restart', function() {5:-- TERMINAL --} | ]]) assert_new_pid() + assert_exitreason() assert_termguicolors_and_no_gui_running() -- The server is now detached and needs to be quit explicitly.