mirror of
https://github.com/neovim/neovim.git
synced 2026-08-19 02:24:46 -05: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.
|
• {width} (`integer`) The new requested width.
|
||||||
• {height} (`integer`) The new requested height.
|
• {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*
|
Win_config Functions *api-win_config*
|
||||||
|
|||||||
+8
-2
@@ -59,15 +59,21 @@ Use cases:
|
|||||||
------------------------------------------------------------------------------
|
------------------------------------------------------------------------------
|
||||||
Stop or detach the current UI
|
Stop or detach the current UI
|
||||||
|
|
||||||
*:detach*
|
*:detach* *E5768*
|
||||||
:detach
|
:[range]detach
|
||||||
Detaches the current UI. Other UIs (if any) remain attached.
|
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
|
The server (typically `nvim --embed`) continues running as
|
||||||
a background process, and you can reattach to it later.
|
a background process, and you can reattach to it later.
|
||||||
Before detaching, you may want to note the server address:
|
Before detaching, you may want to note the server address:
|
||||||
>vim
|
>vim
|
||||||
:echo v:servername
|
: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
|
Note: The server closes the UI RPC channel, so :detach works
|
||||||
for any UI client. But if the client isn't expecting this, it
|
for any UI client. But if the client isn't expecting this, it
|
||||||
may (incorrectly) report an error.
|
may (incorrectly) report an error.
|
||||||
|
|||||||
@@ -223,6 +223,7 @@ EDITOR
|
|||||||
• |:restart| saves/restores the current session (window layout, buffers, …).
|
• |:restart| saves/restores the current session (window layout, buffers, …).
|
||||||
• |:restart!| (with a bang "!") does not save/restore the session.
|
• |:restart!| (with a bang "!") does not save/restore the session.
|
||||||
• |ZR| restarts Nvim (|:restart|).
|
• |ZR| restarts Nvim (|:restart|).
|
||||||
|
• |:detach| with range "%" detaches all UIs except the current one.
|
||||||
• |:uptime| displays uptime.
|
• |:uptime| displays uptime.
|
||||||
• |:packupdate| and |:packdel| for managing |vim.pack|.
|
• |:packupdate| and |:packdel| for managing |vim.pack|.
|
||||||
• 'scrollback' is now also valid in |prompt-buffer| buffers to limit the
|
• '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
|
||||||
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
|
return M
|
||||||
|
|||||||
Generated
+10
@@ -186,6 +186,16 @@ function vim.api.nvim__set_restart_on_crash(progpath, argv) end
|
|||||||
--- @return table<string,any> # Map of various internal stats.
|
--- @return table<string,any> # Map of various internal stats.
|
||||||
function vim.api.nvim__stats() end
|
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.
|
--- WARNING: This feature is experimental/unstable.
|
||||||
---
|
---
|
||||||
--- @param str string
|
--- @param str string
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
#include "nvim/autocmd_defs.h"
|
#include "nvim/autocmd_defs.h"
|
||||||
#include "nvim/channel.h"
|
#include "nvim/channel.h"
|
||||||
#include "nvim/channel_defs.h"
|
#include "nvim/channel_defs.h"
|
||||||
|
#include "nvim/errors.h"
|
||||||
#include "nvim/eval/typval.h"
|
#include "nvim/eval/typval.h"
|
||||||
#include "nvim/eval/vars.h"
|
#include "nvim/eval/vars.h"
|
||||||
#include "nvim/event/defs.h"
|
#include "nvim/event/defs.h"
|
||||||
@@ -40,6 +41,10 @@
|
|||||||
#include "nvim/types_defs.h"
|
#include "nvim/types_defs.h"
|
||||||
#include "nvim/ui.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))
|
#define BUF_POS(ui) ((size_t)((ui)->packer.ptr - (ui)->packer.startptr))
|
||||||
|
|
||||||
#include "api/ui.c.generated.h"
|
#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);
|
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
|
#ifdef EXITFREE
|
||||||
void remote_ui_free_all_mem(void)
|
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_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_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_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"));
|
EXTERN const char e_floatexchange[] INIT(= N_("E5602: Cannot exchange or rotate float"));
|
||||||
|
|||||||
@@ -742,8 +742,8 @@ M.cmds = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
command = 'detach',
|
command = 'detach',
|
||||||
flags = TRLBAR,
|
flags = bit.bor(RANGE, TRLBAR),
|
||||||
addr_type = 'ADDR_NONE',
|
addr_type = 'ADDR_OTHER',
|
||||||
func = 'ex_detach',
|
func = 'ex_detach',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+22
-8
@@ -5961,22 +5961,36 @@ static void ex_tabs(exarg_T *eap)
|
|||||||
///
|
///
|
||||||
/// Detaches the current UI.
|
/// 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)
|
static void ex_detach(exarg_T *eap)
|
||||||
{
|
{
|
||||||
// come on pooky let's burn this mf down
|
// come on pooky let's burn this mf down
|
||||||
if (eap && eap->forceit) {
|
if (!current_ui) {
|
||||||
emsg("bang (!) not supported yet");
|
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 {
|
} else {
|
||||||
// 1. Send "error_exit" UI-event (notification only).
|
// 1. Send "error_exit" UI-event (notification only).
|
||||||
// 2. Perform server-side UI detach.
|
// 2. Perform server-side UI detach.
|
||||||
// 3. Close server-side channel without self-exit.
|
// 3. Close server-side channel without self-exit.
|
||||||
|
|
||||||
if (!current_ui) {
|
|
||||||
emsg("UI not attached");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Channel *chan = find_channel(current_ui);
|
Channel *chan = find_channel(current_ui);
|
||||||
if (!chan) {
|
if (!chan) {
|
||||||
emsg(e_invchan);
|
emsg(e_invchan);
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ describe('vim._core', function()
|
|||||||
|
|
||||||
-- All `vim._core.*` modules are builtin.
|
-- All `vim._core.*` modules are builtin.
|
||||||
t.eq(
|
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]])
|
n.exec_lua([[local k = vim.tbl_keys(require('vim._core.server')); table.sort(k); return k]])
|
||||||
)
|
)
|
||||||
local expected = {
|
local expected = {
|
||||||
|
|||||||
@@ -149,14 +149,16 @@ describe('TUI', function()
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
describe('TUI :detach', function()
|
describe('TUI :detach', function()
|
||||||
it('does not stop server', function()
|
local child_server, screen
|
||||||
|
|
||||||
|
local function setup_detach_child()
|
||||||
n.clear()
|
n.clear()
|
||||||
finally(function()
|
finally(function()
|
||||||
n.check_close()
|
n.check_close()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
local child_server = new_pipename()
|
child_server = new_pipename()
|
||||||
local screen = tt.setup_child_nvim({
|
screen = tt.setup_child_nvim({
|
||||||
'--listen',
|
'--listen',
|
||||||
child_server,
|
child_server,
|
||||||
'-u',
|
'-u',
|
||||||
@@ -169,6 +171,10 @@ describe('TUI :detach', function()
|
|||||||
nvim_set .. ' laststatus=2 background=dark',
|
nvim_set .. ' laststatus=2 background=dark',
|
||||||
}, { env = env_notermguicolors })
|
}, { env = env_notermguicolors })
|
||||||
tt.override_screen_expect_for_conpty(screen)
|
tt.override_screen_expect_for_conpty(screen)
|
||||||
|
end
|
||||||
|
|
||||||
|
it('does not stop server', function()
|
||||||
|
setup_detach_child()
|
||||||
|
|
||||||
tt.feed_data('iHello, World')
|
tt.feed_data('iHello, World')
|
||||||
screen:expect([[
|
screen:expect([[
|
||||||
@@ -193,8 +199,8 @@ describe('TUI :detach', function()
|
|||||||
{ child_session:request('nvim_command', 'detach!') }
|
{ child_session:request('nvim_command', 'detach!') }
|
||||||
)
|
)
|
||||||
eq(
|
eq(
|
||||||
{ false, { 0, 'Vim(detach):E481: No range allowed: 1detach' } },
|
{ false, { 0, 'Vim(detach):E16: Invalid range' } },
|
||||||
{ child_session:request('nvim_command', '1detach') }
|
{ child_session:request('nvim_command', '2detach') }
|
||||||
)
|
)
|
||||||
eq(
|
eq(
|
||||||
{ false, { 0, 'Vim(detach):E488: Trailing characters: foo: detach foo' } },
|
{ false, { 0, 'Vim(detach):E488: Trailing characters: foo: detach foo' } },
|
||||||
@@ -234,6 +240,72 @@ describe('TUI :detach', function()
|
|||||||
{5:-- TERMINAL --} |
|
{5:-- TERMINAL --} |
|
||||||
]])
|
]])
|
||||||
end)
|
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)
|
end)
|
||||||
|
|
||||||
describe('TUI :restart', function()
|
describe('TUI :restart', function()
|
||||||
|
|||||||
Reference in New Issue
Block a user