mirror of
				https://github.com/neovim/neovim.git
				synced 2025-11-04 01:34:25 +00:00 
			
		
		
		
	feat(ui): UI :detach command
Problem: Cannot detach the current UI. Solution: - Introduce `:detach`. - Introduce `Channel.detach`. Co-authored-by: bfredl <bjorn.linse@gmail.com>
This commit is contained in:
		@@ -129,7 +129,7 @@ Each pull request must pass the automated builds on [Cirrus CI] and [GitHub Acti
 | 
			
		||||
  passes various linter checks.
 | 
			
		||||
- CI for FreeBSD runs on [Cirrus CI].
 | 
			
		||||
- To see CI results faster in your PR, you can temporarily set `TEST_FILE` in
 | 
			
		||||
  [test.yml](https://github.com/neovim/neovim/blob/e35b9020b16985eee26e942f9a3f6b045bc3809b/.github/workflows/test.yml#L29).
 | 
			
		||||
  [test.yml](https://github.com/neovim/neovim/blob/ad8e0cfc1dfd937c2577dc032e524c799a772693/.github/workflows/test.yml#L26).
 | 
			
		||||
 | 
			
		||||
### Coverity
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1208,8 +1208,8 @@ nvim_select_popupmenu_item({item}, {insert}, {finish}, {opts})
 | 
			
		||||
 | 
			
		||||
                                                      *nvim_set_client_info()*
 | 
			
		||||
nvim_set_client_info({name}, {version}, {type}, {methods}, {attributes})
 | 
			
		||||
    Self-identifies the client. Sets the `client` object returned by
 | 
			
		||||
    |nvim_get_chan_info()|.
 | 
			
		||||
    Self-identifies the client, and sets optional flags on the channel.
 | 
			
		||||
    Defines the `client` object returned by |nvim_get_chan_info()|.
 | 
			
		||||
 | 
			
		||||
    Clients should call this just after connecting, to provide hints for
 | 
			
		||||
    debugging and orchestration. (Note: Something is better than nothing!
 | 
			
		||||
 
 | 
			
		||||
@@ -47,6 +47,25 @@ can connect to any Nvim instance).
 | 
			
		||||
Example: this sets "g:gui" to the value of the UI's "rgb" field:    >
 | 
			
		||||
        :autocmd UIEnter * let g:gui = filter(nvim_list_uis(),{k,v-> v.chan==v:event.chan})[0].rgb
 | 
			
		||||
<
 | 
			
		||||
------------------------------------------------------------------------------
 | 
			
		||||
Stop or detach the current UI
 | 
			
		||||
 | 
			
		||||
                                                *:detach*
 | 
			
		||||
:detach
 | 
			
		||||
                Detaches the current UI. Other UIs (if any) remain 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
 | 
			
		||||
<
 | 
			
		||||
                Note: The server closes the UI RPC channel, so :detach
 | 
			
		||||
                inherently "works" for all UIs. But if a UI isn't expecting
 | 
			
		||||
                the channel to be closed, it may be (incorrectly) reported as
 | 
			
		||||
                an error.
 | 
			
		||||
 | 
			
		||||
------------------------------------------------------------------------------
 | 
			
		||||
GUI commands
 | 
			
		||||
 | 
			
		||||
                                                *:winp* *:winpos* *E188*
 | 
			
		||||
:winp[os]
 | 
			
		||||
 
 | 
			
		||||
@@ -400,6 +400,8 @@ TUI
 | 
			
		||||
 | 
			
		||||
UI
 | 
			
		||||
 | 
			
		||||
• |:detach| the current UI, let the Nvim server continue running as a background
 | 
			
		||||
  process. Works with the builtin TUI, and all GUIs.
 | 
			
		||||
• |vim.ui.open()| (by default bound to |gx|) accepts an `opt.cmd` parameter
 | 
			
		||||
  which controls the tool used to open the given path or URL. If you want to
 | 
			
		||||
  globally set this, you can override vim.ui.open using the same approach
 | 
			
		||||
 
 | 
			
		||||
@@ -189,6 +189,7 @@ void nvim_ui_attach(uint64_t channel_id, Integer width, Integer height, Dict opt
 | 
			
		||||
  ui->wildmenu_active = false;
 | 
			
		||||
 | 
			
		||||
  pmap_put(uint64_t)(&connected_uis, channel_id, ui);
 | 
			
		||||
  current_ui = channel_id;
 | 
			
		||||
  ui_attach_impl(ui, channel_id);
 | 
			
		||||
 | 
			
		||||
  may_trigger_vim_suspend_resume(false);
 | 
			
		||||
@@ -214,6 +215,7 @@ void nvim_ui_set_focus(uint64_t channel_id, Boolean gained, Error *error)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (gained) {
 | 
			
		||||
    current_ui = channel_id;
 | 
			
		||||
    may_trigger_vim_suspend_resume(false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -359,11 +359,11 @@ void nvim_feedkeys(String keys, String mode, Boolean escape_ks)
 | 
			
		||||
/// @param keys to be typed
 | 
			
		||||
/// @return Number of bytes actually written (can be fewer than
 | 
			
		||||
///         requested if the buffer becomes full).
 | 
			
		||||
Integer nvim_input(String keys)
 | 
			
		||||
Integer nvim_input(uint64_t channel_id, String keys)
 | 
			
		||||
  FUNC_API_SINCE(1) FUNC_API_FAST
 | 
			
		||||
{
 | 
			
		||||
  may_trigger_vim_suspend_resume(false);
 | 
			
		||||
  return (Integer)input_enqueue(keys);
 | 
			
		||||
  return (Integer)input_enqueue(channel_id, keys);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Send mouse event from GUI.
 | 
			
		||||
@@ -1485,7 +1485,8 @@ Array nvim_get_api_info(uint64_t channel_id, Arena *arena)
 | 
			
		||||
  return rv;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Self-identifies the client. Sets the `client` object returned by |nvim_get_chan_info()|.
 | 
			
		||||
/// Self-identifies the client, and sets optional flags on the channel. Defines the `client` object
 | 
			
		||||
/// returned by |nvim_get_chan_info()|.
 | 
			
		||||
///
 | 
			
		||||
/// Clients should call this just after connecting, to provide hints for debugging and
 | 
			
		||||
/// orchestration. (Note: Something is better than nothing! Fields are optional, but at least set
 | 
			
		||||
 
 | 
			
		||||
@@ -219,6 +219,7 @@ Channel *channel_alloc(ChannelStreamType type)
 | 
			
		||||
  chan->refcount = 1;
 | 
			
		||||
  chan->exit_status = -1;
 | 
			
		||||
  chan->streamtype = type;
 | 
			
		||||
  chan->detach = false;
 | 
			
		||||
  assert(chan->id <= VARNUMBER_MAX);
 | 
			
		||||
  pmap_put(uint64_t)(&channels, chan->id, chan);
 | 
			
		||||
  return chan;
 | 
			
		||||
 
 | 
			
		||||
@@ -31,6 +31,9 @@ struct Channel {
 | 
			
		||||
  } stream;
 | 
			
		||||
 | 
			
		||||
  bool is_rpc;
 | 
			
		||||
  bool detach;  ///< Prevents self-exit on channel-close. Normally, Nvim self-exits if its primary
 | 
			
		||||
                ///< RPC channel is closed, unless detach=true. Note: currently, detach=false does
 | 
			
		||||
                ///< not FORCE self-exit.
 | 
			
		||||
  RpcState rpc;
 | 
			
		||||
  Terminal *term;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -315,10 +315,8 @@ static void decref(Proc *proc)
 | 
			
		||||
static void proc_close(Proc *proc)
 | 
			
		||||
  FUNC_ATTR_NONNULL_ARG(1)
 | 
			
		||||
{
 | 
			
		||||
  if (proc_is_tearing_down && (proc->detach || proc->type == kProcTypePty)
 | 
			
		||||
      && proc->closed) {
 | 
			
		||||
    // If a detached/pty process dies while tearing down it might get closed
 | 
			
		||||
    // twice.
 | 
			
		||||
  if (proc_is_tearing_down && proc->closed && (proc->detach || proc->type == kProcTypePty)) {
 | 
			
		||||
    // If a detached/pty process dies while tearing down it might get closed twice.
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  assert(!proc->closed);
 | 
			
		||||
@@ -427,20 +425,22 @@ static void exit_event(void **argv)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void exit_from_channel(int status)
 | 
			
		||||
/// Performs self-exit because the primary RPC channel was closed.
 | 
			
		||||
void exit_on_closed_chan(int status)
 | 
			
		||||
{
 | 
			
		||||
  DLOG("self-exit triggered by closed RPC channel...");
 | 
			
		||||
  multiqueue_put(main_loop.fast_events, exit_event, (void *)(intptr_t)status);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void on_proc_exit(Proc *proc)
 | 
			
		||||
{
 | 
			
		||||
  Loop *loop = proc->loop;
 | 
			
		||||
  ILOG("exited: pid=%d status=%d stoptime=%" PRIu64, proc->pid, proc->status,
 | 
			
		||||
       proc->stopped_time);
 | 
			
		||||
  ILOG("child exited: pid=%d status=%d" PRIu64, proc->pid, proc->status);
 | 
			
		||||
 | 
			
		||||
  if (ui_client_channel_id) {
 | 
			
		||||
    exit_from_channel(proc->status);
 | 
			
		||||
  }
 | 
			
		||||
  // XXX: This assumes the TUI never spawns any other processes...?
 | 
			
		||||
  // if (ui_client_channel_id) {
 | 
			
		||||
  //   exit_on_closed_chan(proc->status);
 | 
			
		||||
  // }
 | 
			
		||||
 | 
			
		||||
  // Process has terminated, but there could still be data to be read from the
 | 
			
		||||
  // OS. We are still in the libuv loop, so we cannot call code that polls for
 | 
			
		||||
 
 | 
			
		||||
@@ -104,9 +104,10 @@ void stream_may_close(Stream *stream, bool rstream)
 | 
			
		||||
  if (stream->closed) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  assert(!stream->closed);
 | 
			
		||||
  DLOG("closing Stream: %p", (void *)stream);
 | 
			
		||||
  stream->closed = true;
 | 
			
		||||
  // TODO(justinmk): stream->close_cb is never actually invoked. Either remove it, or see if it can
 | 
			
		||||
  // be used somewhere...
 | 
			
		||||
  stream->close_cb = NULL;
 | 
			
		||||
  stream->close_cb_data = NULL;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -732,6 +732,12 @@ M.cmds = {
 | 
			
		||||
    addr_type = 'ADDR_NONE',
 | 
			
		||||
    func = 'ex_delfunction',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    command = 'detach',
 | 
			
		||||
    flags = bit.bor(BANG, FILES, CMDARG, ARGOPT, TRLBAR, CMDWIN, LOCK_OK),
 | 
			
		||||
    addr_type = 'ADDR_NONE',
 | 
			
		||||
    func = 'ex_detach',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    command = 'display',
 | 
			
		||||
    flags = bit.bor(EXTRA, NOTRLCOM, TRLBAR, SBOXOK, CMDWIN, LOCK_OK),
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@
 | 
			
		||||
#include "auto/config.h"
 | 
			
		||||
#include "nvim/api/private/defs.h"
 | 
			
		||||
#include "nvim/api/private/helpers.h"
 | 
			
		||||
#include "nvim/api/ui.h"
 | 
			
		||||
#include "nvim/arglist.h"
 | 
			
		||||
#include "nvim/ascii_defs.h"
 | 
			
		||||
#include "nvim/autocmd.h"
 | 
			
		||||
@@ -21,6 +22,7 @@
 | 
			
		||||
#include "nvim/buffer.h"
 | 
			
		||||
#include "nvim/buffer_defs.h"
 | 
			
		||||
#include "nvim/change.h"
 | 
			
		||||
#include "nvim/channel.h"
 | 
			
		||||
#include "nvim/charset.h"
 | 
			
		||||
#include "nvim/cmdexpand.h"
 | 
			
		||||
#include "nvim/cmdexpand_defs.h"
 | 
			
		||||
@@ -67,6 +69,7 @@
 | 
			
		||||
#include "nvim/message.h"
 | 
			
		||||
#include "nvim/mouse.h"
 | 
			
		||||
#include "nvim/move.h"
 | 
			
		||||
#include "nvim/msgpack_rpc/server.h"
 | 
			
		||||
#include "nvim/normal.h"
 | 
			
		||||
#include "nvim/normal_defs.h"
 | 
			
		||||
#include "nvim/ops.h"
 | 
			
		||||
@@ -5530,6 +5533,56 @@ static void ex_tabs(exarg_T *eap)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// ":detach"
 | 
			
		||||
///
 | 
			
		||||
/// Detaches the current UI.
 | 
			
		||||
///
 | 
			
		||||
/// ":detach!" with bang (!) 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");
 | 
			
		||||
  } else {
 | 
			
		||||
    // 1. (TODO) Send "detach" UI-event (notification only).
 | 
			
		||||
    // 2. Perform server-side `nvim_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);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    chan->detach = true;  // Prevent self-exit on channel-close.
 | 
			
		||||
 | 
			
		||||
    // Server-side UI detach. Doesn't close the channel.
 | 
			
		||||
    Error err2 = ERROR_INIT;
 | 
			
		||||
    nvim_ui_detach(chan->id, &err2);
 | 
			
		||||
    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));
 | 
			
		||||
 | 
			
		||||
    ILOG("detach current_ui=%" PRId64, chan->id);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// ":mode":
 | 
			
		||||
/// If no argument given, get the screen size and redraw.
 | 
			
		||||
static void ex_mode(exarg_T *eap)
 | 
			
		||||
 
 | 
			
		||||
@@ -301,6 +301,8 @@ EXTERN bool garbage_collect_at_exit INIT( = false);
 | 
			
		||||
EXTERN sctx_T current_sctx INIT( = { 0, 0, 0 });
 | 
			
		||||
// ID of the current channel making a client API call
 | 
			
		||||
EXTERN uint64_t current_channel_id INIT( = 0);
 | 
			
		||||
/// Last channel that invoked 'nvim_input` or got FocusGained.
 | 
			
		||||
EXTERN uint64_t current_ui INIT( = 0);
 | 
			
		||||
 | 
			
		||||
EXTERN bool did_source_packages INIT( = false);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -224,8 +224,7 @@ static size_t receive_msgpack(RStream *stream, const char *rbuf, size_t c, void
 | 
			
		||||
  if (eof) {
 | 
			
		||||
    channel_close(channel->id, kChannelPartRpc, NULL);
 | 
			
		||||
    char buf[256];
 | 
			
		||||
    snprintf(buf, sizeof(buf), "ch %" PRIu64 " was closed by the client",
 | 
			
		||||
             channel->id);
 | 
			
		||||
    snprintf(buf, sizeof(buf), "ch %" PRIu64 " was closed by the peer", channel->id);
 | 
			
		||||
    chan_close_with_error(channel, buf, LOGLVL_INF);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -293,7 +292,7 @@ static void parse_msgpack(Channel *channel)
 | 
			
		||||
 | 
			
		||||
      Object res = p->result;
 | 
			
		||||
      if (p->result.type != kObjectTypeArray) {
 | 
			
		||||
        chan_close_with_error(channel, "msgpack-rpc request args has to be an array", LOGLVL_ERR);
 | 
			
		||||
        chan_close_with_error(channel, "msgpack-rpc request args must be an array", LOGLVL_ERR);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      Array arg = res.data.array;
 | 
			
		||||
@@ -487,13 +486,16 @@ void rpc_close(Channel *channel)
 | 
			
		||||
  channel->rpc.closed = true;
 | 
			
		||||
  channel_decref(channel);
 | 
			
		||||
 | 
			
		||||
  if (channel->streamtype == kChannelStreamStdio
 | 
			
		||||
      || (channel->id == ui_client_channel_id && channel->streamtype != kChannelStreamProc)) {
 | 
			
		||||
    if (channel->streamtype == kChannelStreamStdio) {
 | 
			
		||||
      // Avoid hanging when there are no other UIs and a prompt is triggered on exit.
 | 
			
		||||
      remote_ui_disconnect(channel->id);
 | 
			
		||||
  if (ui_client_channel_id && channel->id == ui_client_channel_id) {
 | 
			
		||||
    assert(!channel->detach);  // `Channel.detach` is not currently used by the UI client.
 | 
			
		||||
    exit_on_closed_chan(0);
 | 
			
		||||
  } else if (channel->streamtype == kChannelStreamStdio) {
 | 
			
		||||
    // Avoid hanging when there are no other UIs and a prompt is triggered on exit.
 | 
			
		||||
    remote_ui_disconnect(channel->id);
 | 
			
		||||
 | 
			
		||||
    if (!channel->detach) {
 | 
			
		||||
      exit_on_closed_chan(0);
 | 
			
		||||
    }
 | 
			
		||||
    exit_from_channel(0);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -274,8 +274,10 @@ void input_enqueue_raw(const char *data, size_t size)
 | 
			
		||||
  input_write_pos += to_write;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
size_t input_enqueue(String keys)
 | 
			
		||||
size_t input_enqueue(uint64_t chan_id, String keys)
 | 
			
		||||
{
 | 
			
		||||
  current_ui = chan_id;
 | 
			
		||||
 | 
			
		||||
  const char *ptr = keys.data;
 | 
			
		||||
  const char *end = ptr + keys.size;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -61,9 +61,14 @@ uint64_t ui_client_start_server(int argc, char **argv)
 | 
			
		||||
  CallbackReader on_err = CALLBACK_READER_INIT;
 | 
			
		||||
  on_err.fwd_err = true;
 | 
			
		||||
 | 
			
		||||
#ifdef MSWIN
 | 
			
		||||
  bool detach = false;  // TODO(justinmk): detach=true breaks `tt.setup_child_nvim` tests on Windows.
 | 
			
		||||
#else
 | 
			
		||||
  bool detach = true;
 | 
			
		||||
#endif
 | 
			
		||||
  Channel *channel = channel_job_start(args, get_vim_var_str(VV_PROGPATH),
 | 
			
		||||
                                       CALLBACK_READER_INIT, on_err, CALLBACK_NONE,
 | 
			
		||||
                                       false, true, true, false, kChannelStdinPipe,
 | 
			
		||||
                                       false, true, true, detach, kChannelStdinPipe,
 | 
			
		||||
                                       NULL, 0, 0, NULL, &exit_status);
 | 
			
		||||
  if (!channel) {
 | 
			
		||||
    return 0;
 | 
			
		||||
 
 | 
			
		||||
@@ -1265,12 +1265,16 @@ describe('jobs', function()
 | 
			
		||||
    ]])
 | 
			
		||||
 | 
			
		||||
    feed(':q<CR>')
 | 
			
		||||
    screen:expect([[
 | 
			
		||||
                                                        |
 | 
			
		||||
      [Process exited 0]^                                |
 | 
			
		||||
                                                        |*4
 | 
			
		||||
      {3:-- TERMINAL --}                                    |
 | 
			
		||||
    ]])
 | 
			
		||||
    if is_os('freebsd') then
 | 
			
		||||
      screen:expect { any = vim.pesc('[Process exited 0]') }
 | 
			
		||||
    else
 | 
			
		||||
      screen:expect([[
 | 
			
		||||
                                                          |
 | 
			
		||||
        [Process exited 0]^                                |
 | 
			
		||||
                                                          |*4
 | 
			
		||||
        {3:-- TERMINAL --}                                    |
 | 
			
		||||
      ]])
 | 
			
		||||
    end
 | 
			
		||||
  end)
 | 
			
		||||
end)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -931,6 +931,8 @@ describe('put command', function()
 | 
			
		||||
    end)
 | 
			
		||||
 | 
			
		||||
    it('should ring the bell when deleting if not appropriate', function()
 | 
			
		||||
      t.skip(t.is_os('bsd'), 'crashes on freebsd')
 | 
			
		||||
 | 
			
		||||
      command('goto 2')
 | 
			
		||||
      feed('i<bs><esc>')
 | 
			
		||||
      expect([[
 | 
			
		||||
 
 | 
			
		||||
@@ -33,6 +33,95 @@ local assert_log = t.assert_log
 | 
			
		||||
 | 
			
		||||
local testlog = 'Xtest-tui-log'
 | 
			
		||||
 | 
			
		||||
describe('TUI :detach', function()
 | 
			
		||||
  before_each(function()
 | 
			
		||||
    os.remove(testlog)
 | 
			
		||||
  end)
 | 
			
		||||
  teardown(function()
 | 
			
		||||
    os.remove(testlog)
 | 
			
		||||
  end)
 | 
			
		||||
 | 
			
		||||
  it('does not stop server', function()
 | 
			
		||||
    local server_super = n.clear()
 | 
			
		||||
    local client_super = n.new_session(true)
 | 
			
		||||
    finally(function()
 | 
			
		||||
      server_super:close()
 | 
			
		||||
      client_super:close()
 | 
			
		||||
    end)
 | 
			
		||||
 | 
			
		||||
    local child_server = new_pipename()
 | 
			
		||||
    local screen = tt.setup_child_nvim({
 | 
			
		||||
      '--listen',
 | 
			
		||||
      child_server,
 | 
			
		||||
      '-u',
 | 
			
		||||
      'NONE',
 | 
			
		||||
      '-i',
 | 
			
		||||
      'NONE',
 | 
			
		||||
      '--cmd',
 | 
			
		||||
      'colorscheme vim',
 | 
			
		||||
      '--cmd',
 | 
			
		||||
      nvim_set .. ' notermguicolors laststatus=2 background=dark',
 | 
			
		||||
    }, {
 | 
			
		||||
      env = {
 | 
			
		||||
        NVIM_LOG_FILE = testlog,
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    tt.feed_data('iHello, World')
 | 
			
		||||
    screen:expect {
 | 
			
		||||
      grid = [[
 | 
			
		||||
      Hello, World^                                      |
 | 
			
		||||
      {4:~                                                 }|*3
 | 
			
		||||
      {MATCH:No Name}
 | 
			
		||||
      {3:-- INSERT --}                                      |
 | 
			
		||||
      {3:-- TERMINAL --}                                    |
 | 
			
		||||
    ]],
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    local child_session = n.connect(child_server)
 | 
			
		||||
    finally(function()
 | 
			
		||||
      child_session:request('nvim_command', 'qall!')
 | 
			
		||||
    end)
 | 
			
		||||
    local status, child_uis = child_session:request('nvim_list_uis')
 | 
			
		||||
    assert(status)
 | 
			
		||||
    eq(1, #child_uis)
 | 
			
		||||
 | 
			
		||||
    tt.feed_data('\027\027:detach\013')
 | 
			
		||||
    -- Note: "Process exited" message is misleading; tt.setup_child_nvim() sees the foreground
 | 
			
		||||
    -- process (client) exited, and doesn't know the server is still running?
 | 
			
		||||
    screen:expect {
 | 
			
		||||
      any = [[Process exited 0]],
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    child_uis --[[@type any[] ]] = ({ child_session:request('nvim_list_uis') })[2]
 | 
			
		||||
    eq(0, #child_uis)
 | 
			
		||||
 | 
			
		||||
    -- NOTE: The tt.setup_child_nvim() screen just wraps :terminal, it's not connected to the child.
 | 
			
		||||
    -- To use it again, we need to detach the old one.
 | 
			
		||||
    screen:detach()
 | 
			
		||||
 | 
			
		||||
    -- Edit some text on the headless server.
 | 
			
		||||
    status = (child_session:request('nvim_input', 'ddiWe did it, pooky.<Esc><Esc>'))
 | 
			
		||||
    assert(status)
 | 
			
		||||
 | 
			
		||||
    -- Test reattach by connecting a new TUI.
 | 
			
		||||
    local screen_reattached = tt.setup_child_nvim({
 | 
			
		||||
      '--remote-ui',
 | 
			
		||||
      '--server',
 | 
			
		||||
      child_server,
 | 
			
		||||
    })
 | 
			
		||||
    screen_reattached:expect {
 | 
			
		||||
      grid = [[
 | 
			
		||||
      We did it, pooky^.                                 |
 | 
			
		||||
      {4:~                                                 }|*3
 | 
			
		||||
      {5:[No Name] [+]                                     }|
 | 
			
		||||
                                                        |
 | 
			
		||||
      {3:-- TERMINAL --}                                    |
 | 
			
		||||
    ]],
 | 
			
		||||
    }
 | 
			
		||||
  end)
 | 
			
		||||
end)
 | 
			
		||||
 | 
			
		||||
if t.skip(is_os('win')) then
 | 
			
		||||
  return
 | 
			
		||||
end
 | 
			
		||||
@@ -3384,9 +3473,9 @@ describe('TUI client', function()
 | 
			
		||||
 | 
			
		||||
    set_session(client_super)
 | 
			
		||||
    local screen_client = tt.setup_child_nvim({
 | 
			
		||||
      '--remote-ui',
 | 
			
		||||
      '--server',
 | 
			
		||||
      server_pipe,
 | 
			
		||||
      '--remote-ui',
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    screen_client:expect {
 | 
			
		||||
@@ -3428,9 +3517,9 @@ describe('TUI client', function()
 | 
			
		||||
 | 
			
		||||
    set_session(client_super)
 | 
			
		||||
    local screen_client = tt.setup_child_nvim({
 | 
			
		||||
      '--remote-ui',
 | 
			
		||||
      '--server',
 | 
			
		||||
      server_pipe,
 | 
			
		||||
      '--remote-ui',
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    screen_client:expect {
 | 
			
		||||
@@ -3457,7 +3546,7 @@ describe('TUI client', function()
 | 
			
		||||
 | 
			
		||||
    eq(0, api.nvim_get_vvar('shell_error'))
 | 
			
		||||
    -- exits on input eof #22244
 | 
			
		||||
    fn.system({ nvim_prog, '--server', server_pipe, '--remote-ui' })
 | 
			
		||||
    fn.system({ nvim_prog, '--remote-ui', '--server', server_pipe })
 | 
			
		||||
    eq(1, api.nvim_get_vvar('shell_error'))
 | 
			
		||||
 | 
			
		||||
    client_super:close()
 | 
			
		||||
@@ -3470,9 +3559,9 @@ describe('TUI client', function()
 | 
			
		||||
  it('throws error when no server exists', function()
 | 
			
		||||
    clear()
 | 
			
		||||
    local screen = tt.setup_child_nvim({
 | 
			
		||||
      '--remote-ui',
 | 
			
		||||
      '--server',
 | 
			
		||||
      '127.0.0.1:2436546',
 | 
			
		||||
      '--remote-ui',
 | 
			
		||||
    }, { cols = 60 })
 | 
			
		||||
 | 
			
		||||
    screen:expect([[
 | 
			
		||||
@@ -3485,10 +3574,13 @@ describe('TUI client', function()
 | 
			
		||||
  end)
 | 
			
		||||
 | 
			
		||||
  local function test_remote_tui_quit(status)
 | 
			
		||||
    local server_super = n.new_session(false)
 | 
			
		||||
    local server_super = n.clear()
 | 
			
		||||
    local client_super = n.new_session(true)
 | 
			
		||||
    finally(function()
 | 
			
		||||
      server_super:close()
 | 
			
		||||
      client_super:close()
 | 
			
		||||
    end)
 | 
			
		||||
 | 
			
		||||
    set_session(server_super)
 | 
			
		||||
    local server_pipe = new_pipename()
 | 
			
		||||
    local screen_server = tt.setup_child_nvim({
 | 
			
		||||
      '--listen',
 | 
			
		||||
@@ -3535,9 +3627,9 @@ describe('TUI client', function()
 | 
			
		||||
 | 
			
		||||
    set_session(client_super)
 | 
			
		||||
    local screen_client = tt.setup_child_nvim({
 | 
			
		||||
      '--remote-ui',
 | 
			
		||||
      '--server',
 | 
			
		||||
      server_pipe,
 | 
			
		||||
      '--remote-ui',
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    screen_client:expect {
 | 
			
		||||
@@ -3554,26 +3646,8 @@ describe('TUI client', function()
 | 
			
		||||
    set_session(server_super)
 | 
			
		||||
    feed_data(status and ':' .. status .. 'cquit!\n' or ':quit!\n')
 | 
			
		||||
    status = status and status or 0
 | 
			
		||||
    screen_server:expect {
 | 
			
		||||
      grid = [[
 | 
			
		||||
                                                        |
 | 
			
		||||
      [Process exited ]] .. status .. [[]^ {MATCH:%s+}|
 | 
			
		||||
                                                        |*4
 | 
			
		||||
      {3:-- TERMINAL --}                                    |
 | 
			
		||||
    ]],
 | 
			
		||||
    }
 | 
			
		||||
    -- assert that client has exited
 | 
			
		||||
    screen_client:expect {
 | 
			
		||||
      grid = [[
 | 
			
		||||
                                                        |
 | 
			
		||||
      [Process exited ]] .. status .. [[]^ {MATCH:%s+}|
 | 
			
		||||
                                                        |*4
 | 
			
		||||
      {3:-- TERMINAL --}                                    |
 | 
			
		||||
    ]],
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    server_super:close()
 | 
			
		||||
    client_super:close()
 | 
			
		||||
    screen_server:expect({ any = 'Process exited ' .. status })
 | 
			
		||||
    screen_client:expect({ any = 'Process exited ' .. status })
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe('exits when server quits', function()
 | 
			
		||||
 
 | 
			
		||||
@@ -908,8 +908,10 @@ function M.is_asan()
 | 
			
		||||
  return version:match('-fsanitize=[a-z,]*address')
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
-- Returns a valid, platform-independent Nvim listen address.
 | 
			
		||||
-- Useful for communicating with child instances.
 | 
			
		||||
--- Returns a valid, platform-independent Nvim listen address.
 | 
			
		||||
--- Useful for communicating with child instances.
 | 
			
		||||
---
 | 
			
		||||
--- @return string
 | 
			
		||||
function M.new_pipename()
 | 
			
		||||
  -- HACK: Start a server temporarily, get the name, then stop it.
 | 
			
		||||
  local pipename = M.eval('serverstart()')
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user