commit 649bb372f66591c8349a4d3117f05e9f02549a1b
parent bd45e2be634b49f17b86a42359c0218081759f48
Author: Siddhant Agarwal <68201519+siddhantdev@users.noreply.github.com>
Date: Fri, 15 Aug 2025 04:28:09 +0530
feat(ui): :connect command #34586
Add the `:connect <address>` command which connects the currently
running TUI to the server at the given address.
Diffstat:
9 files changed, 218 insertions(+), 3 deletions(-)
diff --git a/runtime/doc/gui.txt b/runtime/doc/gui.txt
@@ -88,6 +88,22 @@ Restart Nvim
Note: Only works if the UI and server are on the same system.
------------------------------------------------------------------------------
+Connect UI to a different server
+
+ *:connect*
+
+:connect {address}
+ Detaches the UI from the server it is currently attached to
+ and attaches it to the server at {address} instead.
+
+ Note: If the current UI hasn't implemented the "connect" UI
+ event, this command is equivalent to |:detach|.
+
+:connect! {address}
+ Same as |:connect| but it also stops the detached server if
+ no other UI is currently attached to it.
+
+------------------------------------------------------------------------------
GUI commands
*:winp* *:winpos* *E188*
diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt
@@ -297,6 +297,8 @@ TUI
UI
• |:restart| restarts Nvim and reattaches the current UI.
+• |:connect| dynamically connects the current UI to the server at the given
+ address.
• |:checkhealth| shows a summary in the header for every healthcheck.
• |ui-multigrid| provides composition information and absolute coordinates.
• `vim._extui` provides an experimental commandline and message UI intended to
diff --git a/src/nvim/api/ui.c b/src/nvim/api/ui.c
@@ -319,6 +319,20 @@ bool remote_ui_restart(uint64_t channel_id, Error *err)
return true;
}
+// Send a connect UI event to the UI on the given channel
+void remote_ui_connect(uint64_t channel_id, char *server_addr, Error *err)
+{
+ RemoteUI *ui = get_ui_or_err(channel_id, err);
+ if (!ui) {
+ return;
+ }
+
+ MAXSIZE_TEMP_ARRAY(args, 1);
+ ADD_C(args, CSTR_AS_OBJ(server_addr));
+
+ push_call(ui, "connect", args);
+}
+
// TODO(bfredl): use me to detach a specific ui from the server
void remote_ui_stop(RemoteUI *ui)
{
diff --git a/src/nvim/api/ui_events.in.h b/src/nvim/api/ui_events.in.h
@@ -27,6 +27,8 @@ void visual_bell(void)
FUNC_API_SINCE(3);
void flush(void)
FUNC_API_SINCE(3) FUNC_API_REMOTE_IMPL;
+void connect(Array args)
+ FUNC_API_SINCE(14) FUNC_API_REMOTE_ONLY FUNC_API_REMOTE_IMPL FUNC_API_CLIENT_IMPL;
void restart(String progpath, Array argv)
FUNC_API_SINCE(14) FUNC_API_REMOTE_ONLY FUNC_API_REMOTE_IMPL FUNC_API_CLIENT_IMPL;
void suspend(void)
diff --git a/src/nvim/event/stream.c b/src/nvim/event/stream.c
@@ -146,7 +146,7 @@ void stream_close_handle(Stream *stream, bool rstream)
static void rstream_close_cb(uv_handle_t *handle)
{
RStream *stream = handle->data;
- if (stream->buffer) {
+ if (stream && stream->buffer) {
free_block(stream->buffer);
}
close_cb(handle);
@@ -155,10 +155,10 @@ static void rstream_close_cb(uv_handle_t *handle)
static void close_cb(uv_handle_t *handle)
{
Stream *stream = handle->data;
- if (stream->close_cb) {
+ if (stream && stream->close_cb) {
stream->close_cb(stream, stream->close_cb_data);
}
- if (stream->internal_close_cb) {
+ if (stream && stream->internal_close_cb) {
stream->internal_close_cb(stream, stream->internal_data);
}
}
diff --git a/src/nvim/ex_cmds.lua b/src/nvim/ex_cmds.lua
@@ -583,6 +583,12 @@ M.cmds = {
func = 'ex_menu',
},
{
+ command = 'connect',
+ flags = bit.bor(BANG, WORD1, NOTRLCOM, NEEDARG),
+ addr_type = 'ADDR_NONE',
+ func = 'ex_connect',
+ },
+ {
command = 'copy',
flags = bit.bor(RANGE, WHOLEFOLD, EXTRA, TRLBAR, CMDWIN, LOCK_OK, MODIFY),
addr_type = 'ADDR_LINES',
diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c
@@ -5649,6 +5649,32 @@ static void ex_detach(exarg_T *eap)
}
}
+/// ":connect"
+///
+/// Connects the current UI to a different server
+///
+/// ":connect <address>" detaches the current UI and connects to the given server.
+/// ":connect! <address>" stops the current server if no other UIs are attached, then connects to the given server.
+static void ex_connect(exarg_T *eap)
+{
+ bool stop_server = eap->forceit ? (ui_active() == 1) : false;
+
+ Error err = ERROR_INIT;
+ remote_ui_connect(current_ui, eap->arg, &err);
+
+ if (ERROR_SET(&err)) {
+ emsg(err.msg);
+ api_clear_error(&err);
+ return;
+ }
+
+ ex_detach(NULL);
+ if (stop_server) {
+ exiting = true;
+ getout(0);
+ }
+}
+
/// ":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
@@ -281,6 +281,38 @@ void ui_client_event_raw_line(GridLineEvent *g)
(const schar_T *)grid_line_buf_char, grid_line_buf_attr);
}
+void ui_client_event_connect(Array args)
+{
+ if (args.size < 1 || args.items[0].type != kObjectTypeString) {
+ ELOG("Error handling UI event 'connect'");
+ return;
+ }
+
+ char *server_addr = args.items[0].data.string.data;
+ multiqueue_put(main_loop.fast_events, channel_connect_event, server_addr);
+}
+
+static void channel_connect_event(void **argv)
+{
+ char *server_addr = argv[0];
+
+ const char *err = "";
+ bool is_tcp = !!strrchr(server_addr, ':');
+ CallbackReader on_data = CALLBACK_READER_INIT;
+ uint64_t chan = channel_connect(is_tcp, server_addr, true, on_data, 50, &err);
+
+ if (!strequal(err, "")) {
+ ELOG("Error handling UI event 'connect': %s", err);
+ return;
+ }
+
+ ui_client_channel_id = chan;
+ ui_client_is_remote = true;
+ ui_client_attach(tui_width, tui_height, tui_term, tui_rgb);
+
+ ELOG("Connected to channel: %" PRId64, chan);
+}
+
/// When a "restart" UI event is received, its arguments are saved here when
/// waiting for the server to exit.
static Array restart_args = ARRAY_DICT_INIT;
diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua
@@ -365,6 +365,123 @@ describe('TUI :restart', function()
end)
end)
+describe('TUI :connect', function()
+ if t.skip(is_os('win'), "relies on :detach which currently doesn't work on windows") then
+ return
+ end
+
+ it('leaves the current server running', function()
+ n.clear()
+ finally(function()
+ n.check_close()
+ end)
+
+ local server1 = new_pipename()
+ local screen = tt.setup_child_nvim({
+ '--listen',
+ server1,
+ '-u',
+ 'NONE',
+ })
+
+ tt.feed_data(':connect\013')
+ screen:expect([[
+ ^ |
+ ~ |*3
+ [No Name] 0,0-1 All|
+ E471: Argument required |
+ {5:-- TERMINAL --} |
+ ]])
+
+ screen:detach()
+
+ local server2 = new_pipename()
+ local screen2 = tt.setup_child_nvim({
+ '--listen',
+ server2,
+ '-u',
+ 'NONE',
+ })
+ tt.feed_data('iThis is server 2.\027')
+ tt.feed_data(':connect ' .. server1 .. '\013')
+
+ screen2:expect({
+ any = [[Process exited]],
+ })
+
+ local server1_session = n.connect(server1)
+ server1_session:request('nvim_command', 'qall!')
+
+ screen2:detach()
+
+ local server2_session = n.connect(server2)
+
+ local screen3 = tt.setup_child_nvim({
+ '--remote-ui',
+ '--server',
+ server2,
+ })
+ screen3:expect([[
+ This is server 2^. |
+ ~ |*3
+ {2:[No Name] [+] 1,17 All}|
+ |
+ {5:-- TERMINAL --} |
+ ]])
+
+ screen3:detach()
+ server2_session:request('nvim_command', 'qall!')
+ end)
+ it('! stops the current server', function()
+ n.clear()
+ finally(function()
+ n.check_close()
+ end)
+
+ local server1 = new_pipename()
+ local screen1 = tt.setup_child_nvim({
+ '--listen',
+ server1,
+ })
+ tt.feed_data('iThis is server 1')
+
+ screen1:detach()
+
+ local server2 = new_pipename()
+ local screen2 = tt.setup_child_nvim({
+ '--listen',
+ server2,
+ })
+ tt.feed_data('\027:connect! ' .. server1 .. '\013')
+ screen2:expect([[
+ This is server 1^ |
+ ~ |*3
+ [No Name] [+] 1,17 All|
+ -- INSERT -- |
+ {5:-- TERMINAL --} |
+ ]])
+
+ local server1_session = n.connect(server1)
+ server1_session:request('nvim_command', 'qall!')
+
+ screen2:detach()
+
+ local screen3 = tt.setup_child_nvim({
+ '--remote-ui',
+ '--server',
+ server2,
+ })
+ screen3:expect([[
+ Remote ui failed to start: connection refused |
+ |
+ [Process exited 1]^ |
+ |*3
+ {5:-- TERMINAL --} |
+ ]])
+ screen3:detach()
+ end)
+end)
+
if t.skip(is_os('win')) then
return
end