diff --git a/runtime/doc/editing.txt b/runtime/doc/editing.txt index dbff1fc2d2..b3a1e759ad 100644 --- a/runtime/doc/editing.txt +++ b/runtime/doc/editing.txt @@ -1209,8 +1209,8 @@ ZZ Write current file, if modified, and close the current ZQ Quit without checking for changes (same as ":q!"). *ZR* -[count]ZR Performs |:restart|. If a [count] is given, restarts - without checking for changes (":restart +qall!"). +[count]ZR Performs |:restart!|. If a [count] is given, restarts + without checking for changes (":restart! +qall!"). MULTIPLE WINDOWS AND BUFFERS *window-exit* diff --git a/runtime/doc/gui.txt b/runtime/doc/gui.txt index b21f072e33..0af0a07495 100644 --- a/runtime/doc/gui.txt +++ b/runtime/doc/gui.txt @@ -75,20 +75,23 @@ Stop or detach the current UI ------------------------------------------------------------------------------ Restart Nvim - *:restart* -:restart [+cmd] [command] + *:restart* *:restart!* +:restart[!] [+cmd] [command] Restarts Nvim. Sets |v:exitreason|. See also |ZR|. + When [!] is included, the session is not restored. - 1. Stops Nvim using `:qall` (or |+cmd|, if given). - 2. Starts a new Nvim server using the same |v:argv| (except - `-- [file…]` files). - 3. Attaches all UIs to the new Nvim server and runs `[command]` + 1. Saves the current session (unless [!] was given). + 2. Stops Nvim using `:qall` (or |+cmd|, if given). + 3. Starts a new Nvim server using the same |v:argv| (except + `-- [file…]` files and `-S [file]`), and sets |v:startreason|. + 4. Restores the saved session (unless [!] was given). + 5. Attaches all UIs to the new Nvim server and runs `[command]` on it. Example: discard changes and stop with `:qall!`, then restart: > :restart +qall! < Example: restart and restore the current session: > - :mksession! Session.vim | restart source Session.vim + :mksession! Session.vim | restart! source Session.vim < Example: restart and update plugins: > :restart lua vim.pack.update() < @@ -96,8 +99,9 @@ Restart Nvim • 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. + • If no UI handles the "restart" event, this leaves a dangling + process. + • To adjust what `:restart` restores, set 'sessionoptions'. ------------------------------------------------------------------------------ Connect UI to a different server diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 1ab7346ff0..456b2798cc 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -191,7 +191,8 @@ EDITOR codeblocks via treesitter and executes them as Lua instead of Vimscript. (Or use `:{range}lua` to skip the detection.) • |:wall| with |++p| auto-creates missing parent directories. -• |ZR| restarts Nvim (|:restart|). +• |:restart| now restores the current session while |:restart!| does not. +• |ZR| restarts Nvim (|:restart!|). EVENTS @@ -453,6 +454,7 @@ VIMSCRIPT • |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:startreason| differentiates between restart and normal start. • |v:starttime| is the process start time (nanoseconds since UNIX epoch). ============================================================================== diff --git a/runtime/doc/starting.txt b/runtime/doc/starting.txt index ce79b9416d..85a4de3eae 100644 --- a/runtime/doc/starting.txt +++ b/runtime/doc/starting.txt @@ -434,7 +434,7 @@ Initialization *initialization* *startup* At startup, Nvim checks environment variables and files and sets values accordingly, proceeding as follows: -1. Set |v:starttime|. +1. Set |v:starttime| and |v:startreason|. 2. Set the 'shell' option. *SHELL* *COMSPEC* The environment variable SHELL, if it exists, is used to set the 'shell' diff --git a/runtime/doc/vvars.txt b/runtime/doc/vvars.txt index e61bc78624..16a5cd55f0 100644 --- a/runtime/doc/vvars.txt +++ b/runtime/doc/vvars.txt @@ -633,6 +633,15 @@ v:stacktrace stack trace. See also |v:exception|, |v:throwpoint|, and |throw-variables|. + *v:startreason* *startreason-variable* +v:startreason + The reason Nvim started. Possible values: + - "normal" normal startup. + - "restart" started by |:restart|. + - "restart!" started by |:restart!| or |ZR|. + + Read-only. + *v:starttime* *starttime-variable* v:starttime Timestamp (nanoseconds since UNIX epoch) when the Nvim process diff --git a/runtime/lua/vim/_core/server.lua b/runtime/lua/vim/_core/server.lua index 3b05142a33..4d9640ebd6 100644 --- a/runtime/lua/vim/_core/server.lua +++ b/runtime/lua/vim/_core/server.lua @@ -1,3 +1,5 @@ +local fs = vim.fs +local uv = vim.uv -- For "--listen" and related functionality. local M = {} @@ -97,4 +99,59 @@ function M.rebind_after_restart(canonical_addr, expected_uis) end) end +-- Called by ex_restart(). Saves the current session and calls back to +-- ex_restart() with the necessary arguments to restore the session. +-- TODO: https://github.com/neovim/neovim/issues/34204 +function M.ex_session_restart(after_cmd, quit_cmd) + -- Commands to run after restart + assert(not after_cmd:find(']==]')) + + -- Use custom quit command if provided + if quit_cmd == '' then + quit_cmd = 'qall' + end + + -- Preserve the value of v:this_session + local this_session = vim.v.this_session + assert(not this_session:find(']==]')) + + -- Get temp file to write session to + local temp_dir = fs.abspath(fs.dirname(fs.dirname(vim.fn.tempname()))) + assert(not temp_dir:find(']==]')) + local fd, session = uv.fs_mkstemp(fs.joinpath(temp_dir, 'restart_session_XXXXXX')) + if not fd then + error('Failed to get temporary filename for restart session') + end + uv.fs_close(fd) + + -- Write session + local session_arg = vim.fn.fnameescape(session) + vim.cmd('%argdelete') + vim.cmd.mksession { session_arg, bang = true } + + -- Lua commands to restore the session and remove the session file + local after_list = {} + table.insert(after_list, ('vim.cmd("source %s")'):format(session_arg)) + table.insert(after_list, ('pcall(vim.fs.rm, [==[%s]==])'):format(session)) + table.insert(after_list, ('vim.v.this_session = [==[%s]==]'):format(this_session)) + -- User provided command + if after_cmd ~= '' then + table.insert(after_list, ('vim.cmd([==[%s]==])'):format(after_cmd)) + end + -- Concatenate everything together + local after = 'lua ' .. table.concat(after_list, ';') + + -- Restart Neovim and run our Lua commands + local success, msg = pcall(function() + -- "+:::" special argument tells the C handler that this is actually a non-bang restart + -- That way, v:startreason can be set correctly + vim.cmd.restart { '+:::', quit_cmd, after, bang = true } + end) + + if not success then + fs.rm(session, { force = true }) + error(msg) + end +end + return M diff --git a/runtime/lua/vim/_meta/vvars.gen.lua b/runtime/lua/vim/_meta/vvars.gen.lua index 543d4169c1..f31865f19c 100644 --- a/runtime/lua/vim/_meta/vvars.gen.lua +++ b/runtime/lua/vim/_meta/vvars.gen.lua @@ -667,6 +667,15 @@ vim.v.shell_error = ... --- @type table[] vim.v.stacktrace = ... +--- The reason Nvim started. Possible values: +--- - "normal" normal startup. +--- - "restart" started by `:restart`. +--- - "restart!" started by `:restart!` or `ZR`. +--- +--- Read-only. +--- @type string +vim.v.startreason = ... + --- Timestamp (nanoseconds since UNIX epoch) when the Nvim process --- started. --- diff --git a/src/nvim/channel.c b/src/nvim/channel.c index 31f6c80bd6..08cd76c398 100644 --- a/src/nvim/channel.c +++ b/src/nvim/channel.c @@ -562,9 +562,9 @@ uint64_t channel_from_stdio(bool rpc, CallbackReader on_output, const char **err os_set_cloexec(stdout_dup_fd); // :restart spawns a replacement server that must not borrow the parent // Nvim process console, because that parent process will soon exit. - const bool restart_alloc_console = os_env_exists("__NVIM_RESTART_ALLOC_CONSOLE", true); + const bool restart_alloc_console = os_env_exists(ENV_RESTART_ALLOC_CONSOLE, true); if (restart_alloc_console) { - os_unsetenv("__NVIM_RESTART_ALLOC_CONSOLE"); + os_unsetenv(ENV_RESTART_ALLOC_CONSOLE); } if (!GetConsoleWindow()) { // Borrow the parent's console so CONOUT$ resolves to the real terminal, diff --git a/src/nvim/eval/funcs.c b/src/nvim/eval/funcs.c index d2c339f008..afe356a72c 100644 --- a/src/nvim/eval/funcs.c +++ b/src/nvim/eval/funcs.c @@ -3385,8 +3385,11 @@ static const char *required_env_vars[] = { NULL }; +/// Builds an environment dict for a child process (job). +/// +/// @param set_nvim_addr Set the $NVIM env var. dict_T *create_environment(const dictitem_T *job_env, const bool clear_env, const bool pty, - const char * const pty_term_name) + const bool set_nvim_addr, const char * const pty_term_name) { dict_T *env = tv_dict_alloc(); @@ -3427,13 +3430,15 @@ dict_T *create_environment(const dictitem_T *job_env, const bool clear_env, cons } // Set $NVIM (in the child process) to v:servername. #3118 - char *nvim_addr = get_vim_var_str(VV_SEND_SERVER); - if (nvim_addr[0] != NUL) { - dictitem_T *dv = tv_dict_find(env, S_LEN("NVIM")); - if (dv) { - tv_dict_item_remove(env, dv); + if (set_nvim_addr) { + char *nvim_addr = get_vim_var_str(VV_SEND_SERVER); + if (nvim_addr[0] != NUL) { + dictitem_T *dv = tv_dict_find(env, S_LEN("NVIM")); + if (dv) { + tv_dict_item_remove(env, dv); + } + tv_dict_add_str(env, S_LEN("NVIM"), nvim_addr); } - tv_dict_add_str(env, S_LEN("NVIM"), nvim_addr); } if (job_env) { @@ -3619,7 +3624,7 @@ void f_jobstart(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) term_name = term_name ? term_name : "ansi"; } - dict_T *env = create_environment(job_env, clear_env, pty, term_name); + dict_T *env = create_environment(job_env, clear_env, pty, true, term_name); Channel *chan = channel_job_start(argv, NULL, on_stdout, on_stderr, on_exit, pty, rpc, overlapped, detach, stdin_mode, cwd, width, height, env, &rettv->vval.v_number); diff --git a/src/nvim/eval/vars.c b/src/nvim/eval/vars.c index 803b121133..958af29d7e 100644 --- a/src/nvim/eval/vars.c +++ b/src/nvim/eval/vars.c @@ -217,6 +217,7 @@ static struct vimvar { VV(VV_VIRTNUM, "virtnum", VAR_NUMBER, VV_RO), VV(VV_STARTTIME, "starttime", VAR_NUMBER, VV_RO), VV(VV_EXITREASON, "exitreason", VAR_STRING, VV_RO), + VV(VV_STARTREASON, "startreason", VAR_STRING, VV_RO), }; #undef VV @@ -316,6 +317,7 @@ void evalvars_init(void) set_vim_var_nr(VV_SEARCHFORWARD, 1); set_vim_var_nr(VV_HLSEARCH, 1); set_vim_var_nr(VV_COUNT1, 1); + set_vim_var_string(VV_STARTREASON, S_LEN("normal")); set_vim_var_special(VV_EXITING, kSpecialVarNull); set_vim_var_nr(VV_TYPE_NUMBER, VAR_TYPE_NUMBER); @@ -345,6 +347,15 @@ void evalvars_init(void) set_vim_var_partial(VV_LUA, vvlua_partial); set_reg_var(0); // default for v:register is not 0 but '"' + + // Set v:startreason via environment variable + const char *startreason = os_getenv_noalloc(ENV_STARTREASON); + if (strequal(startreason, "restart!") || strequal(startreason, "restart")) { + set_vim_var_string(VV_STARTREASON, startreason, -1); + } + if (os_env_exists(ENV_STARTREASON, false)) { + os_unsetenv(ENV_STARTREASON); + } } #if defined(EXITFREE) diff --git a/src/nvim/eval_defs.h b/src/nvim/eval_defs.h index 7cec3d6167..f45cb35213 100644 --- a/src/nvim/eval_defs.h +++ b/src/nvim/eval_defs.h @@ -137,4 +137,5 @@ typedef enum { VV_VIRTNUM, VV_STARTTIME, VV_EXITREASON, + VV_STARTREASON, } VimVarIndex; diff --git a/src/nvim/ex_cmds.lua b/src/nvim/ex_cmds.lua index 5ecb2b2be2..ceece89702 100644 --- a/src/nvim/ex_cmds.lua +++ b/src/nvim/ex_cmds.lua @@ -2260,7 +2260,7 @@ M.cmds = { }, { command = 'restart', - flags = bit.bor(CMDARG, EXTRA, NOTRLCOM), + flags = bit.bor(BANG, CMDARG, EXTRA, NOTRLCOM), addr_type = 'ADDR_NONE', func = 'ex_restart', }, diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index 9531fc34c7..194d8af615 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -38,6 +38,7 @@ #include "nvim/edit.h" #include "nvim/errors.h" #include "nvim/eval/fs.h" +#include "nvim/eval/funcs.h" #include "nvim/eval/typval.h" #include "nvim/eval/typval_defs.h" #include "nvim/eval/userfunc.h" @@ -4975,6 +4976,41 @@ static void ex_quitall(exarg_T *eap) /// ":restart +cmd ": restart the Nvim server using ":cmd" and runs in the new server. static void ex_restart(exarg_T *eap) { + if (!eap->forceit) { + Error err = ERROR_INIT; + MAXSIZE_TEMP_ARRAY(args, 2); + + ADD_C(args, CSTR_AS_OBJ(eap->arg)); + ADD_C(args, CSTR_AS_OBJ(eap->do_ecmd_cmd ? eap->do_ecmd_cmd : "")); + + NLUA_EXEC_STATIC("require'vim._core.server'.ex_session_restart(...)", args, kRetNilBool, NULL, + &err); + + if (ERROR_SET(&err)) { + emsg(err.msg); + } + api_clear_error(&err); + return; + } + + const char *startreason = "restart!"; + char *quit_cmd = (eap->do_ecmd_cmd) ? eap->do_ecmd_cmd : "qall"; + char *after_cmd = eap->arg; + + // "+:::" is how ex_session_restart() signals that it (recursively) called into :restart. + if (strequal(quit_cmd, ":::")) { + startreason = "restart"; + // Set quit_cmd and after_cmd from args + if (eap->argc > 1) { + eap->args[1][eap->arglens[1]] = NUL; + quit_cmd = eap->args[1]; + after_cmd = eap->argc > 2 ? eap->args[2] : ""; + } else { + emsg("restart failed: +cmd did not quit the server"); + return; + } + } + Error err = ERROR_INIT; const bool no_ui = !ui_active(); const char *exepath = get_vim_var_str(VV_PROGPATH); @@ -4992,6 +5028,16 @@ static void ex_restart(exarg_T *eap) if (i > 0 && strequal(arg, "--")) { break; } + // Drop "-S [file]". It conflicts with :restart and usually isn't wanted for :restart! + if (i > 0 && strequal(arg, "-S")) { + if (li->li_next != NULL) { + const char *next_arg = tv_get_string(TV_LIST_ITEM_TV(li->li_next)); + if (next_arg[0] != '-') { + li = li->li_next; + } + } + continue; + } // Drop "-s ": skip the scriptfile arg too. if (i > 0 && strequal(arg, "-s")) { li = li->li_next; @@ -5051,11 +5097,10 @@ static void ex_restart(exarg_T *eap) bool server_stopped = listen_arg ? server_stop(listen_arg, true) : false; #endif + dict_T *env = create_environment(NULL, false, false, false, NULL); + tv_dict_add_str(env, S_LEN(ENV_STARTREASON), startreason); #ifdef MSWIN - bool restart_alloc_console_env = false; - if (os_setenv("__NVIM_RESTART_ALLOC_CONSOLE", "1", 1) == 0) { - restart_alloc_console_env = true; - } + tv_dict_add_str(env, S_LEN(ENV_RESTART_ALLOC_CONSOLE), "1"); #endif CallbackReader on_err = CALLBACK_READER_INIT; @@ -5072,12 +5117,7 @@ static void ex_restart(exarg_T *eap) Channel *channel = channel_job_start(argv, exepath, CALLBACK_READER_INIT, on_err, CALLBACK_NONE, false, true, true, detach, kChannelStdinPipe, - NULL, 0, 0, NULL, &exit_status); -#ifdef MSWIN - if (restart_alloc_console_env) { - os_unsetenv("__NVIM_RESTART_ALLOC_CONSOLE"); - } -#endif + NULL, 0, 0, env, &exit_status); if (!channel) { emsg("cannot create a channel job"); goto fail_1; @@ -5094,12 +5134,12 @@ static void ex_restart(exarg_T *eap) arena_mem_free(result_mem); result_mem = NULL; - if (*eap->arg != NUL) { + if (*after_cmd != NUL) { // Execute [command] on new server on UIEnter. MAXSIZE_TEMP_DICT(autocmd_opts, 3); PUT_C(autocmd_opts, "once", BOOLEAN_OBJ(true)); PUT_C(autocmd_opts, "nested", BOOLEAN_OBJ(true)); - PUT_C(autocmd_opts, "command", CSTR_AS_OBJ(eap->arg)); + PUT_C(autocmd_opts, "command", CSTR_AS_OBJ(after_cmd)); MAXSIZE_TEMP_ARRAY(autocmd_args, 2); ADD_C(autocmd_args, CSTR_AS_OBJ("UIEnter")); ADD_C(autocmd_args, DICT_OBJ(autocmd_opts)); @@ -5154,7 +5194,6 @@ static void ex_restart(exarg_T *eap) 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; // Prepend "confirm " to cmd if :confirm is used diff --git a/src/nvim/log.c b/src/nvim/log.c index 58eda798e5..c7c46ff11e 100644 --- a/src/nvim/log.c +++ b/src/nvim/log.c @@ -73,7 +73,7 @@ static void log_path_init(void) || !log_try_create(log_file_path)) { if (user_set) { // User-provided $NVIM_LOG_FILE. // Used by _core/log.lua:check_log_file to validate logfile on startup. - os_setenv("__NVIM_LOG_FILE_WANT", log_file_path, true); + os_setenv(ENV_LOGFILE_WANT, log_file_path, true); } // Make $XDG_STATE_HOME if it does not exist. char *loghome = get_xdg_home(kXDGStateHome); @@ -91,7 +91,7 @@ static void log_path_init(void) if (len >= size || !log_try_create(log_file_path)) { if (!user_set) { // Default fallback path. // Used by _core/log.lua:check_log_file to validate logfile on startup. - os_setenv("__NVIM_LOG_FILE_WANT", log_file_path, true); + os_setenv(ENV_LOGFILE_WANT, log_file_path, true); } len = xstrlcpy(log_file_path, "nvim.log", size); } diff --git a/src/nvim/msgpack_rpc/server.c b/src/nvim/msgpack_rpc/server.c index 48f3e8ce04..2cd7207ff7 100644 --- a/src/nvim/msgpack_rpc/server.c +++ b/src/nvim/msgpack_rpc/server.c @@ -54,7 +54,7 @@ bool server_init(const char *listen_addr) int rv = server_start(listen_addr); // TODO(justinmk): this is for log_spec. Can remove this after nvim_log #7062 is merged. - if (os_env_exists("__NVIM_TEST_LOG", false)) { + if (os_env_exists(ENV_TEST_LOG, false)) { ELOG("test log message"); } diff --git a/src/nvim/normal.c b/src/nvim/normal.c index ddb0966b7d..b2b64b1c5d 100644 --- a/src/nvim/normal.c +++ b/src/nvim/normal.c @@ -3302,9 +3302,9 @@ static void nv_Zet(cmdarg_T *cap) // "ZR": restart. With count, restart without checking for changes. case 'R': if (cap->count0 >= 1) { - do_cmdline_cmd("restart +qall!"); + do_cmdline_cmd("restart! +qall!"); } else { - do_cmdline_cmd("restart"); + do_cmdline_cmd("restart!"); } break; diff --git a/src/nvim/os/os.h b/src/nvim/os/os.h index cd7db3bf80..d9677cb58b 100644 --- a/src/nvim/os/os.h +++ b/src/nvim/os/os.h @@ -25,4 +25,8 @@ extern char *default_lib_dir; // IWYU pragma: end_exports #define ENV_LOGFILE "NVIM_LOG_FILE" +#define ENV_LOGFILE_WANT "__NVIM_LOG_FILE_WANT" #define ENV_NVIM "NVIM" +#define ENV_RESTART_ALLOC_CONSOLE "__NVIM_RESTART_ALLOC_CONSOLE" +#define ENV_STARTREASON "__NVIM_STARTREASON" +#define ENV_TEST_LOG "__NVIM_TEST_LOG" diff --git a/src/nvim/ui_client.c b/src/nvim/ui_client.c index e5ad4dcb7d..0a769cfcae 100644 --- a/src/nvim/ui_client.c +++ b/src/nvim/ui_client.c @@ -159,7 +159,7 @@ void ui_client_run(void) ui_client_attach(tui_width, tui_height, tui_term, tui_rgb); // TODO(justinmk): this is for log_spec. Can remove this after nvim_log #7062 is merged. - if (os_env_exists("__NVIM_TEST_LOG", true)) { + if (os_env_exists(ENV_TEST_LOG, true)) { ELOG("test log message"); } diff --git a/src/nvim/vvars.lua b/src/nvim/vvars.lua index db87bcda28..e90695b40c 100644 --- a/src/nvim/vvars.lua +++ b/src/nvim/vvars.lua @@ -765,6 +765,17 @@ M.vars = { Read-only. ]=], }, + startreason = { + type = 'string', + desc = [=[ + The reason Nvim started. Possible values: + - "normal" normal startup. + - "restart" started by |:restart|. + - "restart!" started by |:restart!| or |ZR|. + + Read-only. + ]=], + }, statusmsg = { type = 'string', desc = [=[ diff --git a/test/functional/core/main_spec.lua b/test/functional/core/main_spec.lua index 904beb1444..2d8eecce99 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_after_restart', 'serverlist' }, + { 'ex_session_restart', '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/core/server_spec.lua b/test/functional/core/server_spec.lua index cf79fe817f..05d1b546e3 100644 --- a/test/functional/core/server_spec.lua +++ b/test/functional/core/server_spec.lua @@ -352,8 +352,8 @@ describe('startup --listen', function() end) end) -it(':restart works in headless server (no UI)', function() - t.skip(is_os('win'), 'FIXME: --listen not preserved by :restart on Windows') +it(':restart! works in headless server (no UI)', function() + t.skip(is_os('win'), 'FIXME: --listen not preserved by :restart! on Windows') local nvim0 = clear() local server_pipe = n.new_pipename() @@ -364,18 +364,28 @@ it(':restart works in headless server (no UI)', function() n.set_session(nil) end) - fn.jobstart({ n.nvim_prog, '--clean', '--headless', '--listen', server_pipe }) + fn.jobstart({ + n.nvim_prog, + '--clean', + '--headless', + '--listen', + server_pipe, + '--cmd', + 'let g:early_startreason = v:startreason', + }) t.retry(nil, nil, function() neq(nil, vim.uv.fs_stat(server_pipe)) end) n.set_session(n.connect(server_pipe)) - n.expect_exit(n.command, 'restart') + n.expect_exit(n.command, 'restart!') n.set_session(n.connect(server_pipe)) eq(1, api.nvim_get_vvar('vim_did_enter')) + eq('restart!', api.nvim_get_vvar('startreason')) + eq('restart!', n.eval('g:early_startreason')) -- TODO: [command] is currently not executed without UI - -- n.expect_exit(n.command, 'restart lua _G.new_server = 1') + -- n.expect_exit(n.command, 'restart! lua _G.new_server = 1') -- n.set_session(n.connect(server_pipe)) -- eq(1, n.exec_lua('return _G.new_server')) end) diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua index 27d6374626..01e5a99b53 100644 --- a/test/functional/terminal/tui_spec.lua +++ b/test/functional/terminal/tui_spec.lua @@ -261,11 +261,86 @@ describe('TUI :restart', function() end it('validation', function() - eq('Vim(restart):E481: No range allowed: :1restart', t.pcall_err(n.command, ':1restart')) + eq('Vim(restart):E481: No range allowed: :1restart!', t.pcall_err(n.command, ':1restart!')) + end) + + it(':restart (no bang) restores session, window layout', function() + local file = 'Xtest-restart-file' + write_file(file, 'foobar') + finally(function() + os.remove(file) + end) + + local server_pipe = new_pipename() + local server_session + finally(function() + if server_session then + server_session:close() + end + end) + local screen = tt.setup_child_nvim({ + '--clean', + '--listen', + server_pipe, + '--cmd', + 'set notermguicolors laststatus=0 noruler noshowcmd', + }, { + env = vim.tbl_extend('force', env_notermguicolors, { + -- Ignore logs, because assert_restarted may log "connection refused" while it retries. + NVIM_LOG_FILE = testlog, + }), + }) + finally(function() + os.remove(testlog) + end) + + feed_data(':edit ' .. file .. '\r') + feed_data(':wincmd v\r') + screen:expect([[ + ^foobar │foobar | + ~ │~ | + ~ │~ | + ~ │~ | + ~ │~ | + :wincmd v | + {5:-- TERMINAL --} | + ]]) + server_session = n.connect(server_pipe) + local _, starttime = server_session:request('nvim_eval', 'v:starttime') + eq({ true, '' }, { server_session:request('nvim_get_vvar', 'this_session') }) + + -- :restart + feed_data(':restart\r') + screen:expect([[ + ^foobar │foobar | + ~ │~ | + ~ │~ | + ~ │~ | + ~ │~ | + | + {5:-- TERMINAL --} | + ]]) + starttime, server_session = assert_restarted(starttime, server_session, server_pipe) + eq({ true, 'restart' }, { server_session:request('nvim_get_vvar', 'startreason') }) + eq({ true, '' }, { server_session:request('nvim_get_vvar', 'this_session') }) + + -- :restart! + feed_data(':restart!\r') + screen:expect([[ + ^ | + ~ |*4 + | + {5:-- TERMINAL --} | + ]]) + starttime, server_session = assert_restarted(starttime, server_session, server_pipe) + eq({ true, 'restart!' }, { server_session:request('nvim_get_vvar', 'startreason') }) + + feed_data(':qall!\r') + screen:expect({ any = vim.pesc('[Process exited 0]') }) end) it('ZR', function() - -- Just exercise ZR, don't need to test all :restart functionality here. + -- Just exercise ZR, don't need to test all :restart! functionality here. local server_pipe = new_pipename() local server_session finally(function() @@ -345,8 +420,8 @@ describe('TUI :restart', function() }, { env = { COLORTERM = 'truecolor' } }) screen:set_option('rgb', true) - -- 'termguicolors' support should be detected properly after :restart. - -- The value of has("gui_running") should be 0 before and after :restart. + -- 'termguicolors' support should be detected properly after :restart! + -- The value of has("gui_running") should be 0 before and after :restart! local function assert_termguicolors_and_no_gui_running() tt.feed_data(':echo "&termguicolors: " .. &termguicolors\013') screen:expect({ any = '&termguicolors: 1' }) @@ -392,14 +467,14 @@ describe('TUI :restart', function() tt.feed_data(':set nomodified\013') -- Command is run on new server. - tt.feed_data(":restart put ='Hello1'\013") + 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. - tt.feed_data(":restart +qall! put ='Hello2' | put ='World2'\013") + tt.feed_data(":restart! +qall! put ='Hello2' | put ='World2'\013") screen:expect([[ | Hello2 | @@ -413,24 +488,24 @@ describe('TUI :restart', function() assert_exitreason() assert_termguicolors_and_no_gui_running() - -- Check ":restart" on an unmodified buffer. + -- Check ":restart!" on an unmodified buffer. tt.feed_data(':set nomodified\013') - tt.feed_data(':restart\013') + 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') + -- 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. + -- Check ":restart! +echo" cannot restart server. -- Check the full screen state to ensure this doesn't pollute the current UI. - tt.feed_data(':restart +echo\013') + tt.feed_data(':restart! +echo\013') screen:expect([[ ^ | {1:~}{18: }|*3 @@ -442,8 +517,8 @@ describe('TUI :restart', function() tt.feed_data('ithis will be removed\027') screen:expect({ any = vim.pesc('this will be remove^d') }) - -- Check ":confirm restart" on a modified buffer. - tt.feed_data(':confirm restart\013') + -- Check ":confirm restart!" on a modified buffer. + tt.feed_data(':confirm restart!\013') screen:expect({ any = vim.pesc('Save changes to "Untitled"?') }) -- Cancel the operation (abandons restart). @@ -452,9 +527,9 @@ describe('TUI :restart', function() -- Failed/cancelled restarts still fire QuitPre/ExitPre (but not VimLeave[Pre]). assert_exitreason('QuitPre:restart\nExitPre:restart\n') - -- Check :restart respects 'confirm' option. + -- Check :restart! respects 'confirm' option. tt.feed_data(':set confirm\013') - tt.feed_data(':restart\013') + tt.feed_data(':restart!\013') screen:expect({ any = vim.pesc('Save changes to "Untitled"?') }) tt.feed_data('C\013') screen:expect({ any = vim.pesc('[No Name]') }) @@ -462,8 +537,8 @@ describe('TUI :restart', function() -- 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") + -- Check ":confirm restart! " on a modified buffer. + tt.feed_data(":confirm restart! put ='Hello3'\013") screen:expect({ any = vim.pesc('Save changes to "Untitled"?') }) tt.feed_data('N\013') screen:expect({ any = '%^Hello3' }) @@ -471,19 +546,19 @@ describe('TUI :restart', function() assert_exitreason() assert_termguicolors_and_no_gui_running() - -- Check ":confirm restart +echo" correctly ignores ":confirm" - tt.feed_data(':confirm restart +echo\013') + -- Check ":confirm restart! +echo" correctly ignores ":confirm" + tt.feed_data(':confirm restart! +echo\013') screen:expect({ any = vim.pesc('+cmd did not quit the server') }) - -- Check ":restart" on a modified buffer. + -- Check ":restart!" on a modified buffer. tt.feed_data('ithis will be removed\027') - tt.feed_data(':restart\013') + 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. + -- Check ":restart! +qall!" on a modified buffer. tt.feed_data('ithis will be removed\027') - tt.feed_data(':restart +qall!\013') + tt.feed_data(':restart! +qall!\013') screen:expect(s0) assert_new_pid() assert_exitreason() @@ -491,7 +566,7 @@ describe('TUI :restart', function() if not is_os('win') then -- No --listen conflict when server exit is delayed. - feed_data(':lua vim.schedule(function() vim.wait(100) end); vim.cmd.restart()\n') + feed_data(':lua vim.schedule(function() vim.wait(100) end); vim.cmd("restart!")\n') screen:expect(s0) assert_new_pid() assert_exitreason() @@ -507,8 +582,8 @@ describe('TUI :restart', function() {5:-- TERMINAL --} | ]]) - --- Check that ":restart" uses the updated size after terminal resize. - tt.feed_data(':restart echo "restarted"\013') + --- Check that ":restart!" uses the updated size after terminal resize. + tt.feed_data(':restart! echo "restarted"\013') screen:expect([[ ^ | {1:~}{18: }|*2 @@ -527,6 +602,11 @@ describe('TUI :restart', function() it('drops "-" and "-- [files…]" from v:argv #34417', function() t.skip(is_os('win'), 'stdin behavior differs on Windows') + local file = 'file.lua' + write_file(file, "print('-S works')\n") + finally(function() + os.remove(file) + end) local server_session finally(function() if server_session then @@ -543,6 +623,8 @@ describe('TUI :restart', function() server_pipe, '--cmd', 'set notermguicolors', + '-S', + file, '-s', '-', '-', @@ -554,16 +636,18 @@ describe('TUI :restart', function() ^ | ~ |*3 {2:Xtest-file1 0,0-1 All}| - | + -S works | {5:-- TERMINAL --} | ]]) server_session = n.connect(server_pipe) local expr = 'index(v:argv, "-") >= 0 || index(v:argv, "--") >= 0 ? v:true : v:false' local has_s = 'index(v:argv, "-s") >= 0 ? v:true : v:false' + local has_S = 'index(v:argv, "-S") >= 0 ? v:true : v:false' eq({ true, true }, { server_session:request('nvim_eval', expr) }) eq({ true, true }, { server_session:request('nvim_eval', has_s) }) + eq({ true, true }, { server_session:request('nvim_eval', has_S) }) - tt.feed_data(":restart put='foo'\013") + tt.feed_data(":restart! put='foo'\013") screen:expect([[ | ^foo | @@ -577,6 +661,7 @@ describe('TUI :restart', function() eq({ true, false }, { server_session:request('nvim_eval', expr) }) eq({ true, false }, { server_session:request('nvim_eval', has_s) }) + eq({ true, false }, { server_session:request('nvim_eval', has_S) }) -- local argv = ({ server_session:request('nvim_eval', 'v:argv') })[2] --[[@type table]] -- eq(13, #argv) @@ -603,7 +688,7 @@ describe('TUI :restart', function() ]]) -- 'laststatus' should be 0 in the new Nvim and FileType event should be triggered. - feed_data(':restart set nowrap | edit test/functional/fixtures/bigfile.txt\r') + feed_data(':restart! set nowrap | edit test/functional/fixtures/bigfile.txt\r') screen:expect([[ ^0000;;Cc;0;BN;;;;;N;NULL;;;; | 0001;;Cc;0;BN;;;;;N;START OF HEADING;;;; | @@ -666,7 +751,7 @@ describe('TUI :restart', function() {5:-- TERMINAL --} | ]]) - feed_data(':restart echo "restarted"\r') + feed_data(':restart! echo "restarted"\r') screen:expect([[ ^ │0000;;Cc;0;BN;;;;;N| ~ │0001;;Cc;0;BN;;;;;N| @@ -677,7 +762,7 @@ describe('TUI :restart', function() {5:-- TERMINAL --} | ]]) - feed_data(':set sessionoptions-=winsize | restart\r') + feed_data(':set sessionoptions-=winsize | restart!\r') screen:expect([[ ^ │0000;;Cc;0;BN;;| ~ │0001;;Cc;0;BN;;| @@ -3034,8 +3119,8 @@ describe('TUI', function() { 'tty', 'tty' }, child_exec_lua('return { vim.uv.guess_handle(0), vim.uv.guess_handle(1) }') ) - -- Also works after :restart #38745 - feed_data(':restart lua ={ vim.uv.guess_handle(0), vim.uv.guess_handle(1) }\r') + -- Also works after :restart! #38745 + feed_data(':restart! lua ={ vim.uv.guess_handle(0), vim.uv.guess_handle(1) }\r') screen:expect([[ ^ | {100:~ }|*3 @@ -4441,11 +4526,11 @@ describe('TUI client', function() screen_server:expect(s1) end) - it(':restart works when connecting to remote instance (with its own TUI)', function() + it(':restart! works when connecting to remote instance (with its own TUI)', function() local _, screen_server, screen_client = start_tui_and_remote_client() -- Both clients should attach to the new server. - feed_data(':restart +qall!\n') + feed_data(':restart! +qall!\n') local screen_restarted = [[ ^ | {100:~ }|*3 @@ -4460,7 +4545,7 @@ describe('TUI client', function() screen_client:expect({ any = 'GUI Running: 0' }) -- The :vsplit command should only be executed once. - feed_data(':restart vsplit\r') + feed_data(':restart! vsplit\r') screen_restarted = [[ ^ │ | {100:~ }│{100:~ }|*3 @@ -4552,11 +4637,11 @@ describe('TUI client', function() screen_client:expect({ any = 'GUI Running: 0' }) end) - it(':restart works when connecting to remote instance (--headless)', function() + it(':restart! works when connecting to remote instance (--headless)', function() local _, server_pipe, screen_client = start_headless_server_and_client(false) -- The client should attach to the new server and the original server should exit. - feed_data(':restart +qall!\n') + feed_data(':restart! +qall!\n') screen_client:expect([[ ^ | {100:~ }|*4 diff --git a/test/functional/vimscript/vvars_spec.lua b/test/functional/vimscript/vvars_spec.lua index 377d6296fa..8afe719299 100644 --- a/test/functional/vimscript/vvars_spec.lua +++ b/test/functional/vimscript/vvars_spec.lua @@ -48,3 +48,11 @@ describe('v:argf', function() eq({ abs1, abs2, abs3 }, n.eval('v:argf')) end) end) + +describe('v:startreason', function() + it('is read-only and starts as "normal"', function() + n.clear { args = { '--cmd', 'let g:early_startreason = v:startreason' } } + eq('normal', eval('g:early_startreason')) + t.matches('E46', t.pcall_err(command, "let v:startreason = 'restart'")) + end) +end)