aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--runtime/doc/eval.txt7
-rw-r--r--runtime/doc/job_control.txt8
-rw-r--r--src/nvim/eval.c122
-rw-r--r--src/nvim/memory.c19
-rw-r--r--src/nvim/os/rstream.c12
-rw-r--r--test/functional/job/job_spec.lua55
6 files changed, 168 insertions, 55 deletions
diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt
index 869c416b88..fca8ac1291 100644
--- a/runtime/doc/eval.txt
+++ b/runtime/doc/eval.txt
@@ -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
diff --git a/runtime/doc/job_control.txt b/runtime/doc/job_control.txt
index 49ee3889bc..226244875d 100644
--- a/runtime/doc/job_control.txt
+++ b/runtime/doc/job_control.txt
@@ -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:
diff --git a/src/nvim/eval.c b/src/nvim/eval.c
index d75807800c..59363a3608 100644
--- a/src/nvim/eval.c
+++ b/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
diff --git a/src/nvim/memory.c b/src/nvim/memory.c
index 59edefec4a..4b24213ecd 100644
--- a/src/nvim/memory.c
+++ b/src/nvim/memory.c
@@ -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}
diff --git a/src/nvim/os/rstream.c b/src/nvim/os/rstream.c
index f16226cdd1..e36a0213c8 100644
--- a/src/nvim/os/rstream.c
+++ b/src/nvim/os/rstream.c
@@ -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
diff --git a/test/functional/job/job_spec.lua b/test/functional/job/job_spec.lua
index 9046d85f10..85a1e92e38 100644
--- a/test/functional/job/job_spec.lua
+++ b/test/functional/job/job_spec.lua
@@ -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)