commit 86835b3db3249fa6743e48b6d1aedd3d50a4ed97
parent 236243029d1c0e101faa0d0b9c797322fb6d1380
Author: Sathya Pramodh <94102031+sathya-pramodh@users.noreply.github.com>
Date: Mon, 2 Jun 2025 18:24:17 +0530
feat(editor): ":restart" command #33953
Problem:
Developing/troubleshooting plugins has friction because "restarting"
Nvim requires quitting and manually starting again. #32484
Solution:
- Implement a `:restart` command which emits `restart` UI event.
- Handle the `restart` UI event in the builtin TUI client: stop the
`nvim --embed` server, start a new one, and attach to it.
Diffstat:
7 files changed, 206 insertions(+), 6 deletions(-)
diff --git a/runtime/doc/gui.txt b/runtime/doc/gui.txt
@@ -67,6 +67,20 @@ Stop or detach the current UI
Note: Not supported on Windows, currently.
------------------------------------------------------------------------------
+Restart the embedded Nvim server
+
+ *:restart*
+:restart
+ Detaches the embedded server from the UI and then restarts
+ it. This fails when changes have been made
+ and Vim refuses to |abandon| the current buffer.
+
+ Note: This only works if the UI started the server initially.
+:restart!
+ Force restarts the embedded server irrespective of unsaved
+ buffers.
+
+------------------------------------------------------------------------------
GUI commands
*:winp* *:winpos* *E188*
diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt
@@ -224,6 +224,8 @@ UI
• Error messages are more concise:
• "Error detected while processing:" changed to "Error in:".
• "Error executing Lua:" changed to "Lua:".
+• |:restart| detaches the embedded server from the UI and then restarts
+ it.
VIMSCRIPT
diff --git a/src/nvim/api/ui_events.in.h b/src/nvim/api/ui_events.in.h
@@ -177,3 +177,5 @@ void msg_history_clear(void)
void error_exit(Integer status)
FUNC_API_SINCE(12);
+void restart(void)
+ FUNC_API_SINCE(14) FUNC_API_REMOTE_ONLY FUNC_API_CLIENT_IMPL;
diff --git a/src/nvim/ex_cmds.lua b/src/nvim/ex_cmds.lua
@@ -3368,6 +3368,12 @@ M.cmds = {
addr_type = 'ADDR_LINES',
func = 'ex_substitute',
},
+ {
+ command = 'restart',
+ flags = bit.bor(BANG, FILES, CMDARG, ARGOPT, TRLBAR, CMDWIN, LOCK_OK),
+ addr_type = 'ADDR_NONE',
+ func = 'ex_restart',
+ },
-- commands that start with an uppercase letter
{
command = 'Next',
diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c
@@ -69,6 +69,7 @@
#include "nvim/message.h"
#include "nvim/mouse.h"
#include "nvim/move.h"
+#include "nvim/msgpack_rpc/channel.h"
#include "nvim/msgpack_rpc/server.h"
#include "nvim/normal.h"
#include "nvim/normal_defs.h"
@@ -101,6 +102,7 @@
#include "nvim/tag.h"
#include "nvim/types_defs.h"
#include "nvim/ui.h"
+#include "nvim/ui_client.h"
#include "nvim/undo.h"
#include "nvim/undo_defs.h"
#include "nvim/usercmd.h"
@@ -5592,6 +5594,42 @@ static void ex_detach(exarg_T *eap)
}
}
+/// ":restart" command
+/// Restarts the server by delegating the work to the UI.
+static void ex_restart(exarg_T *eap)
+{
+ bool forceit = eap && eap->forceit;
+
+ // Refuse to restart if text is locked (i.e in command line etc.)
+ if (text_locked()) {
+ text_locked_msg();
+ return;
+ }
+
+ // Refuse to restart if buffer is locked.
+ if (curbuf_locked()) {
+ return;
+ }
+
+ win_T *wp = curwin;
+
+ // If any buffer is changed and not saved, we cannot restart.
+ // But if called using bang (!), we will force restart.
+ if ((!buf_hide(wp->w_buffer)
+ && check_changed(wp->w_buffer, (p_awa ? CCGD_AW : 0)
+ | (forceit ? CCGD_FORCEIT : 0)
+ | CCGD_EXCMD))
+ || check_more(true, forceit) == FAIL
+ || check_changed_any(forceit, true)) {
+ if (!forceit) {
+ return;
+ }
+ }
+
+ // Send an ui restart event.
+ ui_call_restart();
+}
+
/// ":mode":
/// If no argument given, get the screen size and redraw.
static void ex_mode(exarg_T *eap)
diff --git a/src/nvim/ui_client.c b/src/nvim/ui_client.c
@@ -12,6 +12,7 @@
#include "nvim/channel.h"
#include "nvim/channel_defs.h"
#include "nvim/eval.h"
+#include "nvim/eval/typval.h"
#include "nvim/eval/typval_defs.h"
#include "nvim/event/multiqueue.h"
#include "nvim/globals.h"
@@ -37,6 +38,10 @@
#endif
static TUIData *tui = NULL;
+static int tui_width = 0;
+static int tui_height = 0;
+static char *tui_term = "";
+static bool tui_rgb = false;
static bool ui_client_is_remote = false;
// uncrustify:off
@@ -164,12 +169,8 @@ void ui_client_run(bool remote_ui)
FUNC_ATTR_NORETURN
{
ui_client_is_remote = remote_ui;
- int width, height;
- char *term;
- bool rgb;
- tui_start(&tui, &width, &height, &term, &rgb);
-
- ui_client_attach(width, height, term, rgb);
+ tui_start(&tui, &tui_width, &tui_height, &tui_term, &tui_rgb);
+ 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)) {
@@ -284,6 +285,59 @@ void ui_client_event_raw_line(GridLineEvent *g)
(const schar_T *)grid_line_buf_char, grid_line_buf_attr);
}
+/// Restarts the embedded server without killing the UI.
+void ui_client_event_restart(Array args)
+{
+ // 1. Client-side server detach.
+ ui_client_detach();
+
+ // 2. Close ui client channel (auto kills the `nvim --embed` server due to self-exit).
+ const char *error;
+ bool success = channel_close(ui_client_channel_id, kChannelPartAll, &error);
+ if (!success) {
+ ELOG("%s", error);
+ return;
+ }
+
+ // 3. Get v:argv.
+ typval_T *tv = get_vim_var_tv(VV_ARGV);
+ if (tv->v_type != VAR_LIST || tv->vval.v_list == NULL) {
+ ELOG("failed to get vim var typval");
+ return;
+ }
+ list_T *l = tv->vval.v_list;
+ int argc = tv_list_len(l);
+
+ // Assert to be positive for safe conversion to size_t.
+ assert(argc > 0);
+
+ char **argv = xmalloc(sizeof(char *) * ((size_t)argc + 1));
+ listitem_T *li = tv_list_first(l);
+ for (int i = 0; i < argc && li != NULL; i++, li = TV_LIST_ITEM_NEXT(l, li)) {
+ if (TV_LIST_ITEM_TV(li)->v_type == VAR_STRING && TV_LIST_ITEM_TV(li)->vval.v_string != NULL) {
+ argv[i] = TV_LIST_ITEM_TV(li)->vval.v_string;
+ } else {
+ argv[i] = "";
+ }
+ }
+ argv[argc] = NULL;
+
+ // 4. Start a new `nvim --embed` server.
+ uint64_t rv = ui_client_start_server(argc, argv);
+ if (!rv) {
+ ELOG("failed to start nvim server");
+ goto cleanup;
+ }
+
+ // 5. Client-side server re-attach.
+ ui_client_channel_id = rv;
+ ui_client_attach(tui_width, tui_height, tui_term, tui_rgb);
+
+ ILOG("restarted server id=%" PRId64, rv);
+cleanup:
+ xfree(argv);
+}
+
#ifdef EXITFREE
void ui_client_free_all_mem(void)
{
diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua
@@ -164,6 +164,90 @@ if t.skip(is_os('win')) then
return
end
+describe('TUI :restart', function()
+ before_each(function()
+ os.remove(testlog)
+ end)
+ teardown(function()
+ os.remove(testlog)
+ end)
+
+ it('resets buffer to blank', function()
+ local server_super = n.clear()
+ local client_super = n.new_session(true)
+ local job_opts = {
+ env = {
+ NVIM_LOG_FILE = testlog,
+ },
+ }
+
+ finally(function()
+ server_super:close()
+ client_super:close()
+ end)
+
+ local screen = tt.setup_child_nvim({
+ '-u',
+ 'NONE',
+ '-i',
+ 'NONE',
+ '--cmd',
+ 'colorscheme vim',
+ '--cmd',
+ nvim_set .. ' notermguicolors laststatus=2 background=dark',
+ }, job_opts)
+
+ -- Check ":restart" on an unmodified buffer.
+ tt.feed_data('\027\027:restart\013')
+ screen:expect {
+ grid = [[
+ ^ |
+ {4:~ }|*3
+ {5:[No Name] }|
+ |
+ {3:-- TERMINAL --} |
+ ]],
+ }
+
+ tt.feed_data('ithis will be removed')
+ screen:expect {
+ grid = [[
+ this will be removed^ |
+ {4:~ }|*3
+ {5:[No Name] [+] }|
+ {3:-- INSERT --} |
+ {3:-- TERMINAL --} |
+ ]],
+ }
+
+ -- Check ":restart" on a modified buffer.
+ tt.feed_data('\027\027:restart\013')
+ screen:expect {
+ grid = [[
+ this will be removed |
+ {5: }|
+ {8:E37: No write since last change} |
+ {8:E162: No write since last change for buffer "[No N}|
+ {8:ame]"} |
+ {10:Press ENTER or type command to continue}^ |
+ {3:-- TERMINAL --} |
+ ]],
+ }
+
+ -- Check ":restart!".
+ tt.feed_data('\027\027:restart!\013')
+ screen:expect {
+ grid = [[
+ ^ |
+ {4:~ }|*3
+ {5:[No Name] }|
+ |
+ {3:-- TERMINAL --} |
+ ]],
+ }
+ end)
+end)
+
describe('TUI', function()
local screen --[[@type test.functional.ui.screen]]
local child_session --[[@type test.Session]]