diff --git a/runtime/doc/gui.txt b/runtime/doc/gui.txt index 85a4dbc93d..aab9861fa0 100644 --- a/runtime/doc/gui.txt +++ b/runtime/doc/gui.txt @@ -87,6 +87,22 @@ Restart Nvim event, this command is equivalent to `:qall`. 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 diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 1167faf251..33efc3ac44 100644 --- 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 index 93806d726b..c607c40351 100644 --- 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 index ecd5d32164..af4f5eaf27 100644 --- 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 index b73a6a1f2c..65a573176e 100644 --- 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 index 8ddd19fdaf..335030bd5d 100644 --- a/src/nvim/ex_cmds.lua +++ b/src/nvim/ex_cmds.lua @@ -582,6 +582,12 @@ M.cmds = { addr_type = 'ADDR_OTHER', 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), diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index 31907d7b0f..707b43da87 100644 --- 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
" detaches the current UI and connects to the given server. +/// ":connect! " 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 index bcf756fc5f..d4bf0aa7a4 100644 --- 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 index 82c844963d..2fe55777a8 100644 --- 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