mirror of
https://github.com/neovim/neovim.git
synced 2026-08-25 16:41:52 +00:00
feat(detach): opt-in to "server keeps running" #41133
Problem: By default, `nvim` does not survive if its host terminal dies. This is inconvenient if you want to use Nvim as a "session manager" (like tmux). Solution: Let users opt-in to the "survive" behavior via `:detach!` (bang "!"). This marks the current UI as "detachable", so the server will not self-exit if the UI channel closes.
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
},
|
||||
"diagnostics": {
|
||||
"disable": [
|
||||
"preferred-local-alias",
|
||||
"unnecessary-if"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -60,27 +60,35 @@ Use cases:
|
||||
Stop or detach the current UI
|
||||
|
||||
*:detach* *E5768*
|
||||
:[range]detach
|
||||
Detaches the current UI. Other UIs (if any) remain attached.
|
||||
Fails with "E5768" if no UI is attached. If [range] is "%",
|
||||
all UIs _except_ the current UI are detached: >vim
|
||||
:[range]detach Detaches the current UI. Other UIs (if any) remain attached,
|
||||
unless [range] was given. 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:
|
||||
a background process. Use |:connect| or |--remote-ui| to
|
||||
reattach. 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.
|
||||
for any GUI/UI; but if the client isn't expecting this, it may
|
||||
(incorrectly) report an error.
|
||||
|
||||
*:detach!*
|
||||
:detach! Marks the current UI as "detachable" without detaching it now:
|
||||
the UI stays attached, but if it later disconnects
|
||||
unexpectedly (e.g. host terminal closed, ssh disconnect,
|
||||
etc.), the server keeps running in the background.
|
||||
Use |:connect| or |--remote-ui| to reattach.
|
||||
|
||||
If you always want Nvim to survive accidental disconnect, put
|
||||
this in your config: >vim
|
||||
|
||||
silent detach!
|
||||
<
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
Restart Nvim
|
||||
@@ -118,8 +126,8 @@ 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.
|
||||
Attaches the UI to another Nvim instance: detaches from the
|
||||
current server, then attaches to {address}.
|
||||
|
||||
Note: If the current UI hasn't implemented the "connect" UI
|
||||
event, this command is equivalent to |:detach|.
|
||||
|
||||
@@ -256,6 +256,10 @@ EDITOR
|
||||
• |:restart!| (with a bang "!") does not save/restore the session.
|
||||
• |ZR| restarts Nvim (|:restart|).
|
||||
• |:detach| with range "%" detaches all UIs except the current one.
|
||||
• |:detach!| (with bang "!") marks the current UI as detachable: Nvim will
|
||||
continue running if the UI disconnects unexpectedly (e.g. if you
|
||||
accidentally close your terminal, ssh connection lost, etc.).
|
||||
Use |:connect| to reattach.
|
||||
• |:uptime| displays uptime.
|
||||
• |:packupdate| and |:packdel| for managing |vim.pack|.
|
||||
• 'scrollback' is now also valid in |prompt-buffer| buffers to limit the
|
||||
|
||||
@@ -742,7 +742,7 @@ M.cmds = {
|
||||
},
|
||||
{
|
||||
command = 'detach',
|
||||
flags = bit.bor(RANGE, TRLBAR),
|
||||
flags = bit.bor(BANG, RANGE, TRLBAR),
|
||||
addr_type = 'ADDR_OTHER',
|
||||
func = 'ex_detach',
|
||||
},
|
||||
|
||||
@@ -5825,11 +5825,15 @@ static void ex_tabs(exarg_T *eap)
|
||||
/// ":%detach" detaches all UIs _except_ the current UI.
|
||||
static void ex_detach(exarg_T *eap)
|
||||
{
|
||||
if (!current_ui) {
|
||||
Channel *chan = find_channel(current_ui);
|
||||
if (!chan) {
|
||||
emsg(_(e_noui));
|
||||
return;
|
||||
} else if (eap && eap->forceit) {
|
||||
emsg("bang (!) not supported yet");
|
||||
}
|
||||
|
||||
if (eap && eap->forceit) {
|
||||
chan->detach = true;
|
||||
msg(_("Nvim will continue running if the UI disconnects"), 0);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,10 @@
|
||||
#include "nvim/ui.h"
|
||||
#include "nvim/ui_client.h"
|
||||
|
||||
#ifdef MSWIN
|
||||
# include "nvim/os/os_win_console.h"
|
||||
#endif
|
||||
|
||||
#include "msgpack_rpc/channel.c.generated.h"
|
||||
|
||||
#ifdef NVIM_LOG_DEBUG
|
||||
@@ -494,6 +498,13 @@ static void rpc_close_event(void **argv)
|
||||
|
||||
channel_decref(channel);
|
||||
|
||||
#ifdef MSWIN
|
||||
// For ":detach!": unexpected disconnect does not call ui_detach_channel, unlike the normal
|
||||
// ":detach" case, so we need to do some cleanup here.
|
||||
bool detached_ui_stdio = channel->streamtype == kChannelStreamStdio
|
||||
&& channel->detach && channel->rpc.ui != NULL;
|
||||
#endif
|
||||
|
||||
// No more I/O can happen on this channel. Remove UI if there is one attached.
|
||||
// Do this here instead of in rpc_free() which isn't always called on exit, so that
|
||||
// UILeave events behave consistently.
|
||||
@@ -517,6 +528,12 @@ static void rpc_close_event(void **argv)
|
||||
} else if (channel->streamtype == kChannelStreamStdio && !channel->detach) {
|
||||
exit_on_closed_chan(0);
|
||||
}
|
||||
#ifdef MSWIN
|
||||
else if (detached_ui_stdio) {
|
||||
// Move this server off the now-dead console so it keeps working (CONIN$/CONOUT$).
|
||||
os_swap_to_hidden_console();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void rpc_free(Channel *channel)
|
||||
|
||||
@@ -153,7 +153,8 @@ end)
|
||||
describe('TUI :detach', function()
|
||||
local child_server, screen
|
||||
|
||||
local function setup_detach_child()
|
||||
local function setup_detach_child(opts)
|
||||
opts = opts or {}
|
||||
n.clear()
|
||||
finally(function()
|
||||
n.check_close()
|
||||
@@ -171,7 +172,7 @@ describe('TUI :detach', function()
|
||||
'colorscheme vim',
|
||||
'--cmd',
|
||||
nvim_set .. ' laststatus=2 background=dark',
|
||||
}, { env = env_notermguicolors })
|
||||
}, { env = env_notermguicolors, cols = opts.cols })
|
||||
tt.override_screen_expect_for_conpty(screen)
|
||||
end
|
||||
|
||||
@@ -196,10 +197,6 @@ describe('TUI :detach', function()
|
||||
assert(status)
|
||||
eq(1, #child_uis)
|
||||
|
||||
eq(
|
||||
{ false, { 0, 'Vim(detach):E477: No ! allowed: detach!' } },
|
||||
{ child_session:request('nvim_command', 'detach!') }
|
||||
)
|
||||
eq(
|
||||
{ false, { 0, 'Vim(detach):E16: Invalid range' } },
|
||||
{ child_session:request('nvim_command', '2detach') }
|
||||
@@ -243,6 +240,41 @@ describe('TUI :detach', function()
|
||||
]])
|
||||
end)
|
||||
|
||||
it('detach! keeps the UI attached but survives client disconnect', function()
|
||||
-- Wide enough that the confirmation message doesn't trigger a |hit-enter| prompt.
|
||||
setup_detach_child({ cols = 80 })
|
||||
-- Capture the client (foreground TUI) job so we can kill it below.
|
||||
local term_job = n.api.nvim_buf_get_var(0, 'terminal_job_id')
|
||||
|
||||
-- Wait for the child server to come up (its socket to exist), then connect.
|
||||
local child_session ---@type test.Session
|
||||
retry(nil, nil, function()
|
||||
child_session = n.connect(child_server)
|
||||
end)
|
||||
finally(function()
|
||||
-- Stop the (surviving) server so it doesn't dangle.
|
||||
pcall(function()
|
||||
n.connect(child_server):request('nvim_command', 'qall!')
|
||||
end)
|
||||
end)
|
||||
|
||||
feed_data('\027\027:detach!\013')
|
||||
screen:expect({ any = vim.pesc('Nvim will continue running') })
|
||||
|
||||
-- Unlike ":detach", the UI is still attached.
|
||||
eq(1, #({ child_session:request('nvim_list_uis') })[2])
|
||||
|
||||
-- Simulate the host terminal disconnecting: SIGKILL the client so it can't shut down cleanly.
|
||||
-- On Windows this also exercises the console hand-off in rpc_close_event().
|
||||
n.exec_lua(function(pid)
|
||||
vim.uv.kill(pid, 'sigkill')
|
||||
end, n.fn.jobpid(term_job))
|
||||
|
||||
retry(nil, 4000, function()
|
||||
eq(2, ({ child_session:request('nvim_eval', '1+1') })[2])
|
||||
end)
|
||||
end)
|
||||
|
||||
it('% detaches other UIs', function()
|
||||
setup_detach_child()
|
||||
finally(function()
|
||||
@@ -479,9 +511,9 @@ describe('TUI :restart', function()
|
||||
tt.feed_data('ifoo\027')
|
||||
tt.feed_data('ZR')
|
||||
screen:expect({ any = 'E37:' })
|
||||
-- Dismiss hit-enter so the next "E37" assertion below doesn't match this one immediately.
|
||||
tt.feed_data('\013')
|
||||
screen:expect({ any = vim.pesc('[No Name]') })
|
||||
-- Overwrite the message so the next "E37" is a real transition, not a stale immediate match.
|
||||
tt.feed_data(':echo "cleared"\r')
|
||||
screen:expect({ any = 'cleared', none = 'E37:' })
|
||||
tt.feed_data('1ZR')
|
||||
screen:expect({ any = 'E37:' })
|
||||
|
||||
|
||||
Reference in New Issue
Block a user