mirror of
https://github.com/neovim/neovim.git
synced 2026-07-22 17:02:59 +00:00
feat(ui): ":%detach!" detaches all other UIs #39216
Problem: Not easy for one UI to kick the others off a shared Nvim server. Solution: Treat `:%detach` as detach-other-UIs.
This commit is contained in:
@@ -3863,6 +3863,20 @@ 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*
|
||||
|
||||
@@ -59,15 +59,21 @@ Use cases:
|
||||
------------------------------------------------------------------------------
|
||||
Stop or detach the current UI
|
||||
|
||||
*:detach*
|
||||
:detach
|
||||
*: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
|
||||
a background process, and you can reattach to it later.
|
||||
Before detaching, you may want to note the server address:
|
||||
>vim
|
||||
:echo v:servername
|
||||
<
|
||||
When [range] covers the entire buffer (`%`, as in `:%detach`),
|
||||
all other UIs are detached, keeping only the current one. The
|
||||
command fails with |E16| for any other range. No-op if only
|
||||
one UI is attached.
|
||||
|
||||
Note: The server closes the UI RPC channel, so :detach works
|
||||
for any UI client. But if the client isn't expecting this, it
|
||||
may (incorrectly) report an error.
|
||||
|
||||
@@ -223,6 +223,7 @@ EDITOR
|
||||
• |:restart| saves/restores the current session (window layout, buffers, …).
|
||||
• |:restart!| (with a bang "!") does not save/restore the session.
|
||||
• |ZR| restarts Nvim (|:restart|).
|
||||
• |:detach| with range "%" detaches all UIs except the current one.
|
||||
• |:uptime| displays uptime.
|
||||
• |:packupdate| and |:packdel| for managing |vim.pack|.
|
||||
• 'scrollback' is now also valid in |prompt-buffer| buffers to limit the
|
||||
|
||||
@@ -219,4 +219,24 @@ 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
|
||||
|
||||
10
runtime/lua/vim/_meta/api.gen.lua
generated
10
runtime/lua/vim/_meta/api.gen.lua
generated
@@ -186,6 +186,16 @@ 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
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "nvim/autocmd_defs.h"
|
||||
#include "nvim/channel.h"
|
||||
#include "nvim/channel_defs.h"
|
||||
#include "nvim/errors.h"
|
||||
#include "nvim/eval/typval.h"
|
||||
#include "nvim/eval/vars.h"
|
||||
#include "nvim/event/defs.h"
|
||||
@@ -40,6 +41,10 @@
|
||||
#include "nvim/types_defs.h"
|
||||
#include "nvim/ui.h"
|
||||
|
||||
#ifdef MSWIN
|
||||
# include "nvim/os/os_win_console.h"
|
||||
#endif
|
||||
|
||||
#define BUF_POS(ui) ((size_t)((ui)->packer.ptr - (ui)->packer.startptr))
|
||||
|
||||
#include "api/ui.c.generated.h"
|
||||
@@ -108,6 +113,36 @@ 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.
|
||||
///
|
||||
/// @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)
|
||||
{
|
||||
Channel *c = find_channel((uint64_t)chan);
|
||||
VALIDATE(c != NULL, "%s", e_invchan, {
|
||||
return;
|
||||
});
|
||||
#ifdef MSWIN
|
||||
bool detach_stdio = c->streamtype == kChannelStreamStdio;
|
||||
#endif
|
||||
c->detach = true;
|
||||
remote_ui_disconnect((uint64_t)chan, err, true);
|
||||
if (ERROR_SET(err)) {
|
||||
return;
|
||||
}
|
||||
channel_close((uint64_t)chan, kChannelPartAll, NULL);
|
||||
#ifdef MSWIN
|
||||
if (detach_stdio) {
|
||||
os_swap_to_hidden_console();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef EXITFREE
|
||||
void remote_ui_free_all_mem(void)
|
||||
{
|
||||
|
||||
@@ -164,6 +164,7 @@ EXTERN const char e_auabort[] INIT(= N_("E855: Autocommands caused command to ab
|
||||
EXTERN const char e_api_error[] INIT(= N_("E5555: API call: %s"));
|
||||
|
||||
EXTERN const char e_fast_api_disabled[] INIT(= N_("E5560: %s must not be called in a fast event context"));
|
||||
EXTERN const char e_noui[] INIT(= N_("E5768: No UI attached"));
|
||||
|
||||
EXTERN const char e_floatonly[] INIT(= N_("E5601: Cannot close window, only floating window would remain"));
|
||||
EXTERN const char e_floatexchange[] INIT(= N_("E5602: Cannot exchange or rotate float"));
|
||||
|
||||
@@ -742,8 +742,8 @@ M.cmds = {
|
||||
},
|
||||
{
|
||||
command = 'detach',
|
||||
flags = TRLBAR,
|
||||
addr_type = 'ADDR_NONE',
|
||||
flags = bit.bor(RANGE, TRLBAR),
|
||||
addr_type = 'ADDR_OTHER',
|
||||
func = 'ex_detach',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -5961,22 +5961,36 @@ static void ex_tabs(exarg_T *eap)
|
||||
///
|
||||
/// Detaches the current UI.
|
||||
///
|
||||
/// ":detach!" with bang (!) detaches all UIs _except_ the current UI.
|
||||
/// ":%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 (eap && eap->forceit) {
|
||||
emsg("bang (!) not supported yet");
|
||||
if (!current_ui) {
|
||||
emsg(_(e_noui));
|
||||
return;
|
||||
}
|
||||
|
||||
if (eap && eap->addr_count > 0) {
|
||||
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);
|
||||
} else {
|
||||
// 1. Send "error_exit" UI-event (notification only).
|
||||
// 2. Perform server-side UI detach.
|
||||
// 3. Close server-side channel without self-exit.
|
||||
|
||||
if (!current_ui) {
|
||||
emsg("UI not attached");
|
||||
return;
|
||||
}
|
||||
|
||||
Channel *chan = find_channel(current_ui);
|
||||
if (!chan) {
|
||||
emsg(e_invchan);
|
||||
|
||||
@@ -193,7 +193,7 @@ describe('vim._core', function()
|
||||
|
||||
-- All `vim._core.*` modules are builtin.
|
||||
t.eq(
|
||||
{ 'ex_session_restart', 'rebind_after_restart', 'serverlist' },
|
||||
{ 'detach_others', '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 = {
|
||||
|
||||
@@ -149,14 +149,16 @@ describe('TUI', function()
|
||||
end)
|
||||
|
||||
describe('TUI :detach', function()
|
||||
it('does not stop server', function()
|
||||
local child_server, screen
|
||||
|
||||
local function setup_detach_child()
|
||||
n.clear()
|
||||
finally(function()
|
||||
n.check_close()
|
||||
end)
|
||||
|
||||
local child_server = new_pipename()
|
||||
local screen = tt.setup_child_nvim({
|
||||
child_server = new_pipename()
|
||||
screen = tt.setup_child_nvim({
|
||||
'--listen',
|
||||
child_server,
|
||||
'-u',
|
||||
@@ -169,6 +171,10 @@ describe('TUI :detach', function()
|
||||
nvim_set .. ' laststatus=2 background=dark',
|
||||
}, { env = env_notermguicolors })
|
||||
tt.override_screen_expect_for_conpty(screen)
|
||||
end
|
||||
|
||||
it('does not stop server', function()
|
||||
setup_detach_child()
|
||||
|
||||
tt.feed_data('iHello, World')
|
||||
screen:expect([[
|
||||
@@ -193,8 +199,8 @@ describe('TUI :detach', function()
|
||||
{ child_session:request('nvim_command', 'detach!') }
|
||||
)
|
||||
eq(
|
||||
{ false, { 0, 'Vim(detach):E481: No range allowed: 1detach' } },
|
||||
{ child_session:request('nvim_command', '1detach') }
|
||||
{ false, { 0, 'Vim(detach):E16: Invalid range' } },
|
||||
{ child_session:request('nvim_command', '2detach') }
|
||||
)
|
||||
eq(
|
||||
{ false, { 0, 'Vim(detach):E488: Trailing characters: foo: detach foo' } },
|
||||
@@ -234,6 +240,72 @@ describe('TUI :detach', function()
|
||||
{5:-- TERMINAL --} |
|
||||
]])
|
||||
end)
|
||||
|
||||
it('% detaches other UIs', function()
|
||||
setup_detach_child()
|
||||
finally(function()
|
||||
pcall(function()
|
||||
local s = n.connect(child_server)
|
||||
s:request('nvim_command', 'qall!')
|
||||
end)
|
||||
end)
|
||||
|
||||
tt.feed_data('iHello from detach!')
|
||||
screen:expect({ any = vim.pesc('Hello from detach!^') })
|
||||
tt.feed_data('\027')
|
||||
|
||||
local child_session = n.connect(child_server)
|
||||
local _, uis_before = child_session:request('nvim_list_uis')
|
||||
eq(1, #uis_before)
|
||||
local tui_chan_id = uis_before[1].chan
|
||||
|
||||
-- :%detach with only 1 UI is a no-op.
|
||||
tt.feed_data(':%detach\013')
|
||||
screen:expect({ any = vim.pesc('No other UIs are attached') })
|
||||
local _, uis_noop = child_session:request('nvim_list_uis')
|
||||
eq(1, #uis_noop)
|
||||
|
||||
child_session:request('nvim_ui_attach', 50, 7, {})
|
||||
local _, uis_attached = child_session:request('nvim_list_uis')
|
||||
eq(2, #uis_attached)
|
||||
|
||||
tt.feed_data(':%detach\013')
|
||||
screen:expect([[
|
||||
Hello from detach^! |
|
||||
{100:~ }|*3
|
||||
{3:[No Name] [+] }|
|
||||
|
|
||||
{5:-- TERMINAL --} |
|
||||
]])
|
||||
|
||||
local verify_session = n.connect(child_server)
|
||||
local _, uis_after = verify_session:request('nvim_list_uis')
|
||||
eq(1, #uis_after)
|
||||
eq(tui_chan_id, uis_after[1].chan)
|
||||
end)
|
||||
|
||||
it('% detaches the original UI when invoked from a later UI', function()
|
||||
setup_detach_child()
|
||||
finally(function()
|
||||
pcall(function()
|
||||
local s = n.connect(child_server)
|
||||
s:request('nvim_command', 'qall!')
|
||||
end)
|
||||
end)
|
||||
|
||||
screen:expect({ any = vim.pesc('[No Name]') })
|
||||
local child_session = n.connect(child_server)
|
||||
child_session:request('nvim_ui_attach', 50, 7, {})
|
||||
local _, uis_before = child_session:request('nvim_list_uis')
|
||||
eq(2, #uis_before)
|
||||
|
||||
eq({ true, vim.NIL }, { child_session:request('nvim_command', '%detach') })
|
||||
screen:expect({ any = [[Process exited 0]] })
|
||||
|
||||
local status, uis_after = child_session:request('nvim_list_uis')
|
||||
ok(status)
|
||||
eq(1, #uis_after)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('TUI :restart', function()
|
||||
|
||||
Reference in New Issue
Block a user