feat(:restart): v:starttime, v:exitreason #39319

This commit is contained in:
Justin M. Keyes
2026-04-22 14:58:47 -04:00
committed by GitHub
parent c407e3e67b
commit fd1b193d51
16 changed files with 265 additions and 117 deletions

View File

@@ -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

View File

@@ -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*

View File

@@ -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 <NL>).
*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.

View File

@@ -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.

View File

@@ -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())

View File

@@ -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

View File

@@ -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) {

View File

@@ -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

View File

@@ -135,4 +135,6 @@ typedef enum {
VV_LUA,
VV_RELNUM,
VV_VIRTNUM,
VV_STARTTIME,
VV_EXITREASON,
} VimVarIndex;

View File

@@ -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 <scriptfile>": 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);

View File

@@ -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.

View File

@@ -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 = [=[

View File

@@ -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)

View File

@@ -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()

View File

@@ -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 = {

View File

@@ -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 <command> 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 <cmd>" 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.