Merge pull request #17439 from groves/1750

feat: restore --remote
This commit is contained in:
bfredl 2022-03-11 19:47:24 +01:00 committed by GitHub
commit 965f1fd6fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 441 additions and 1 deletions

131
runtime/doc/remote.txt Normal file
View File

@ -0,0 +1,131 @@
*remote.txt* Nvim
VIM REFERENCE MANUAL by Bram Moolenaar
Vim client-server communication *client-server*
Type |gO| to see the table of contents.
==============================================================================
1. Common functionality *clientserver*
Nvim's |RPC| functionality allows clients to programmatically control Nvim. Nvim
itself takes command-line arguments that cause it to become a client to another
Nvim running as a server. These arguments match those provided by Vim's
clientserver option.
The following command line arguments are available:
argument meaning ~
--remote [+{cmd}] {file} ... *--remote*
Open the file list in a remote Vim. When
there is no Vim server, execute locally.
Vim allows one init command: +{cmd}.
This must be an Ex command that can be
followed by "|". It's not yet supported by
Nvim.
The rest of the command line is taken as the
file list. Thus any non-file arguments must
come before this.
You cannot edit stdin this way |--|.
The remote Vim is raised. If you don't want
this use >
nvim --remote-send "<C-\><C-N>:n filename<CR>"
<
--remote-silent [+{cmd}] {file} ... *--remote-silent*
As above, but don't complain if there is no
server and the file is edited locally.
*--remote-tab*
--remote-tab Like --remote but open each file in a new
tabpage.
*--remote-tab-silent*
--remote-tab-silent Like --remote-silent but open each file in a
new tabpage.
*--remote-send*
--remote-send {keys} Send {keys} to server and exit. The {keys}
are not mapped. Special key names are
recognized, e.g., "<CR>" results in a CR
character.
*--remote-expr*
--remote-expr {expr} Evaluate {expr} in server and print the result
on stdout.
*--server*
--server {addr} Connect to the named pipe or socket at the
given address for executing remote commands.
See |--listen| for specifying an address when
starting a server.
Examples ~
Start an Nvim server listening on a named pipe at '~/.cache/nvim/server.pipe': >
nvim --listen ~/.cache/nvim/server.pipe
Edit "file.txt" in an Nvim server listening at '~/.cache/nvim/server.pipe': >
nvim --server ~/.cache/nvim/server.pipe --remote file.txt
This doesn't work, all arguments after --remote will be used as file names: >
nvim --remote --server ~/.cache/nvim/server.pipe file.txt
Tell the remote server to write all files and exit: >
nvim --server ~/.cache/nvim/server.pipe --remote-send '<C-\><C-N>:wqa<CR>'
REMOTE EDITING
The --remote argument will cause a |:drop| command to be constructed from the
rest of the command line and sent as described above.
Note that the --remote and --remote-wait arguments will consume the rest of
the command line. I.e. all remaining arguments will be regarded as filenames.
You can not put options there!
==============================================================================
2. Missing functionality *E5600* *clientserver-missing*
Vim supports additional functionality in clientserver that's not yet
implemented in Nvim. In particular, none of the 'wait' variants are supported
yet. The following command line arguments are not yet available:
argument meaning ~
--remote-wait [+{cmd}] {file} ... *--remote-wait*
Not yet supported by Nvim.
As --remote, but wait for files to complete
(unload) in remote Vim.
--remote-wait-silent [+{cmd}] {file} ... *--remote-wait-silent*
Not yet supported by Nvim.
As --remote-wait, but don't complain if there
is no server.
*--remote-tab-wait*
--remote-tab-wait Not yet supported by Nvim.
Like --remote-wait but open each file in a new
tabpage.
*--remote-tab-wait-silent*
--remote-tab-wait-silent Not yet supported by Nvim.
Like --remote-wait-silent but open each file
in a new tabpage.
*--servername*
--servername {name} Not yet supported by Nvim.
Become the server {name}. When used together
with one of the --remote commands: connect to
server {name} instead of the default (see
below). The name used will be uppercase.
*--serverlist*
--serverlist Not yet supported by Nvim.
Output a list of server names.
SERVER NAME *client-server-name*
By default Vim will try to register the name under which it was invoked (gvim,
egvim ...). This can be overridden with the --servername argument. Nvim
either listens on a named pipe or a socket and does not yet support this
--servername functionality.
vim:tw=78:sw=4:ts=8:noet:ft=help:norl:

View File

@ -636,6 +636,68 @@ function vim.pretty_print(...)
return ...
end
function vim._cs_remote(rcid, server_addr, connect_error, args)
local function connection_failure_errmsg(consequence)
local explanation
if server_addr == '' then
explanation = "No server specified with --server"
else
explanation = "Failed to connect to '" .. server_addr .. "'"
if connect_error ~= "" then
explanation = explanation .. ": " .. connect_error
end
end
return "E247: " .. explanation .. ". " .. consequence
end
local f_silent = false
local f_tab = false
local subcmd = string.sub(args[1],10)
if subcmd == 'tab' then
f_tab = true
elseif subcmd == 'silent' then
f_silent = true
elseif subcmd == 'wait' or subcmd == 'wait-silent' or subcmd == 'tab-wait' or subcmd == 'tab-wait-silent' then
return { errmsg = 'E5600: Wait commands not yet implemented in nvim' }
elseif subcmd == 'tab-silent' then
f_tab = true
f_silent = true
elseif subcmd == 'send' then
if rcid == 0 then
return { errmsg = connection_failure_errmsg('Send failed.') }
end
vim.fn.rpcrequest(rcid, 'nvim_input', args[2])
return { should_exit = true, tabbed = false }
elseif subcmd == 'expr' then
if rcid == 0 then
return { errmsg = connection_failure_errmsg('Send expression failed.') }
end
print(vim.fn.rpcrequest(rcid, 'nvim_eval', args[2]))
return { should_exit = true, tabbed = false }
elseif subcmd ~= '' then
return { errmsg='Unknown option argument: ' .. args[1] }
end
if rcid == 0 then
if not f_silent then
vim.notify(connection_failure_errmsg("Editing locally"), vim.log.levels.WARN)
end
else
local command = {}
if f_tab then table.insert(command, 'tab') end
table.insert(command, 'drop')
for i = 2, #args do
table.insert(command, vim.fn.fnameescape(args[i]))
end
vim.fn.rpcrequest(rcid, 'nvim_command', table.concat(command, ' '))
end
return {
should_exit = rcid ~= 0,
tabbed = f_tab,
}
end
require('vim._meta')

View File

@ -1997,7 +1997,7 @@ Array nvim_get_proc_children(Integer pid, Error *err)
DLOG("fallback to vim._os_proc_children()");
Array a = ARRAY_DICT_INIT;
ADD(a, INTEGER_OBJ(pid));
String s = cstr_to_string("return vim._os_proc_children(select(1, ...))");
String s = cstr_to_string("return vim._os_proc_children(...)");
Object o = nlua_exec(s, a, err);
api_free_string(s);
api_free_array(a);

View File

@ -268,6 +268,10 @@ int main(int argc, char **argv)
}
server_init(params.listen_addr);
if (params.remote) {
handle_remote_client(&params, params.remote,
params.server_addr, argc, argv);
}
if (GARGCOUNT > 0) {
fname = get_fname(&params, cwd);
@ -803,6 +807,91 @@ static void init_locale(void)
}
#endif
/// Handle remote subcommands
static void handle_remote_client(mparm_T *params, int remote_args,
char *server_addr, int argc, char **argv)
{
Object rvobj = OBJECT_INIT;
rvobj.data.dictionary = (Dictionary)ARRAY_DICT_INIT;
rvobj.type = kObjectTypeDictionary;
CallbackReader on_data = CALLBACK_READER_INIT;
const char *connect_error = NULL;
uint64_t rc_id = 0;
if (server_addr != NULL) {
rc_id = channel_connect(false, server_addr, true, on_data, 50, &connect_error);
}
int t_argc = remote_args;
Array args = ARRAY_DICT_INIT;
String arg_s;
for (; t_argc < argc; t_argc++) {
arg_s = cstr_to_string(argv[t_argc]);
ADD(args, STRING_OBJ(arg_s));
}
Error err = ERROR_INIT;
Array a = ARRAY_DICT_INIT;
ADD(a, INTEGER_OBJ((int)rc_id));
ADD(a, CSTR_TO_OBJ(server_addr));
ADD(a, CSTR_TO_OBJ(connect_error));
ADD(a, ARRAY_OBJ(args));
String s = STATIC_CSTR_AS_STRING("return vim._cs_remote(...)");
Object o = nlua_exec(s, a, &err);
api_free_array(a);
if (ERROR_SET(&err)) {
mch_errmsg(err.msg);
mch_errmsg("\n");
os_exit(2);
}
if (o.type == kObjectTypeDictionary) {
rvobj.data.dictionary = o.data.dictionary;
} else {
mch_errmsg("vim._cs_remote returned unexpected value\n");
os_exit(2);
}
TriState should_exit = kNone;
TriState tabbed = kNone;
for (size_t i = 0; i < rvobj.data.dictionary.size ; i++) {
if (strcmp(rvobj.data.dictionary.items[i].key.data, "errmsg") == 0) {
if (rvobj.data.dictionary.items[i].value.type != kObjectTypeString) {
mch_errmsg("vim._cs_remote returned an unexpected type for 'errmsg'\n");
os_exit(2);
}
mch_errmsg(rvobj.data.dictionary.items[i].value.data.string.data);
mch_errmsg("\n");
os_exit(2);
} else if (strcmp(rvobj.data.dictionary.items[i].key.data, "tabbed") == 0) {
if (rvobj.data.dictionary.items[i].value.type != kObjectTypeBoolean) {
mch_errmsg("vim._cs_remote returned an unexpected type for 'tabbed'\n");
os_exit(2);
}
tabbed = rvobj.data.dictionary.items[i].value.data.boolean ? kTrue : kFalse;
} else if (strcmp(rvobj.data.dictionary.items[i].key.data, "should_exit") == 0) {
if (rvobj.data.dictionary.items[i].value.type != kObjectTypeBoolean) {
mch_errmsg("vim._cs_remote returned an unexpected type for 'should_exit'\n");
os_exit(2);
}
should_exit = rvobj.data.dictionary.items[i].value.data.boolean ? kTrue : kFalse;
}
}
if (should_exit == kNone || tabbed == kNone) {
mch_errmsg("vim._cs_remote didn't return a value for should_exit or tabbed, bailing\n");
os_exit(2);
}
api_free_object(o);
if (should_exit == kTrue) {
os_exit(0);
}
if (tabbed == kTrue) {
params->window_count = argc - remote_args - 1;
params->window_layout = WIN_TABS;
}
}
/// Decides whether text (as opposed to commands) will be read from stdin.
/// @see EDIT_STDIN
static bool edit_stdin(bool explicit, mparm_T *parmp)
@ -868,6 +957,8 @@ static void command_line_scan(mparm_T *parmp)
// "--version" give version message
// "--noplugin[s]" skip plugins
// "--cmd <cmd>" execute cmd before vimrc
// "--remote" execute commands remotey on a server
// "--server" name of vim server to send remote commands to
if (STRICMP(argv[0] + argv_idx, "help") == 0) {
usage();
os_exit(0);
@ -906,6 +997,11 @@ static void command_line_scan(mparm_T *parmp)
argv_idx += 6;
} else if (STRNICMP(argv[0] + argv_idx, "literal", 7) == 0) {
// Do nothing: file args are always literal. #7679
} else if (STRNICMP(argv[0] + argv_idx, "remote", 6) == 0) {
parmp->remote = parmp->argc - argc;
} else if (STRNICMP(argv[0] + argv_idx, "server", 6) == 0) {
want_argument = true;
argv_idx += 6;
} else if (STRNICMP(argv[0] + argv_idx, "noplugin", 8) == 0) {
p_lpl = false;
} else if (STRNICMP(argv[0] + argv_idx, "cmd", 3) == 0) {
@ -1137,6 +1233,9 @@ static void command_line_scan(mparm_T *parmp)
} else if (strequal(argv[-1], "--listen")) {
// "--listen {address}"
parmp->listen_addr = argv[0];
} else if (strequal(argv[-1], "--server")) {
// "--server {address}"
parmp->server_addr = argv[0];
}
// "--startuptime <file>" already handled
break;
@ -1291,6 +1390,8 @@ static void init_params(mparm_T *paramp, int argc, char **argv)
paramp->use_debug_break_level = -1;
paramp->window_count = -1;
paramp->listen_addr = NULL;
paramp->server_addr = NULL;
paramp->remote = 0;
}
/// Initialize global startuptime file if "--startuptime" passed as an argument.
@ -2041,6 +2142,8 @@ static void usage(void)
mch_msg(_(" --headless Don't start a user interface\n"));
mch_msg(_(" --listen <address> Serve RPC API from this address\n"));
mch_msg(_(" --noplugin Don't load plugins\n"));
mch_msg(_(" --remote[-subcommand] Execute commands remotely on a server\n"));
mch_msg(_(" --server <address> Specify RPC server to send commands to\n"));
mch_msg(_(" --startuptime <file> Write startup timing messages to <file>\n"));
mch_msg(_("\nSee \":help startup-options\" for all options.\n"));
}

View File

@ -39,6 +39,8 @@ typedef struct {
int diff_mode; // start with 'diff' set
char *listen_addr; // --listen {address}
int remote; // --remote-[subcmd] {file1} {file2}
char *server_addr; // --server {address}
} mparm_T;
#ifdef INCLUDE_GENERATED_DECLARATIONS

View File

@ -0,0 +1,142 @@
local helpers = require('test.functional.helpers')(after_each)
local clear = helpers.clear
local command = helpers.command
local eq = helpers.eq
local expect = helpers.expect
local funcs = helpers.funcs
local insert = helpers.insert
local meths = helpers.meths
local new_argv = helpers.new_argv
local neq = helpers.neq
local set_session = helpers.set_session
local spawn = helpers.spawn
local tmpname = helpers.tmpname
local write_file = helpers.write_file
describe('Remote', function()
local fname, other_fname
local contents = 'The call is coming from outside the process'
local other_contents = "A second file's contents"
before_each(function()
fname = tmpname() .. ' with spaces in the filename'
other_fname = tmpname()
write_file(fname, contents)
write_file(other_fname, other_contents)
end)
describe('connect to server and', function()
local server
before_each(function()
server = spawn(new_argv(), true)
set_session(server)
end)
after_each(function()
server:close()
end)
local function run_remote(...)
set_session(server)
local addr = funcs.serverlist()[1]
local client_argv = new_argv({args={'--server', addr, ...}})
-- Create an nvim instance just to run the remote-invoking nvim. We want
-- to wait for the remote instance to exit and calling jobwait blocks
-- the event loop. If the server event loop is blocked, it can't process
-- our incoming --remote calls.
local client_starter = spawn(new_argv(), false, nil, true)
set_session(client_starter)
local client_job_id = funcs.jobstart(client_argv)
eq({ 0 }, funcs.jobwait({client_job_id}))
client_starter:close()
set_session(server)
end
it('edit a single file', function()
run_remote('--remote', fname)
expect(contents)
eq(2, #funcs.getbufinfo())
end)
it('tab edit a single file with a non-changed buffer', function()
run_remote('--remote-tab', fname)
expect(contents)
eq(1, #funcs.gettabinfo())
end)
it('tab edit a single file with a changed buffer', function()
insert('hello')
run_remote('--remote-tab', fname)
expect(contents)
eq(2, #funcs.gettabinfo())
end)
it('edit multiple files', function()
run_remote('--remote', fname, other_fname)
expect(contents)
command('next')
expect(other_contents)
eq(3, #funcs.getbufinfo())
end)
it('send keys', function()
run_remote('--remote-send', ':edit '..fname..'<CR><C-W>v')
expect(contents)
eq(2, #funcs.getwininfo())
-- Only a single buffer as we're using edit and not drop like --remote does
eq(1, #funcs.getbufinfo())
end)
it('evaluate expressions', function()
run_remote('--remote-expr', 'setline(1, "Yo")')
expect('Yo')
end)
end)
it('creates server if not found', function()
clear('--remote', fname)
expect(contents)
eq(1, #funcs.getbufinfo())
-- Since we didn't pass silent, we should get a complaint
neq(nil, string.find(meths.exec('messages', true), 'E247'))
end)
it('creates server if not found with tabs', function()
clear('--remote-tab-silent', fname, other_fname)
expect(contents)
eq(2, #funcs.gettabinfo())
eq(2, #funcs.getbufinfo())
-- We passed silent, so no message should be issued about the server not being found
eq(nil, string.find(meths.exec('messages', true), 'E247'))
end)
describe('exits with error on', function()
local function run_and_check_exit_code(...)
local bogus_argv = new_argv(...)
-- Create an nvim instance just to run the remote-invoking nvim. We want
-- to wait for the remote instance to exit and calling jobwait blocks
-- the event loop. If the server event loop is blocked, it can't process
-- our incoming --remote calls.
clear()
local bogus_job_id = funcs.jobstart(bogus_argv)
eq({2}, funcs.jobwait({bogus_job_id}))
end
it('bogus subcommand', function()
run_and_check_exit_code('--remote-bogus')
end)
it('send without server', function()
run_and_check_exit_code('--remote-send', 'i')
end)
it('expr without server', function()
run_and_check_exit_code('--remote-expr', 'setline(1, "Yo")')
end)
it('wait subcommand', function()
run_and_check_exit_code('--remote-wait', fname)
end)
end)
end)