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.)
This commit is contained in:
Justin M. Keyes
2026-02-14 05:34:30 -05:00
committed by GitHub
parent b5ce7e74dc
commit 0864939cc5
5 changed files with 100 additions and 96 deletions

View File

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

View File

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

View File

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

View File

@@ -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;
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);
// 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;
}
});
}
// 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";

View File

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