refactor(detach): eliminate nvim__ui_detach #40793

Problem:
`nvim__ui_detach` was added in 85e0559d46 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).
This commit is contained in:
Justin M. Keyes
2026-07-17 16:37:45 -04:00
committed by GitHub
parent 85e0559d46
commit e0e7f65b74
7 changed files with 68 additions and 103 deletions

View File

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

View File

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

View File

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

View File

@@ -186,16 +186,6 @@ function vim.api.nvim__set_restart_on_crash(progpath, argv) end
--- @return table<string,any> # 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

View File

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

View File

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

View File

@@ -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 = {