commit 0864939cc58d3f0af1a88a4baf34ed6b2a2c53ed
parent b5ce7e74dcde885afeee7f0d943a196a5ec450f2
Author: Justin M. Keyes <justinkz@gmail.com>
Date: Sat, 14 Feb 2026 05:34:30 -0500
fix(restart): append `-c <cmd>` at end, drop `-- [files…]` #37846
Problem:
- `:restart <cmd>` prepends `-c <cmd>` before the original `-c` args (if
any). So the original `-c` args may "override" it, which is
surprising.
- Confusing logic: `v:argv` is partially prepared in `ex_docmd.c`, and
then later `ui.c` skips other parts of it.
Current behavior is nonsense, for example this sequence:
:restart echo "Hello"
:restart +qall echo "Hello" | echo "World"
results in this v:argv:
[
'nvim'
'-c'
'echo "Hello" | echo "World"'
'--embed'
'-c'
'echo "Hello"'
...
]
Whereas after this commit, v:argv is:
[
'nvim'
'--embed'
...
'-c'
'echo "Hello" | echo "World"'
]
Solution:
- Append `-c <cmd>` at the _end_ of `v:argv`, not the start.
- Use a dummy placeholder `+:::` to mark where the "restart command"
appears in `v:argv`.
- Do all `v:argv` preparation in `ex_docmd.c`. This simplifies `ui.c`.
- Drop `-- [files…]` from `v:argv` since it is probably more annoying
than useful. (Users can use sessions to restore files on restart.)
Diffstat:
5 files changed, 100 insertions(+), 96 deletions(-)
diff --git a/runtime/doc/gui.txt b/runtime/doc/gui.txt
@@ -74,8 +74,8 @@ Restart Nvim
Restarts Nvim.
1. Stops Nvim using `:qall` (or |+cmd|, if given).
- 2. Starts a new Nvim server using the same |v:argv|,
- optionally running [command] at startup. |-c|
+ 2. Starts a new Nvim server using the same |v:argv| (except
+ `-- [file…]` files), appended with `-c [command]`.
3. Attaches the current UI to the new Nvim server. Other UIs
(if any) will not reattach on restart (this may change in
the future).
diff --git a/src/nvim/api/ui.c b/src/nvim/api/ui.c
@@ -286,35 +286,9 @@ bool remote_ui_restart(uint64_t channel_id, Error *err)
int argc = tv_list_len(l);
assert(argc > 0);
Array argv = arena_array(&arena, (size_t)argc + 1);
- bool had_minmin = false;
- bool skipping_minc = false; // Skip -c <cmd> from argv.
- bool first_minc = true; // Avoid skipping the first -c <cmd> from argv.
TV_LIST_ITER_CONST(l, li, {
const char *arg = tv_get_string(TV_LIST_ITEM_TV(li));
- if (argv.size > 0 && !had_minmin && strequal(arg, "--")) {
- had_minmin = true;
- skipping_minc = false;
- }
- bool startswith_min = strlen(arg) > 0 && arg[0] == '-';
- bool startswith_minmin = strlen(arg) > 1 && arg[0] == '-' && arg[1] == '-';
- if (skipping_minc && (startswith_min || startswith_minmin)) {
- skipping_minc = false;
- }
- if (!had_minmin && !skipping_minc && strequal(arg, "-c")) {
- if (!first_minc) {
- skipping_minc = true;
- continue;
- }
- first_minc = false;
- }
- // Exclude --embed/--headless/-c <cmd> from `argv`, as the client may start the server in a
- // different way than how the server was originally started.
- // Eg: 'nvim -c foo -c bar --embed --headless -- example.txt' would be parsed as { 'nvim', '-c', 'foo', '--', 'example.txt' }.
- if (argv.size == 0 || had_minmin
- || (!strequal(arg, "--embed") && !strequal(arg, "--headless") && !skipping_minc)) {
- ADD_C(argv, CSTR_AS_OBJ(arg));
- }
- skipping_minc = false;
+ ADD_C(argv, CSTR_AS_OBJ(arg));
});
ADD_C(args, ARRAY_OBJ(argv));
diff --git a/src/nvim/eval/typval.c b/src/nvim/eval/typval.c
@@ -1014,8 +1014,8 @@ static int list_join_inner(garray_T *const gap, list_T *const l, const char *con
/// Join list into a string using given separator
///
-/// @param[out] gap Garray where result will be saved.
-/// @param[in] l Joined list.
+/// @param[out] gap Garray where the joined list will be saved.
+/// @param[in] l List.
/// @param[in] sep Separator.
///
/// @return OK in case of success, FAIL otherwise.
diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c
@@ -4943,27 +4943,42 @@ static void ex_restart(exarg_T *eap)
const list_T *l = get_vim_var_list(VV_ARGV);
int argc = tv_list_len(l);
list_T *argv_cpy = tv_list_alloc(eap->arg ? argc + 2 : argc);
- bool added_startup_arg = false;
- TV_LIST_ITER_CONST(l, li, {
+ // Copy v:argv, skipping unwanted items.
+ for (listitem_T *li = l != NULL ? l->lv_first : NULL; li != NULL; li = li->li_next) {
const char *arg = tv_get_string(TV_LIST_ITEM_TV(li));
size_t arg_size = strlen(arg);
assert(arg_size <= (size_t)SSIZE_MAX);
- // Skip "-" argument (stdin input marker).
- if (strequal(arg, "-")) {
- continue;
- }
- tv_list_append_string(argv_cpy, arg, (ssize_t)arg_size);
-
- // Patch v:argv to include "-c <arg>" when it restarts.
- if (eap->arg && !added_startup_arg) {
- tv_list_append_string(argv_cpy, "-c", 2);
- tv_list_append_string(argv_cpy, eap->arg, (ssize_t)strlen(eap->arg));
- added_startup_arg = true;
+ if (strequal(arg, "--embed") || strequal(arg, "--headless")) {
+ continue; // Drop --embed/--headless: the client decides how to start+attach the server.
+ } else if (strequal(arg, "-")) {
+ continue; // Drop stdin ("-") argument.
+ } else if (strequal(arg, "+:::")) {
+ // The special placeholder "+:::" marks a previous :restart command.
+ // Drop the `"+:::", "-c", "…"` triplet, to avoid "stacking" commands from previous :restart(s).
+ listitem_T *next1 = li->li_next;
+ if (next1 != NULL && strequal(tv_get_string(TV_LIST_ITEM_TV(next1)), "-c")) {
+ listitem_T *next2 = next1->li_next;
+ if (next2 != NULL) {
+ li = next2;
+ continue;
+ }
+ }
+ continue; // If the triplet is incomplete, just skip "+:::"
+ } else if (strequal(arg, "--")) {
+ break; // Drop "-- [files…]". Usually isn't wanted. User can :mksession instead.
}
- });
+ tv_list_append_string(argv_cpy, arg, (ssize_t)arg_size);
+ }
+ // Append `"+:::", "-c", "<command>"` to end of v:argv.
+ // The "+:::" item is a no-op placeholder to mark the :restart "<command>".
+ if (eap->arg && eap->arg[0] != '\0') {
+ tv_list_append_string(argv_cpy, S_LEN("+:::"));
+ tv_list_append_string(argv_cpy, S_LEN("-c"));
+ tv_list_append_string(argv_cpy, eap->arg, (ssize_t)strlen(eap->arg));
+ }
set_vim_var_list(VV_ARGV, argv_cpy);
char *quit_cmd = (eap->do_ecmd_cmd) ? eap->do_ecmd_cmd : "qall";
diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua
@@ -237,7 +237,12 @@ describe('TUI :detach', function()
end)
describe('TUI :restart', function()
- it('resets buffer to blank', function()
+ it('validation', function()
+ clear()
+ eq('Vim(restart):E481: No range allowed: :1restart', t.pcall_err(n.command, ':1restart'))
+ end)
+
+ it('works', function()
clear()
finally(function()
n.check_close()
@@ -272,7 +277,7 @@ describe('TUI :restart', function()
end
-- The value of has("gui_running") should be 0 before and after :restart.
- local function gui_running_check()
+ local function assert_no_gui_running()
tt.feed_data(':echo "GUI Running: " .. has("gui_running")\013')
screen:expect({ any = 'GUI Running: 0' })
end
@@ -285,12 +290,12 @@ describe('TUI :restart', function()
{5:-- TERMINAL --} |
]]
screen_expect(s0)
- gui_running_check()
+ assert_no_gui_running()
local server_session = n.connect(server_pipe)
local _, server_pid = server_session:request('nvim_call_function', 'getpid', {})
- local function restart_pid_check()
+ local function assert_new_pid()
server_session:close()
server_session = n.connect(server_pipe)
local _, new_pid = server_session:request('nvim_call_function', 'getpid', {})
@@ -298,52 +303,58 @@ describe('TUI :restart', function()
server_pid = new_pid
end
- tt.feed_data(':1restart\013')
- screen:expect({ any = vim.pesc('{101:E481: No range allowed}') })
+ --- 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 = [[
|
- |
- {2: }|
+ ^Hello1 |
+ {100:~ }|*2
+ {3:[No Name] [+] }|
{MATCH:%d+ +}|
- Hello |
- {102:Press ENTER or type command to continue}^ |
{5:-- TERMINAL --} |
]]
- -- Check trailing characters are considered in -c
- tt.feed_data(':restart echo "Hello"\013')
+ tt.feed_data(':set nomodified\013')
+ -- Command is added as "-c" arg.
+ tt.feed_data(":restart put ='Hello1'\013")
screen_expect(s1)
tt.feed_data('\013')
- restart_pid_check()
- gui_running_check()
+ assert_new_pid()
+ assert_no_gui_running()
+ eq("--cmd echo getpid() +::: -c put ='Hello1'", get_argv(4))
- -- Check trailing characters after +cmd are considered in -c
- tt.feed_data(':restart +qall echo "Hello" | echo "World"\013')
+ -- Complex command following +cmd.
+ tt.feed_data(":restart +qall! put ='Hello2' | put ='World2'\013")
screen_expect([[
|
- {2: }|
+ Hello2 |
+ ^World2 |
+ {100:~ }|
+ {3:[No Name] [+] }|
{MATCH:%d+ +}|
- Hello |
- World |
- {102:Press ENTER or type command to continue}^ |
{5:-- TERMINAL --} |
]])
- tt.feed_data('\013')
- restart_pid_check()
- gui_running_check()
+ assert_new_pid()
+ assert_no_gui_running()
+ eq("--cmd echo getpid() +::: -c put ='Hello2' | put ='World2'", get_argv(4))
-- Check ":restart" on an unmodified buffer.
+ tt.feed_data(':set nomodified\013')
tt.feed_data(':restart\013')
screen_expect(s0)
- restart_pid_check()
- gui_running_check()
+ assert_new_pid()
+ assert_no_gui_running()
-- Check ":restart +qall!" on an unmodified buffer.
tt.feed_data(':restart +qall!\013')
screen_expect(s0)
- restart_pid_check()
- gui_running_check()
+ assert_new_pid()
+ assert_no_gui_running()
+ eq('--cmd echo getpid()', get_argv(1))
-- Check ":restart +echo" cannot restart server.
tt.feed_data(':restart +echo\013')
@@ -375,15 +386,13 @@ describe('TUI :restart', function()
tt.feed_data(':set noconfirm\013')
-- Check ":confirm restart <cmd>" on a modified buffer.
- tt.feed_data(':confirm restart echo "Hello"\013')
+ tt.feed_data(":confirm restart put ='Hello3'\013")
screen:expect({ any = vim.pesc('Save changes to "Untitled"?') })
tt.feed_data('N\013')
-
- -- Check if the -c <cmd> runs after restart.
- screen_expect(s1)
- tt.feed_data('\013')
- restart_pid_check()
- gui_running_check()
+ screen:expect({ any = '%^Hello3' })
+ assert_new_pid()
+ assert_no_gui_running()
+ eq("--cmd echo getpid() +::: -c put ='Hello3'", get_argv(4))
-- Check ":confirm restart +echo" correctly ignores ":confirm"
tt.feed_data(':confirm restart +echo\013')
@@ -398,14 +407,14 @@ describe('TUI :restart', function()
tt.feed_data('ithis will be removed\027')
tt.feed_data(':restart +qall!\013')
screen_expect(s0)
- restart_pid_check()
- gui_running_check()
+ assert_new_pid()
+ assert_no_gui_running()
-- No --listen conflict when server exit is delayed.
feed_data(':lua vim.schedule(function() vim.wait(100) end); vim.cmd.restart()\n')
screen_expect(s0)
- restart_pid_check()
- gui_running_check()
+ assert_new_pid()
+ assert_no_gui_running()
screen:try_resize(60, 6)
screen_expect([[
@@ -425,11 +434,11 @@ describe('TUI :restart', function()
{MATCH:%d+ +}|
{5:-- TERMINAL --} |
]])
- restart_pid_check()
- gui_running_check()
+ assert_new_pid()
+ assert_no_gui_running()
end)
- it('filters stdin marker from v:argv on restart #34417', function()
+ it('drops "-" and "-- [files…]" from v:argv #34417', function()
t.skip(is_os('win'), 'stdin behavior differs on Windows')
clear()
local server_session
@@ -450,31 +459,37 @@ describe('TUI :restart', function()
'--cmd',
'set notermguicolors',
'-',
+ '--',
+ 'Xtest-file1',
+ 'Xtest-file2',
})
screen:expect([[
^ |
~ |*3
- {2:[No Name] [RO] 1,0-1 All}|
+ {2:Xtest-file1 0,0-1 All}|
|
{5:-- TERMINAL --} |
]])
server_session = n.connect(server_pipe)
- local expr = 'index(v:argv, "-") >= 0 ? v:true : v:false'
- local _, has_stdin = server_session:request('nvim_eval', expr)
- eq(true, has_stdin)
+ local expr = 'index(v:argv, "-") >= 0 || index(v:argv, "--") >= 0 ? v:true : v:false'
+ eq({ true, true }, { server_session:request('nvim_eval', expr) })
- tt.feed_data(':restart\013')
+ tt.feed_data(":restart put='foo'\013")
screen:expect([[
- ^ |
- ~ |*3
- {2:[No Name] 0,0-1 All}|
+ |
+ ^foo |
+ ~ |*2
+ {2:[No Name] [+] 2,1 All}|
|
{5:-- TERMINAL --} |
]])
server_session:close()
server_session = n.connect(server_pipe)
- local _, has_stdin_after = server_session:request('nvim_eval', expr)
- eq(false, has_stdin_after)
+
+ eq({ true, false }, { server_session:request('nvim_eval', expr) })
+ local argv = ({ server_session:request('nvim_eval', 'v:argv') })[2] --[[@type table]]
+ eq(13, #argv)
+ eq("-c put='foo'", table.concat(argv, ' ', #argv - 1, #argv))
end)
end)