mirror of
https://github.com/neovim/neovim.git
synced 2025-02-25 18:55:25 -06:00
This commit is contained in:
commit
05ae9781b5
@ -4014,8 +4014,15 @@ items({dict}) *items()*
|
||||
|
||||
jobsend({job}, {data}) {Nvim} *jobsend()*
|
||||
Send data to {job} by writing it to the stdin of the process.
|
||||
Returns 1 if the write succeeded, 0 otherwise.
|
||||
See |job-control| for more information.
|
||||
|
||||
{data} may be a string, string convertible, or a list. If
|
||||
{data} is a list, the items will be separated by newlines and
|
||||
any newlines in an item will be sent as a NUL. For example: >
|
||||
:call jobsend(j, ["abc", "123\n456"])
|
||||
< will send "abc<NL>123<NUL>456<NL>".
|
||||
|
||||
jobstart({name}, {prog}[, {argv}]) {Nvim} *jobstart()*
|
||||
Spawns {prog} as a job and associate it with the {name} string,
|
||||
which will be used to match the "filename pattern" in
|
||||
|
@ -47,9 +47,9 @@ event. The best way to understand is with a complete example:
|
||||
|
||||
function JobHandler()
|
||||
if v:job_data[1] == 'stdout'
|
||||
let str = 'shell '. v:job_data[0].' stdout: '.v:job_data[2]
|
||||
let str = 'shell '. v:job_data[0].' stdout: '.join(v:job_data[2])
|
||||
elseif v:job_data[1] == 'stderr'
|
||||
let str = 'shell '.v:job_data[0].' stderr: '.v:job_data[2]
|
||||
let str = 'shell '.v:job_data[0].' stderr: '.join(v:job_data[2])
|
||||
else
|
||||
let str = 'shell '.v:job_data[0].' exited'
|
||||
endif
|
||||
@ -80,8 +80,8 @@ Here's what is happening:
|
||||
following elements:
|
||||
0: The job id
|
||||
1: The kind of activity: one of "stdout", "stderr" or "exit"
|
||||
2: When "activity" is "stdout" or "stderr", this will contain the data read
|
||||
from stdout or stderr
|
||||
2: When "activity" is "stdout" or "stderr", this will contain a list of
|
||||
lines read from stdout or stderr
|
||||
|
||||
To send data to the job's stdin, one can use the |jobsend()| function, like
|
||||
this:
|
||||
|
122
src/nvim/eval.c
122
src/nvim/eval.c
@ -453,6 +453,7 @@ static dictitem_T vimvars_var; /* variable used for v: */
|
||||
typedef struct {
|
||||
int id;
|
||||
char *name, *type, *received;
|
||||
size_t received_len;
|
||||
} JobEvent;
|
||||
#define JobEventFreer(x)
|
||||
KMEMPOOL_INIT(JobEventPool, JobEvent, JobEventFreer)
|
||||
@ -10592,9 +10593,9 @@ static void f_jobsend(typval_T *argvars, typval_T *rettv)
|
||||
return;
|
||||
}
|
||||
|
||||
if (argvars[0].v_type != VAR_NUMBER || argvars[1].v_type != VAR_STRING) {
|
||||
// First argument is the job id and second is the string to write to
|
||||
// the job's stdin
|
||||
if (argvars[0].v_type != VAR_NUMBER || argvars[1].v_type == VAR_UNKNOWN) {
|
||||
// First argument is the job id and second is the string or list to write
|
||||
// to the job's stdin
|
||||
EMSG(_(e_invarg));
|
||||
return;
|
||||
}
|
||||
@ -10607,10 +10608,15 @@ static void f_jobsend(typval_T *argvars, typval_T *rettv)
|
||||
return;
|
||||
}
|
||||
|
||||
WBuffer *buf = wstream_new_buffer(xstrdup((char *)argvars[1].vval.v_string),
|
||||
strlen((char *)argvars[1].vval.v_string),
|
||||
1,
|
||||
free);
|
||||
ssize_t input_len;
|
||||
char *input = (char *) save_tv_as_string(&argvars[1], &input_len, true);
|
||||
if (input_len < 0) {
|
||||
return; // Error handled by save_tv_as_string().
|
||||
} else if (input_len == 0) {
|
||||
return; // Not an error, but nothing to do.
|
||||
}
|
||||
|
||||
WBuffer *buf = wstream_new_buffer(input, input_len, 1, free);
|
||||
rettv->vval.v_number = job_write(job, buf);
|
||||
}
|
||||
|
||||
@ -14433,6 +14439,29 @@ static void f_synstack(typval_T *argvars, typval_T *rettv)
|
||||
}
|
||||
}
|
||||
|
||||
static list_T* string_to_list(char_u *str, size_t len)
|
||||
{
|
||||
list_T *list = list_alloc();
|
||||
|
||||
// Copy each line to a list element using NL as the delimiter.
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
char_u *start = str + i;
|
||||
size_t line_len = (char_u *) xmemscan(start, NL, len - i) - start;
|
||||
i += line_len;
|
||||
|
||||
// Don't use a str function to copy res as it may contains NULs.
|
||||
char_u *s = xmemdupz(start, line_len);
|
||||
memchrsub(s, NUL, NL, line_len); // Replace NUL with NL to avoid truncation
|
||||
|
||||
listitem_T *li = listitem_alloc();
|
||||
li->li_tv.v_type = VAR_STRING;
|
||||
li->li_tv.vval.v_string = s;
|
||||
list_append(list, li);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
static void get_system_output_as_rettv(typval_T *argvars, typval_T *rettv,
|
||||
bool retlist)
|
||||
{
|
||||
@ -14445,7 +14474,7 @@ static void get_system_output_as_rettv(typval_T *argvars, typval_T *rettv,
|
||||
|
||||
// get input to the shell command (if any), and its length
|
||||
ssize_t input_len;
|
||||
char *input = (char *) save_tv_as_string(&argvars[1], &input_len);
|
||||
char *input = (char *) save_tv_as_string(&argvars[1], &input_len, false);
|
||||
if (input_len == -1) {
|
||||
return;
|
||||
}
|
||||
@ -14467,23 +14496,9 @@ static void get_system_output_as_rettv(typval_T *argvars, typval_T *rettv,
|
||||
}
|
||||
|
||||
if (retlist) {
|
||||
list_T *list = rettv_list_alloc(rettv);
|
||||
|
||||
// Copy each line to a list element using NL as the delimiter.
|
||||
for (size_t i = 0; i < nread; i++) {
|
||||
char_u *start = (char_u *) res + i;
|
||||
size_t len = (char_u *) xmemscan(start, NL, nread - i) - start;
|
||||
i += len;
|
||||
|
||||
// Don't use a str function to copy res as it may contains NULs.
|
||||
char_u *s = xmemdupz(start, len);
|
||||
memchrsub(s, NUL, NL, len); // Replace NUL with NL to avoid truncation.
|
||||
|
||||
listitem_T *li = listitem_alloc();
|
||||
li->li_tv.v_type = VAR_STRING;
|
||||
li->li_tv.vval.v_string = s;
|
||||
list_append(list, li);
|
||||
}
|
||||
rettv->vval.v_list = string_to_list((char_u *) res, nread);
|
||||
rettv->vval.v_list->lv_refcount++;
|
||||
rettv->v_type = VAR_LIST;
|
||||
|
||||
free(res);
|
||||
} else {
|
||||
@ -15148,9 +15163,10 @@ static bool write_list(FILE *fd, list_T *list, bool binary)
|
||||
///
|
||||
/// @param[in] tv A value to store as a string.
|
||||
/// @param[out] len The length of the resulting string or -1 on error.
|
||||
/// @param[in] endnl If true, the output will end in a newline (if a list).
|
||||
/// @returns an allocated string if `tv` represents a VimL string, list, or
|
||||
/// number; NULL otherwise.
|
||||
static char_u *save_tv_as_string(typval_T *tv, ssize_t *len)
|
||||
static char_u *save_tv_as_string(typval_T *tv, ssize_t *len, bool endnl)
|
||||
FUNC_ATTR_MALLOC FUNC_ATTR_NONNULL_ALL
|
||||
{
|
||||
if (tv->v_type == VAR_UNKNOWN) {
|
||||
@ -15182,13 +15198,13 @@ static char_u *save_tv_as_string(typval_T *tv, ssize_t *len)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char_u *ret = xmalloc(*len);
|
||||
char_u *ret = xmalloc(*len + endnl);
|
||||
char_u *end = ret;
|
||||
for (listitem_T *li = list->lv_first; li != NULL; li = li->li_next) {
|
||||
for (char_u *s = get_tv_string(&li->li_tv); *s != NUL; s++) {
|
||||
*end++ = (*s == '\n') ? NUL : *s;
|
||||
}
|
||||
if (li->li_next != NULL) {
|
||||
if (endnl || li->li_next != NULL) {
|
||||
*end++ = '\n';
|
||||
}
|
||||
}
|
||||
@ -19528,15 +19544,33 @@ char_u *do_string_sub(char_u *str, char_u *pat, char_u *sub, char_u *flags)
|
||||
|
||||
// JobActivity autocommands will execute vimscript code, so it must be executed
|
||||
// on Nvim main loop
|
||||
#define push_job_event(j, r, t) \
|
||||
#define push_job_event(j, r, t, eof) \
|
||||
do { \
|
||||
JobEvent *event_data = kmp_alloc(JobEventPool, job_event_pool); \
|
||||
event_data->received = NULL; \
|
||||
size_t read_count = 0; \
|
||||
if (r) { \
|
||||
size_t read_count = rstream_pending(r); \
|
||||
event_data->received = xmalloc(read_count + 1); \
|
||||
if (eof) { \
|
||||
read_count = rstream_pending(r); \
|
||||
} else { \
|
||||
char *read = rstream_read_ptr(r); \
|
||||
char *lastnl = xmemrchr(read, NL, rstream_pending(r)); \
|
||||
if (lastnl) { \
|
||||
read_count = (size_t) (lastnl - read) + 1; \
|
||||
} else if (rstream_available(r) == 0) { \
|
||||
/* No newline or room to grow; flush everything. */ \
|
||||
read_count = rstream_pending(r); \
|
||||
} \
|
||||
} \
|
||||
if (read_count == 0) { \
|
||||
/* Either we're at EOF or we need to wait until next time */ \
|
||||
/* to receive a '\n. */ \
|
||||
kmp_free(JobEventPool, job_event_pool, event_data); \
|
||||
return; \
|
||||
} \
|
||||
event_data->received_len = read_count; \
|
||||
event_data->received = xmallocz(read_count); \
|
||||
rstream_read(r, event_data->received, read_count); \
|
||||
event_data->received[read_count] = NUL; \
|
||||
} \
|
||||
event_data->id = job_id(j); \
|
||||
event_data->name = job_data(j); \
|
||||
@ -19549,31 +19583,33 @@ char_u *do_string_sub(char_u *str, char_u *pat, char_u *sub, char_u *flags)
|
||||
|
||||
static void on_job_stdout(RStream *rstream, void *data, bool eof)
|
||||
{
|
||||
if (!eof) {
|
||||
push_job_event(data, rstream, "stdout");
|
||||
if (rstream_pending(rstream)) {
|
||||
push_job_event(data, rstream, "stdout", eof);
|
||||
}
|
||||
}
|
||||
|
||||
static void on_job_stderr(RStream *rstream, void *data, bool eof)
|
||||
{
|
||||
if (!eof) {
|
||||
push_job_event(data, rstream, "stderr");
|
||||
if (rstream_pending(rstream)) {
|
||||
push_job_event(data, rstream, "stderr", eof);
|
||||
}
|
||||
}
|
||||
|
||||
static void on_job_exit(Job *job, void *data)
|
||||
{
|
||||
push_job_event(job, NULL, "exit");
|
||||
push_job_event(job, NULL, "exit", true);
|
||||
}
|
||||
|
||||
static void on_job_event(Event event)
|
||||
{
|
||||
JobEvent *data = event.data;
|
||||
apply_job_autocmds(data->id, data->name, data->type, data->received);
|
||||
apply_job_autocmds(data->id, data->name, data->type,
|
||||
data->received, data->received_len);
|
||||
kmp_free(JobEventPool, job_event_pool, data);
|
||||
}
|
||||
|
||||
static void apply_job_autocmds(int id, char *name, char *type, char *received)
|
||||
static void apply_job_autocmds(int id, char *name, char *type,
|
||||
char *received, size_t received_len)
|
||||
{
|
||||
// Create the list which will be set to v:job_data
|
||||
list_T *list = list_alloc();
|
||||
@ -19582,10 +19618,14 @@ static void apply_job_autocmds(int id, char *name, char *type, char *received)
|
||||
|
||||
if (received) {
|
||||
listitem_T *str_slot = listitem_alloc();
|
||||
str_slot->li_tv.v_type = VAR_STRING;
|
||||
str_slot->li_tv.v_type = VAR_LIST;
|
||||
str_slot->li_tv.v_lock = 0;
|
||||
str_slot->li_tv.vval.v_string = (uint8_t *)received;
|
||||
str_slot->li_tv.vval.v_list =
|
||||
string_to_list((char_u *) received, received_len);
|
||||
str_slot->li_tv.vval.v_list->lv_refcount++;
|
||||
list_append(list, str_slot);
|
||||
|
||||
free(received);
|
||||
}
|
||||
|
||||
// Update v:job_data for the autocommands
|
||||
|
@ -389,6 +389,25 @@ char *xstrdup(const char *str)
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// A version of memchr that starts the search at `src + len`.
|
||||
///
|
||||
/// Based on glibc's memrchr.
|
||||
///
|
||||
/// @param src The source memory object.
|
||||
/// @param c The byte to search for.
|
||||
/// @param len The length of the memory object.
|
||||
/// @returns a pointer to the found byte in src[len], or NULL.
|
||||
void *xmemrchr(void *src, uint8_t c, size_t len)
|
||||
FUNC_ATTR_NONNULL_ALL FUNC_ATTR_PURE
|
||||
{
|
||||
while (len--) {
|
||||
if (((uint8_t *)src)[len] == c) {
|
||||
return (uint8_t *) src + len;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/// strndup() wrapper
|
||||
///
|
||||
/// @see {xmalloc}
|
||||
|
@ -190,6 +190,18 @@ RStream * rstream_new(rstream_cb cb, RBuffer *buffer, void *data)
|
||||
return rv;
|
||||
}
|
||||
|
||||
/// Returns the read pointer used by the rstream.
|
||||
char *rstream_read_ptr(RStream *rstream)
|
||||
{
|
||||
return rbuffer_read_ptr(rstream->buffer);
|
||||
}
|
||||
|
||||
/// Returns the number of bytes before the rstream is full.
|
||||
size_t rstream_available(RStream *rstream)
|
||||
{
|
||||
return rbuffer_available(rstream->buffer);
|
||||
}
|
||||
|
||||
/// Frees all memory allocated for a RStream instance
|
||||
///
|
||||
/// @param rstream The `RStream` instance
|
||||
|
@ -11,8 +11,12 @@ describe('jobs', function()
|
||||
before_each(clear)
|
||||
|
||||
-- Creates the string to make an autocmd to notify us.
|
||||
local notify_str = function(expr)
|
||||
return "au! JobActivity xxx call rpcnotify("..channel..", "..expr..")"
|
||||
local notify_str = function(expr1, expr2)
|
||||
local str = "au! JobActivity xxx call rpcnotify("..channel..", "..expr1
|
||||
if expr2 ~= nil then
|
||||
str = str..", "..expr2
|
||||
end
|
||||
return str..")"
|
||||
end
|
||||
|
||||
local notify_job = function()
|
||||
@ -33,21 +37,52 @@ describe('jobs', function()
|
||||
end)
|
||||
|
||||
it('allows interactive commands', function()
|
||||
nvim('command', notify_str('v:job_data[2]'))
|
||||
nvim('command', notify_str('v:job_data[1]', 'v:job_data[2]'))
|
||||
nvim('command', "let j = jobstart('xxx', 'cat', ['-'])")
|
||||
neq(0, eval('j'))
|
||||
nvim('command', "call jobsend(j, 'abc')")
|
||||
eq({'notification', 'abc', {}}, next_message())
|
||||
nvim('command', "call jobsend(j, '123')")
|
||||
eq({'notification', '123', {}}, next_message())
|
||||
nvim('command', 'call jobsend(j, "abc\\n")')
|
||||
eq({'notification', 'stdout', {{'abc'}}}, next_message())
|
||||
nvim('command', 'call jobsend(j, "123\\nxyz\\n")')
|
||||
eq({'notification', 'stdout', {{'123', 'xyz'}}}, next_message())
|
||||
nvim('command', 'call jobsend(j, [123, "xyz"])')
|
||||
eq({'notification', 'stdout', {{'123', 'xyz'}}}, next_message())
|
||||
nvim('command', notify_str('v:job_data[1])'))
|
||||
nvim('command', "call jobstop(j)")
|
||||
eq({'notification', 'exit', {}}, next_message())
|
||||
end)
|
||||
|
||||
it('preserves NULs', function()
|
||||
-- Make a file with NULs in it.
|
||||
local filename = os.tmpname()
|
||||
local file = io.open(filename, "w")
|
||||
file:write("abc\0def\n")
|
||||
file:close()
|
||||
|
||||
-- v:job_data preserves NULs.
|
||||
nvim('command', notify_str('v:job_data[1]', 'v:job_data[2]'))
|
||||
nvim('command', "let j = jobstart('xxx', 'cat', ['"..filename.."'])")
|
||||
eq({'notification', 'stdout', {{'abc\ndef'}}}, next_message())
|
||||
os.remove(filename)
|
||||
|
||||
-- jobsend() preserves NULs.
|
||||
nvim('command', "let j = jobstart('xxx', 'cat', ['-'])")
|
||||
nvim('command', [[call jobsend(j, ["123\n456"])]])
|
||||
eq({'notification', 'stdout', {{'123\n456'}}}, next_message())
|
||||
nvim('command', "call jobstop(j)")
|
||||
end)
|
||||
|
||||
it('will hold data if it does not end in a newline', function()
|
||||
nvim('command', notify_str('v:job_data[1]', 'v:job_data[2]'))
|
||||
nvim('command', "let j = jobstart('xxx', 'cat', ['-'])")
|
||||
nvim('command', 'call jobsend(j, "abc\\nxyz")')
|
||||
eq({'notification', 'stdout', {{'abc'}}}, next_message())
|
||||
nvim('command', "call jobstop(j)")
|
||||
eq({'notification', 'stdout', {{'xyz'}}}, next_message())
|
||||
end)
|
||||
|
||||
it('will not allow jobsend/stop on a non-existent job', function()
|
||||
eq(false, pcall(eval, "jobsend(-1, 'lol')"))
|
||||
eq(false, pcall(eval, "jobstop(-1, 'lol')"))
|
||||
eq(false, pcall(eval, "jobstop(-1)"))
|
||||
end)
|
||||
|
||||
it('will not allow jobstop twice on the same job', function()
|
||||
@ -65,9 +100,9 @@ describe('jobs', function()
|
||||
nvim('command', notify_job())
|
||||
nvim('command', "let j = jobstart('xxx', 'cat', ['-'])")
|
||||
local jobid = nvim('eval', 'j')
|
||||
nvim('eval', 'jobsend(j, "abc\ndef")')
|
||||
nvim('eval', 'jobsend(j, "abcdef")')
|
||||
nvim('eval', 'jobstop(j)')
|
||||
eq({'notification', 'j', {{jobid, 'stdout', 'abc\ndef'}}}, next_message())
|
||||
eq({'notification', 'j', {{jobid, 'stdout', {'abcdef'}}}}, next_message())
|
||||
eq({'notification', 'j', {{jobid, 'exit'}}}, next_message())
|
||||
end)
|
||||
end)
|
||||
|
Loading…
Reference in New Issue
Block a user