From e0e7f65b74f7e0e901b415bbb62f6ce52a13b7cb Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Fri, 17 Jul 2026 16:37:45 -0400 Subject: [PATCH] refactor(detach): eliminate `nvim__ui_detach` #40793 Problem: `nvim__ui_detach` was added in 85e0559d46c7 in order to implement `detach_others` from Lua. Using Lua in this case is doing more harm than good. Solution: Extract `ui_detach_channel`, which also allows it to be used for the "self detach" path from `ex_detach`. Bonus: the old MSWIN path always called `os_swap_to_hidden_console()`; now `ui_detach_channel` only does so for a stdio channel. For the common stdio TUI this is identical; for a socket UI it's more correct (no parent console to swap). --- runtime/doc/api.txt | 14 ------- runtime/doc/gui.txt | 10 +++-- runtime/lua/vim/_core/server.lua | 20 --------- runtime/lua/vim/_meta/api.gen.lua | 10 ----- src/nvim/api/ui.c | 50 ++++++++++++++++++----- src/nvim/ex_docmd.c | 65 +++++++++--------------------- test/functional/core/main_spec.lua | 2 +- 7 files changed, 68 insertions(+), 103 deletions(-) diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt index d34bbb2617..bcdb12fafd 100644 --- a/runtime/doc/api.txt +++ b/runtime/doc/api.txt @@ -3863,20 +3863,6 @@ nvim_ui_try_resize_grid({grid}, {width}, {height}) • {width} (`integer`) The new requested width. • {height} (`integer`) The new requested height. -nvim__ui_detach({chan}) *nvim__ui_detach()* - WARNING: This feature is experimental/unstable. - - Detaches the UI on channel `chan`. - - Sets the channel's detach flag (so the server doesn't exit on stdio UIs), - sends an "error_exit" UI event, and closes the channel. - - Attributes: ~ - Since: 0.12.0 - - Parameters: ~ - • {chan} (`integer`) UI channel to disconnect. - ============================================================================== Win_config Functions *api-win_config* diff --git a/runtime/doc/gui.txt b/runtime/doc/gui.txt index 2789cd5849..376c37d9b9 100644 --- a/runtime/doc/gui.txt +++ b/runtime/doc/gui.txt @@ -59,11 +59,15 @@ Use cases: ------------------------------------------------------------------------------ Stop or detach the current UI - *:detach* *E5768* + *:detach* *E5768* :[range]detach Detaches the current UI. Other UIs (if any) remain attached. - Fails with |E5768| if no UI is attached. - The server (typically `nvim --embed`) continues running as + Fails with "E5768" if no UI is attached. If [range] is "%", + all UIs _except_ the current UI are detached: >vim + + :%detach + +< The server (typically `nvim --embed`) continues running as a background process, and you can reattach to it later. Before detaching, you may want to note the server address: >vim diff --git a/runtime/lua/vim/_core/server.lua b/runtime/lua/vim/_core/server.lua index 1649582067..e5e2737a7f 100644 --- a/runtime/lua/vim/_core/server.lua +++ b/runtime/lua/vim/_core/server.lua @@ -219,24 +219,4 @@ function M.ex_session_restart(eap, extra) end end ---- Disconnects every UI except `keep_chan`, keeping the server running. ---- ---- @param keep_chan integer Channel ID of the UI to preserve. ---- @return integer # Number of UIs detached. -function M.detach_others(keep_chan) - local uicount = #vim.api.nvim_list_uis() - for _, ui in ipairs(vim.api.nvim_list_uis()) do - if ui.chan and ui.chan ~= keep_chan then - vim.api.nvim__ui_detach(ui.chan) - end - end - local n = uicount - #vim.api.nvim_list_uis() - if n == 0 then - vim.api.nvim_echo({ { 'No other UIs are attached' } }, true, {}) - else - vim.api.nvim_echo({ { ('Detached %d non-current UIs'):format(n) } }, true, {}) - end - return n -end - return M diff --git a/runtime/lua/vim/_meta/api.gen.lua b/runtime/lua/vim/_meta/api.gen.lua index 4f8b292d6d..88ffde519a 100644 --- a/runtime/lua/vim/_meta/api.gen.lua +++ b/runtime/lua/vim/_meta/api.gen.lua @@ -186,16 +186,6 @@ function vim.api.nvim__set_restart_on_crash(progpath, argv) end --- @return table # Map of various internal stats. function vim.api.nvim__stats() end ---- WARNING: This feature is experimental/unstable. ---- ---- Detaches the UI on channel `chan`. ---- ---- Sets the channel's detach flag (so the server doesn't exit on stdio UIs), ---- sends an "error_exit" UI event, and closes the channel. ---- ---- @param chan integer UI channel to disconnect. -function vim.api.nvim__ui_detach(chan) end - --- WARNING: This feature is experimental/unstable. --- --- @param str string diff --git a/src/nvim/api/ui.c b/src/nvim/api/ui.c index a2f4aaa7db..6725a1be64 100644 --- a/src/nvim/api/ui.c +++ b/src/nvim/api/ui.c @@ -113,36 +113,68 @@ void remote_ui_disconnect(uint64_t channel_id, Error *err, bool send_error_exit) remote_ui_destroy(ui); } -/// Detaches the UI on channel `chan`. -/// -/// Sets the channel's detach flag (so the server doesn't exit on stdio UIs), -/// sends an "error_exit" UI event, and closes the channel. +/// Detaches the UI on channel `chan` (server keeps running). Sets `channel.detach`, sends an +/// "error_exit" UI event, and closes the channel. /// /// @param chan UI channel to disconnect. /// @param[out] err Error details, if any. -void nvim__ui_detach(Integer chan, Error *err) - FUNC_API_SINCE(14) +void ui_detach_channel(uint64_t chan, Error *err) { - Channel *c = find_channel((uint64_t)chan); + Channel *c = find_channel(chan); VALIDATE(c != NULL, "%s", e_invchan, { return; }); #ifdef MSWIN bool detach_stdio = c->streamtype == kChannelStreamStdio; #endif + // Prevent server self-exit on channel-close. c->detach = true; - remote_ui_disconnect((uint64_t)chan, err, true); + // Server-side UI detach. Doesn't close the channel. + remote_ui_disconnect(chan, err, true); if (ERROR_SET(err)) { return; } - channel_close((uint64_t)chan, kChannelPartAll, NULL); + // Server-side channel close. + channel_close(chan, kChannelPartAll, NULL); #ifdef MSWIN if (detach_stdio) { + // After UI/channel detach, move this server off the parent's console so it + // survives terminal closure and still has working CONIN$/CONOUT$. os_swap_to_hidden_console(); } #endif } +/// Detaches every UI except `keep_chan` (server keeps running). +/// +/// @param keep_chan UI channel to preserve. +/// @return Number of UIs detached. +size_t ui_detach_others(uint64_t keep_chan) +{ + // Snapshot the channel ids first: ui_detach_channel() mutates connected_uis. + kvec_t(uint64_t) chans = KV_INITIAL_VALUE; + uint64_t chan; + map_foreach_key(&connected_uis, chan, { + if (chan != keep_chan) { + kv_push(chans, chan); + } + }); + + size_t detached = 0; + for (size_t i = 0; i < kv_size(chans); i++) { + Error err = ERROR_INIT; + ui_detach_channel(kv_A(chans, i), &err); + if (ERROR_SET(&err)) { + api_clear_error(&err); + } else { + detached++; + } + } + + kv_destroy(chans); + return detached; +} + #ifdef EXITFREE void remote_ui_free_all_mem(void) { diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index 0cbe4d70aa..15f3824f5b 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -5964,69 +5964,42 @@ static void ex_tabs(exarg_T *eap) /// ":%detach" detaches all UIs _except_ the current UI. static void ex_detach(exarg_T *eap) { - // come on pooky let's burn this mf down if (!current_ui) { emsg(_(e_noui)); return; + } else if (eap && eap->forceit) { + emsg("bang (!) not supported yet"); + return; } if (eap && eap->addr_count > 0) { + // ":%detach" detaches every UI except the current one. if (eap->line1 != 1 || eap->line2 != curbuf->b_ml.ml_line_count) { emsg(_(e_invrange)); return; } - typval_T lua_args[] = { - { .v_type = VAR_NUMBER, .vval.v_number = (varnumber_T)current_ui }, - { .v_type = VAR_UNKNOWN }, - }; - typval_T rv = TV_INITIAL_VALUE; - nlua_call_typval("vim._core.server", "detach_others", lua_args, &rv); - ILOG("%%detach current_ui=%" PRIu64 " detached=%zu", current_ui, - (rv.v_type == VAR_NUMBER && rv.vval.v_number > 0) - ? (size_t)rv.vval.v_number : (size_t)0); - tv_clear(&rv); + size_t n = ui_detach_others(current_ui); + if (n == 0) { + msg(_("No other UIs are attached"), 0); + } else { + smsg(0, _("Detached %d non-current UIs"), (int)n); + } + ILOG("%%detach current_ui=%" PRIu64 " detached=%zu", current_ui, n); } else { + // come on pooky let's burn this mf down + // // 1. Send "error_exit" UI-event (notification only). // 2. Perform server-side UI detach. // 3. Close server-side channel without self-exit. - - Channel *chan = find_channel(current_ui); - if (!chan) { - emsg(e_invchan); + Error err = ERROR_INIT; + ui_detach_channel(current_ui, &err); + if (ERROR_SET(&err)) { + emsg(err.msg); // UI disappeared already? + api_clear_error(&err); return; } - // Prevent self-exit on channel-close. - Error detach_err = ERROR_INIT; - nvim__chan_set_detach(chan->id, true, &detach_err); - api_clear_error(&detach_err); - - // Server-side UI detach. Doesn't close the channel. - Error err2 = ERROR_INIT; - remote_ui_disconnect(chan->id, &err2, true); - if (ERROR_SET(&err2)) { - emsg(err2.msg); // UI disappeared already? - api_clear_error(&err2); - return; - } - - // Server-side channel close. - const char *err = NULL; - bool rv = channel_close(chan->id, kChannelPartAll, &err); - if (!rv && err) { - emsg(err); // UI disappeared already? - return; - } - // XXX: Can't do this, channel_decref() is async... - // assert(!find_channel(chan->id)); - -#ifdef MSWIN - // After UI/channel detach, move this server off the parent's console so it - // survives terminal closure and still has working CONIN$/CONOUT$. - os_swap_to_hidden_console(); -#endif - - ILOG("detach current_ui=%" PRId64, chan->id); + ILOG("detach current_ui=%" PRIu64, current_ui); } } diff --git a/test/functional/core/main_spec.lua b/test/functional/core/main_spec.lua index 8419184c6d..d185156d7d 100644 --- a/test/functional/core/main_spec.lua +++ b/test/functional/core/main_spec.lua @@ -193,7 +193,7 @@ describe('vim._core', function() -- All `vim._core.*` modules are builtin. t.eq( - { 'detach_others', 'ex_session_restart', 'rebind_after_restart', 'serverlist' }, + { 'ex_session_restart', 'rebind_after_restart', 'serverlist' }, n.exec_lua([[local k = vim.tbl_keys(require('vim._core.server')); table.sort(k); return k]]) ) local expected = {