Merge pull request #18357 from bfredl/ui_stdin

feat(ui): allow embedder to emulate "cat data | nvim -" behaviour
This commit is contained in:
bfredl 2022-05-02 23:24:37 +02:00 committed by GitHub
commit ddf7bb24f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 106 additions and 17 deletions

View File

@ -52,6 +52,11 @@ with these (optional) keys:
`term_name` Sets the name of the terminal 'term'. `term_name` Sets the name of the terminal 'term'.
`term_colors` Sets the number of supported colors 't_Co'. `term_colors` Sets the number of supported colors 't_Co'.
`term_background` Sets the default value of 'background'. `term_background` Sets the default value of 'background'.
`stdin_fd` Read buffer from `fd` as if it was a stdin pipe
This option can only used by |--embed| ui,
see |ui-startup-stdin|.
Specifying an unknown option is an error; UIs can check the |api-metadata| Specifying an unknown option is an error; UIs can check the |api-metadata|
`ui_options` key for supported options. `ui_options` key for supported options.
@ -140,6 +145,19 @@ procedure:
Inside this request handler, the UI can safely do any initialization before Inside this request handler, the UI can safely do any initialization before
entering normal mode, for example reading variables set by init.vim. entering normal mode, for example reading variables set by init.vim.
*ui-startup-stdin*
An UI can support the native read from stdin feature as invoked with
`command | nvim -` for the builtin TUI. |--|
The embedding process can detect that its stdin is open to a file which
not is a terminal, just like nvim does. It then needs to forward this fd
to Nvim. As fd=0 is already is used to send rpc data from the embedder to
Nvim, it needs to use some other file descriptor, like fd=3 or higher.
Then, `stdin_fd` option should be passed to `nvim_ui_attach` and nvim will
implicitly read it as a buffer. This option can only be used when Nvim is
launched with `--embed` option, as described above.
============================================================================== ==============================================================================
Global Events *ui-global* Global Events *ui-global*

View File

@ -283,6 +283,22 @@ static void ui_set_option(UI *ui, bool init, String name, Object value, Error *e
return; return;
} }
if (strequal(name.data, "stdin_fd")) {
if (value.type != kObjectTypeInteger || value.data.integer < 0) {
api_set_error(error, kErrorTypeValidation, "stdin_fd must be a non-negative Integer");
return;
}
if (starting != NO_SCREEN) {
api_set_error(error, kErrorTypeValidation,
"stdin_fd can only be used with first attached ui");
return;
}
stdin_fd = (int)value.data.integer;
return;
}
// LEGACY: Deprecated option, use `ext_cmdline` instead. // LEGACY: Deprecated option, use `ext_cmdline` instead.
bool is_popupmenu = strequal(name.data, "popupmenu_external"); bool is_popupmenu = strequal(name.data, "popupmenu_external");

View File

@ -176,7 +176,7 @@ void filemess(buf_T *buf, char_u *name, char_u *s, int attr)
int readfile(char_u *fname, char_u *sfname, linenr_T from, linenr_T lines_to_skip, int readfile(char_u *fname, char_u *sfname, linenr_T from, linenr_T lines_to_skip,
linenr_T lines_to_read, exarg_T *eap, int flags, bool silent) linenr_T lines_to_read, exarg_T *eap, int flags, bool silent)
{ {
int fd = 0; int fd = stdin_fd >= 0 ? stdin_fd : 0;
int newfile = (flags & READ_NEW); int newfile = (flags & READ_NEW);
int check_readonly; int check_readonly;
int filtering = (flags & READ_FILTER); int filtering = (flags & READ_FILTER);
@ -1722,17 +1722,19 @@ failed:
xfree(buffer); xfree(buffer);
if (read_stdin) { if (read_stdin) {
close(0); close(fd);
if (stdin_fd < 0) {
#ifndef WIN32 #ifndef WIN32
// On Unix, use stderr for stdin, makes shell commands work. // On Unix, use stderr for stdin, makes shell commands work.
vim_ignored = dup(2); vim_ignored = dup(2);
#else #else
// On Windows, use the console input handle for stdin. // On Windows, use the console input handle for stdin.
HANDLE conin = CreateFile("CONIN$", GENERIC_READ | GENERIC_WRITE, HANDLE conin = CreateFile("CONIN$", GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ, (LPSECURITY_ATTRIBUTES)NULL, FILE_SHARE_READ, (LPSECURITY_ATTRIBUTES)NULL,
OPEN_EXISTING, 0, (HANDLE)NULL); OPEN_EXISTING, 0, (HANDLE)NULL);
vim_ignored = _open_osfhandle(conin, _O_RDONLY); vim_ignored = _open_osfhandle(conin, _O_RDONLY);
#endif #endif
}
} }
if (tmpname != NULL) { if (tmpname != NULL) {

View File

@ -507,6 +507,9 @@ EXTERN int v_dying INIT(= 0);
EXTERN int stdin_isatty INIT(= true); EXTERN int stdin_isatty INIT(= true);
// is stdout a terminal? // is stdout a terminal?
EXTERN int stdout_isatty INIT(= true); EXTERN int stdout_isatty INIT(= true);
/// filedesc set by embedder for reading first buffer like `cmd | nvim -`
EXTERN int stdin_fd INIT(= -1);
// true when doing full-screen output, otherwise only writing some messages. // true when doing full-screen output, otherwise only writing some messages.
// volatile because it is used in a signal handler. // volatile because it is used in a signal handler.
EXTERN volatile int full_screen INIT(= false); EXTERN volatile int full_screen INIT(= false);
@ -847,7 +850,6 @@ EXTERN linenr_T printer_page_num;
EXTERN bool typebuf_was_filled INIT(= false); // received text from client EXTERN bool typebuf_was_filled INIT(= false); // received text from client
// or from feedkeys() // or from feedkeys()
#ifdef BACKSLASH_IN_FILENAME #ifdef BACKSLASH_IN_FILENAME
EXTERN char psepc INIT(= '\\'); // normal path separator character EXTERN char psepc INIT(= '\\'); // normal path separator character
EXTERN char psepcN INIT(= '/'); // abnormal path separator character EXTERN char psepcN INIT(= '/'); // abnormal path separator character

View File

@ -454,7 +454,7 @@ int main(int argc, char **argv)
// writing end of the pipe doesn't like, e.g., in case stdin and stderr // writing end of the pipe doesn't like, e.g., in case stdin and stderr
// are the same terminal: "cat | vim -". // are the same terminal: "cat | vim -".
// Using autocommands here may cause trouble... // Using autocommands here may cause trouble...
if (params.edit_type == EDIT_STDIN && !recoverymode) { if ((params.edit_type == EDIT_STDIN || stdin_fd >= 0) && !recoverymode) {
read_stdin(); read_stdin();
} }

View File

@ -361,14 +361,15 @@ local function remove_args(args, args_rm)
return new_args return new_args
end end
function module.spawn(argv, merge, env, keep) --- @param io_extra used for stdin_fd, see :help ui-option
function module.spawn(argv, merge, env, keep, io_extra)
if session and not keep then if session and not keep then
session:close() session:close()
end end
local child_stream = ChildProcessStream.spawn( local child_stream = ChildProcessStream.spawn(
merge and module.merge_args(prepend_argv, argv) or argv, merge and module.merge_args(prepend_argv, argv) or argv,
env) env, io_extra)
return Session.new(child_stream) return Session.new(child_stream)
end end
@ -415,8 +416,8 @@ end
-- clear('-e') -- clear('-e')
-- clear{args={'-e'}, args_rm={'-i'}, env={TERM=term}} -- clear{args={'-e'}, args_rm={'-i'}, env={TERM=term}}
function module.clear(...) function module.clear(...)
local argv, env = module.new_argv(...) local argv, env, io_extra = module.new_argv(...)
module.set_session(module.spawn(argv, nil, env)) module.set_session(module.spawn(argv, nil, env, nil, io_extra))
end end
-- Builds an argument list for use in clear(). -- Builds an argument list for use in clear().
@ -426,6 +427,7 @@ function module.new_argv(...)
local args = {unpack(module.nvim_argv)} local args = {unpack(module.nvim_argv)}
table.insert(args, '--headless') table.insert(args, '--headless')
local new_args local new_args
local io_extra
local env = nil local env = nil
local opts = select(1, ...) local opts = select(1, ...)
if type(opts) == 'table' then if type(opts) == 'table' then
@ -461,13 +463,14 @@ function module.new_argv(...)
end end
end end
new_args = opts.args or {} new_args = opts.args or {}
io_extra = opts.io_extra
else else
new_args = {...} new_args = {...}
end end
for _, arg in ipairs(new_args) do for _, arg in ipairs(new_args) do
table.insert(args, arg) table.insert(args, arg)
end end
return args, env return args, env, io_extra
end end
function module.insert(...) function module.insert(...)

View File

@ -1,3 +1,5 @@
local uv = require'luv'
local helpers = require('test.functional.helpers')(after_each) local helpers = require('test.functional.helpers')(after_each)
local Screen = require('test.functional.ui.screen') local Screen = require('test.functional.ui.screen')
@ -98,3 +100,49 @@ end
describe('--embed UI on startup (ext_linegrid=true)', function() test_embed(true) end) describe('--embed UI on startup (ext_linegrid=true)', function() test_embed(true) end)
describe('--embed UI on startup (ext_linegrid=false)', function() test_embed(false) end) describe('--embed UI on startup (ext_linegrid=false)', function() test_embed(false) end)
describe('--embed UI', function()
it('can pass stdin', function()
local pipe = assert(uv.pipe())
local writer = assert(uv.new_pipe(false))
writer:open(pipe.write)
clear {args_rm={'--headless'}, io_extra=pipe.read}
-- attach immediately after startup, for early UI
local screen = Screen.new(40, 8)
screen:attach {stdin_fd=3}
screen:set_default_attr_ids {
[1] = {bold = true, foreground = Screen.colors.Blue1};
[2] = {bold = true};
}
writer:write "hello nvim\nfrom external input\n"
writer:shutdown(function() writer:close() end)
screen:expect{grid=[[
^hello nvim |
from external input |
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
|
]]}
-- stdin (rpc input) still works
feed 'o'
screen:expect{grid=[[
hello nvim |
^ |
from external input |
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{2:-- INSERT --} |
]]}
end)
end)

View File

@ -225,7 +225,7 @@ if(USE_BUNDLED_BUSTED)
# nvim-client: https://github.com/neovim/lua-client # nvim-client: https://github.com/neovim/lua-client
add_custom_command(OUTPUT ${ROCKS_DIR}/nvim-client add_custom_command(OUTPUT ${ROCKS_DIR}/nvim-client
COMMAND ${LUAROCKS_BINARY} COMMAND ${LUAROCKS_BINARY}
ARGS build nvim-client 0.2.2-1 ${LUAROCKS_BUILDARGS} ARGS build nvim-client 0.2.3-1 ${LUAROCKS_BUILDARGS}
DEPENDS luv) DEPENDS luv)
add_custom_target(nvim-client DEPENDS ${ROCKS_DIR}/nvim-client) add_custom_target(nvim-client DEPENDS ${ROCKS_DIR}/nvim-client)