eval: Refactor vimscript job control API

- Remove JobActivity autocmd and v:job_data variable
- Simplify `jobstart` to receive:
  - An argument vector
  - An optional dictionary which may contain any of the current `jobstart`
    options plus `on_stdout`, `on_stderr` and `on_exit` callbacks.
- Refactor and add more job tests
- Update documentation
This commit is contained in:
Thiago de Arruda
2015-03-25 23:11:05 -03:00
parent 4b98ea1e80
commit 6e7757ad51
12 changed files with 398 additions and 210 deletions

View File

@@ -307,7 +307,6 @@ Name triggered by ~
|InsertLeave| when leaving Insert mode |InsertLeave| when leaving Insert mode
|InsertCharPre| when a character was typed in Insert mode, before |InsertCharPre| when a character was typed in Insert mode, before
inserting it inserting it
|JobActivity| when something interesting happens with a job
|TextChanged| after a change was made to the text in Normal mode |TextChanged| after a change was made to the text in Normal mode
|TextChangedI| after a change was made to the text in Insert mode |TextChangedI| after a change was made to the text in Insert mode
@@ -733,10 +732,6 @@ InsertEnter Just before starting Insert mode. Also for
*InsertLeave* *InsertLeave*
InsertLeave When leaving Insert mode. Also when using InsertLeave When leaving Insert mode. Also when using
CTRL-O |i_CTRL-O|. But not for |i_CTRL-C|. CTRL-O |i_CTRL-O|. But not for |i_CTRL-C|.
{Nvim} *JobActivity*
JobActivity When something interesting happens with a job
spawned by |jobstart()|. See |job-control| for
details.
*MenuPopup* *MenuPopup*
MenuPopup Just before showing the popup menu (under the MenuPopup Just before showing the popup menu (under the
right mouse button). Useful for adjusting the right mouse button). Useful for adjusting the

View File

@@ -4012,6 +4012,11 @@ items({dict}) *items()*
entry and the value of this entry. The |List| is in arbitrary entry and the value of this entry. The |List| is in arbitrary
order. order.
jobresize({job}, {width}, {height}) {Nvim} *jobresize()*
Resize {job}'s pseudo terminal window to {width} and {height}.
This function will fail if used on jobs started without the
"pty" option.
jobsend({job}, {data}) {Nvim} *jobsend()* jobsend({job}, {data}) {Nvim} *jobsend()*
Send data to {job} by writing it to the stdin of the process. Send data to {job} by writing it to the stdin of the process.
Returns 1 if the write succeeded, 0 otherwise. Returns 1 if the write succeeded, 0 otherwise.
@@ -4024,14 +4029,28 @@ jobsend({job}, {data}) {Nvim} *jobsend()*
:call jobsend(j, ["abc", "123\n456", ""]) :call jobsend(j, ["abc", "123\n456", ""])
< will send "abc<NL>123<NUL>456<NL>". < will send "abc<NL>123<NUL>456<NL>".
jobstart({name}, {prog}[, {argv}]) {Nvim} *jobstart()* jobstart({argv}[, {opts}]) {Nvim} *jobstart()*
Spawns {prog} as a job and associate it with the {name} string, Spawns {argv}(list) as a job. If passed, {opts} must be a
which will be used to match the "filename pattern" in dictionary with any of the following keys:
|JobActivity| events. It returns: - on_stdout: stdout event handler
- The job id on success, which is used by |jobsend()| and - on_stderr: stderr event handler
- on_exit: exit event handler
- pty: If set, the job will be connected to a new pseudo
terminal, and the job streams are connected to the master
file descriptor.
- width: Width of the terminal screen(only if pty is set)
- height: Height of the terminal screen(only if pty is set)
- TERM: $TERM environment variable(only if pty is set)
Either funcrefs or function names can be passed as event
handlers. The {opts} object is also used as the "self"
argument for the callback, so the caller may pass arbitrary
data by setting other key.(see |Dictionary-function| for more
information).
Returns:
- The job ID on success, which is used by |jobsend()| and
|jobstop()| |jobstop()|
- 0 when the job table is full or on invalid arguments - 0 when the job table is full or on invalid arguments
- -1 when {prog} is not executable - -1 when {argv}[0] is not executable
See |job-control| for more information. See |job-control| for more information.
jobstop({job}) {Nvim} *jobstop()* jobstop({job}) {Nvim} *jobstop()*

View File

@@ -37,37 +37,28 @@ for details
============================================================================== ==============================================================================
2. Usage *job-control-usage* 2. Usage *job-control-usage*
Here's a quick one-liner that creates a job which invokes the "ls" shell
command and prints the result:
>
call jobstart('', 'ls', ['-a'])|au JobActivity * echo v:job_data|au!
JobActivity
In the one-liner above, creating the JobActivity event handler immediately
after the call to jobstart() is not a race because the Nvim job system will
not publish the job result (even though it may receive it) until evaluation of
the chained user commands (`expr1|expr2|...|exprN`) has completed.
Job control is achieved by calling a combination of the |jobstart()|, Job control is achieved by calling a combination of the |jobstart()|,
|jobsend()| and |jobstop()| functions, and by listening to the |JobActivity| |jobsend()| and |jobstop()| functions. Here's an example:
event. The best way to understand is with a complete example:
> >
let job1 = jobstart('shell1', 'bash') function s:JobHandler(job_id, data, event)
let job2 = jobstart('shell2', 'bash', ['-c', 'for ((i = 0; i < 10; i++)); do echo hello $i!; sleep 1; done']) if a:event == 'stdout'
let str = self.shell.' stdout: '.join(a:data)
function JobHandler() elseif a:event == 'stderr'
if v:job_data[1] == 'stdout' let str = self.shell.' stderr: '.join(a:data)
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: '.join(v:job_data[2])
else else
let str = 'shell '.v:job_data[0].' exited' let str = self.shell.' exited'
endif endif
call append(line('$'), str) call append(line('$'), str)
endfunction endfunction
let s:callbacks = {
\ 'on_stdout': function('s:JobHandler'),
\ 'on_stderr': function('s:JobHandler'),
\ 'on_exit': function('s:JobHandler')
\ }
let job1 = jobstart(['bash'], extend({'shell': 'shell 1'}, s:callbacks))
let job2 = jobstart(['bash', '-c', 'for i in {1..10}; do echo hello $i!; sleep 1; done'], extend({'shell': 'shell 2'}, s:callbacks))
au JobActivity shell* call JobHandler()
< <
To test the above, copy it to the file ~/jobcontrol.vim and start with a clean To test the above, copy it to the file ~/jobcontrol.vim and start with a clean
nvim instance: nvim instance:
@@ -82,16 +73,51 @@ Here's what is happening:
- The second shell is started with the -c argument, causing it to execute a - The second shell is started with the -c argument, causing it to execute a
command then exit. In this case, the command is a for loop that will print 0 command then exit. In this case, the command is a for loop that will print 0
through 9 then exit. through 9 then exit.
- The `JobHandler()` function is called by the `JobActivity` autocommand (notice - The `JobHandler()` function is a callback passed to |jobstart()| to handle
how the shell* pattern matches the names `shell1` and `shell2` passed to various job events. It takes care of displaying stdout/stderr received from
|jobstart()|), and it takes care of displaying stdout/stderr received from
the shells. the shells.
- The v:job_data is an array set by the JobActivity event. It has the - The arguments passed to `JobHandler()` are:
following elements:
0: The job id 0: The job id
1: The kind of activity: one of "stdout", "stderr" or "exit" 1: If the event is "stdout" or "stderr", a list with lines read from the
2: When "activity" is "stdout" or "stderr", this will contain a list of corresponding stream. For "exit", it is the status returned by the
lines read from stdout or stderr program.
2: The event type, which is "stdout", "stderr" or "exit".
The options dictionary is passed as the "self" variable to the callback
function. Here's a more object-oriented version of the above:
>
let Shell = {}
function Shell.on_stdout(job_id, data)
call append(line('$'), self.get_name().' stdout: '.join(a:data))
endfunction
function Shell.on_stderr(job_id, data)
call append(line('$'), self.get_name().' stderr: '.join(a:data))
endfunction
function Shell.on_exit(job_id, data)
call append(line('$'), self.get_name().' exited')
endfunction
function Shell.get_name()
return 'shell '.self.name
endfunction
function Shell.new(name, ...)
let instance = extend(copy(g:Shell), {'name': a:name})
let argv = ['bash']
if a:0 > 0
let argv += ['-c', a:1]
endif
let instance.id = jobstart(argv, instance)
return instance
endfunction
let s1 = Shell.new('1')
let s2 = Shell.new('2', 'for i in {1..10}; do echo hello $i!; sleep 1; done')
To send data to the job's stdin, one can use the |jobsend()| function, like To send data to the job's stdin, one can use the |jobsend()| function, like
this: this:

View File

@@ -427,7 +427,6 @@ static struct vimvar {
{VV_NAME("oldfiles", VAR_LIST), 0}, {VV_NAME("oldfiles", VAR_LIST), 0},
{VV_NAME("windowid", VAR_NUMBER), VV_RO}, {VV_NAME("windowid", VAR_NUMBER), VV_RO},
{VV_NAME("progpath", VAR_STRING), VV_RO}, {VV_NAME("progpath", VAR_STRING), VV_RO},
{VV_NAME("job_data", VAR_LIST), 0},
{VV_NAME("command_output", VAR_STRING), 0} {VV_NAME("command_output", VAR_STRING), 0}
}; };
@@ -447,7 +446,8 @@ typedef struct {
Terminal *term; Terminal *term;
bool exited; bool exited;
int refcount; int refcount;
char *autocmd_file; ufunc_T *on_stdout, *on_stderr, *on_exit;
dict_T *self;
} TerminalJobData; } TerminalJobData;
@@ -460,9 +460,12 @@ typedef struct {
valid character */ valid character */
// Memory pool for reusing JobEvent structures // Memory pool for reusing JobEvent structures
typedef struct { typedef struct {
int id; int job_id;
char *name, *type; TerminalJobData *data;
ufunc_T *callback;
const char *type;
list_T *received; list_T *received;
int status;
} JobEvent; } JobEvent;
#define JobEventFreer(x) #define JobEventFreer(x)
KMEMPOOL_INIT(JobEventPool, JobEvent, JobEventFreer) KMEMPOOL_INIT(JobEventPool, JobEvent, JobEventFreer)
@@ -5933,6 +5936,41 @@ dictitem_T *dict_find(dict_T *d, char_u *key, int len)
return HI2DI(hi); return HI2DI(hi);
} }
// Get a function from a dictionary
static ufunc_T *get_dict_callback(dict_T *d, char *key)
{
dictitem_T *di = dict_find(d, (uint8_t *)key, -1);
if (di == NULL) {
return NULL;
}
if (di->di_tv.v_type != VAR_FUNC && di->di_tv.v_type != VAR_STRING) {
EMSG(_("Argument is not a function or function name"));
return NULL;
}
uint8_t *name = di->di_tv.vval.v_string;
uint8_t *n = name;
ufunc_T *rv;
if (*n > '9' || *n < '0') {
n = trans_function_name(&n, false, TFN_INT|TFN_QUIET, NULL);
rv = find_func(n);
free(n);
} else {
// dict function, name is already translated
rv = find_func(n);
}
if (!rv) {
EMSG2(_("Function %s doesn't exist"), name);
return NULL;
}
rv->uf_refcount++;
return rv;
}
/* /*
* Get a string item from a dictionary. * Get a string item from a dictionary.
* When "save" is TRUE allocate memory for it. * When "save" is TRUE allocate memory for it.
@@ -6497,7 +6535,7 @@ static struct fst {
{"items", 1, 1, f_items}, {"items", 1, 1, f_items},
{"jobresize", 3, 3, f_jobresize}, {"jobresize", 3, 3, f_jobresize},
{"jobsend", 2, 2, f_jobsend}, {"jobsend", 2, 2, f_jobsend},
{"jobstart", 2, 4, f_jobstart}, {"jobstart", 1, 2, f_jobstart},
{"jobstop", 1, 1, f_jobstop}, {"jobstop", 1, 1, f_jobstop},
{"join", 1, 2, f_join}, {"join", 1, 2, f_join},
{"keys", 1, 1, f_keys}, {"keys", 1, 1, f_keys},
@@ -10654,7 +10692,7 @@ static void f_jobsend(typval_T *argvars, typval_T *rettv)
rettv->vval.v_number = job_write(job, buf); rettv->vval.v_number = job_write(job, buf);
} }
// "jobresize()" function // "jobresize(job, width, height)" function
static void f_jobresize(typval_T *argvars, typval_T *rettv) static void f_jobresize(typval_T *argvars, typval_T *rettv)
{ {
rettv->v_type = VAR_NUMBER; rettv->v_type = VAR_NUMBER;
@@ -10690,11 +10728,6 @@ static void f_jobresize(typval_T *argvars, typval_T *rettv)
// "jobstart()" function // "jobstart()" function
static void f_jobstart(typval_T *argvars, typval_T *rettv) static void f_jobstart(typval_T *argvars, typval_T *rettv)
{ {
list_T *args = NULL;
listitem_T *arg;
int i, argvl, argsl;
char **argv = NULL;
rettv->v_type = VAR_NUMBER; rettv->v_type = VAR_NUMBER;
rettv->vval.v_number = 0; rettv->vval.v_number = 0;
@@ -10702,55 +10735,60 @@ static void f_jobstart(typval_T *argvars, typval_T *rettv)
return; return;
} }
if (argvars[0].v_type != VAR_STRING if (argvars[0].v_type != VAR_LIST
|| argvars[1].v_type != VAR_STRING || (argvars[1].v_type != VAR_DICT && argvars[1].v_type != VAR_UNKNOWN)) {
|| (argvars[2].v_type != VAR_LIST && argvars[2].v_type != VAR_UNKNOWN)) {
// Wrong argument types // Wrong argument types
EMSG(_(e_invarg)); EMSG(_(e_invarg));
return; return;
} }
argsl = 0; list_T *args = argvars[0].vval.v_list;
if (argvars[2].v_type == VAR_LIST) { // Assert that all list items are strings
args = argvars[2].vval.v_list; for (listitem_T *arg = args->lv_first; arg != NULL; arg = arg->li_next) {
argsl = args->lv_len; if (arg->li_tv.v_type != VAR_STRING) {
// Assert that all list items are strings EMSG(_(e_invarg));
for (arg = args->lv_first; arg != NULL; arg = arg->li_next) { return;
if (arg->li_tv.v_type != VAR_STRING) {
EMSG(_(e_invarg));
return;
}
} }
} }
if (!os_can_exe(get_tv_string(&argvars[1]), NULL)) { int argc = args->lv_len;
// String is not executable if (!argc) {
EMSG2(e_jobexe, get_tv_string(&argvars[1])); EMSG(_("Argument vector must have at least one item"));
return; return;
} }
// Allocate extra memory for the argument vector and the NULL pointer if (!os_can_exe(args->lv_first->li_tv.vval.v_string, NULL)) {
argvl = argsl + 2; // String is not executable
argv = xmalloc(sizeof(char_u *) * argvl); EMSG2(e_jobexe, args->lv_first->li_tv.vval.v_string);
return;
}
// Copy program name dict_T *job_opts = NULL;
argv[0] = xstrdup((char *)get_tv_string(&argvars[1])); ufunc_T *on_stdout = NULL, *on_stderr = NULL, *on_exit = NULL;
if (argvars[1].v_type == VAR_DICT) {
i = 1; job_opts = argvars[1].vval.v_dict;
// Copy arguments to the vector common_job_callbacks(job_opts, &on_stdout, &on_stderr, &on_exit);
if (argsl > 0) { if (did_emsg) {
for (arg = args->lv_first; arg != NULL; arg = arg->li_next) { return;
argv[i++] = xstrdup((char *)get_tv_string(&arg->li_tv));
} }
} }
// The last item of argv must be NULL // Build the argument vector
argv[i] = NULL; int i = 0;
JobOptions opts = common_job_options(argv, (char *)argvars[0].vval.v_string); char **argv = xcalloc(argc + 1, sizeof(char *));
for (listitem_T *arg = args->lv_first; arg != NULL; arg = arg->li_next) {
argv[i++] = xstrdup((char *)arg->li_tv.vval.v_string);
}
if (args && argvars[3].v_type == VAR_DICT) { JobOptions opts = common_job_options(argv, on_stdout, on_stderr, on_exit,
dict_T *job_opts = argvars[3].vval.v_dict; job_opts);
opts.pty = true;
if (!job_opts) {
goto start;
}
opts.pty = get_dict_number(job_opts, (uint8_t *)"pty");
if (opts.pty) {
uint16_t width = get_dict_number(job_opts, (uint8_t *)"width"); uint16_t width = get_dict_number(job_opts, (uint8_t *)"width");
if (width > 0) { if (width > 0) {
opts.width = width; opts.width = width;
@@ -10765,6 +10803,13 @@ static void f_jobstart(typval_T *argvars, typval_T *rettv)
} }
} }
start:
if (!on_stdout) {
opts.stdout_cb = NULL;
}
if (!on_stderr) {
opts.stderr_cb = NULL;
}
common_job_start(opts, rettv); common_job_start(opts, rettv);
} }
@@ -14866,15 +14911,25 @@ static void f_termopen(typval_T *argvars, typval_T *rettv)
} }
if (argvars[0].v_type != VAR_STRING if (argvars[0].v_type != VAR_STRING
|| (argvars[1].v_type != VAR_STRING || (argvars[1].v_type != VAR_DICT && argvars[1].v_type != VAR_UNKNOWN)) {
&& argvars[1].v_type != VAR_UNKNOWN)) {
// Wrong argument types // Wrong argument types
EMSG(_(e_invarg)); EMSG(_(e_invarg));
return; return;
} }
ufunc_T *on_stdout = NULL, *on_stderr = NULL, *on_exit = NULL;
dict_T *job_opts = NULL;
if (argvars[1].v_type == VAR_DICT) {
job_opts = argvars[1].vval.v_dict;
common_job_callbacks(job_opts, &on_stdout, &on_stderr, &on_exit);
if (did_emsg) {
return;
}
}
char **argv = shell_build_argv((char *)argvars[0].vval.v_string, NULL); char **argv = shell_build_argv((char *)argvars[0].vval.v_string, NULL);
JobOptions opts = common_job_options(argv, NULL); JobOptions opts = common_job_options(argv, on_stdout, on_stderr, on_exit,
job_opts);
opts.pty = true; opts.pty = true;
opts.width = curwin->w_width; opts.width = curwin->w_width;
opts.height = curwin->w_height; opts.height = curwin->w_height;
@@ -14906,7 +14961,6 @@ static void f_termopen(typval_T *argvars, typval_T *rettv)
// the 'swapfile' option to ensure no swap file will be created // the 'swapfile' option to ensure no swap file will be created
curbuf->b_p_swf = false; curbuf->b_p_swf = false;
(void)setfname(curbuf, (uint8_t *)buf, NULL, true); (void)setfname(curbuf, (uint8_t *)buf, NULL, true);
data->autocmd_file = xstrdup(buf);
// Save the job id and pid in b:terminal_job_{id,pid} // Save the job id and pid in b:terminal_job_{id,pid}
Error err; Error err;
dict_set_value(curbuf->b_vars, cstr_as_string("terminal_job_id"), dict_set_value(curbuf->b_vars, cstr_as_string("terminal_job_id"),
@@ -19824,12 +19878,14 @@ char_u *do_string_sub(char_u *str, char_u *pat, char_u *sub, char_u *flags)
return ret; return ret;
} }
static inline JobOptions common_job_options(char **argv, char *autocmd_file) static inline JobOptions common_job_options(char **argv, ufunc_T *on_stdout,
ufunc_T *on_stderr, ufunc_T *on_exit, dict_T *self)
{ {
TerminalJobData *data = xcalloc(1, sizeof(TerminalJobData)); TerminalJobData *data = xcalloc(1, sizeof(TerminalJobData));
if (autocmd_file) { data->on_stdout = on_stdout;
data->autocmd_file = xstrdup(autocmd_file); data->on_stderr = on_stderr;
} data->on_exit = on_exit;
data->self = self;
JobOptions opts = JOB_OPTIONS_INIT; JobOptions opts = JOB_OPTIONS_INIT;
opts.argv = argv; opts.argv = argv;
opts.data = data; opts.data = data;
@@ -19839,6 +19895,28 @@ static inline JobOptions common_job_options(char **argv, char *autocmd_file)
return opts; return opts;
} }
static inline void common_job_callbacks(dict_T *vopts, ufunc_T **on_stdout,
ufunc_T **on_stderr, ufunc_T **on_exit)
{
*on_stdout = get_dict_callback(vopts, "on_stdout");
*on_stderr = get_dict_callback(vopts, "on_stderr");
*on_exit = get_dict_callback(vopts, "on_exit");
if (did_emsg) {
if (*on_stdout) {
user_func_unref(*on_stdout);
}
if (*on_stderr) {
user_func_unref(*on_stderr);
}
if (*on_exit) {
user_func_unref(*on_exit);
}
return;
}
vopts->dv_refcount++;
}
static inline Job *common_job_start(JobOptions opts, typval_T *rettv) static inline Job *common_job_start(JobOptions opts, typval_T *rettv)
{ {
TerminalJobData *data = opts.data; TerminalJobData *data = opts.data;
@@ -19849,8 +19927,7 @@ static inline Job *common_job_start(JobOptions opts, typval_T *rettv)
if (rettv->vval.v_number == 0) { if (rettv->vval.v_number == 0) {
EMSG(_(e_jobtblfull)); EMSG(_(e_jobtblfull));
free(opts.term_name); free(opts.term_name);
free(data->autocmd_file); free_term_job_data(data);
free(data);
} else { } else {
EMSG(_(e_jobexe)); EMSG(_(e_jobexe));
} }
@@ -19860,10 +19937,23 @@ static inline Job *common_job_start(JobOptions opts, typval_T *rettv)
return job; return job;
} }
// JobActivity autocommands will execute vimscript code, so it must be executed static inline void free_term_job_data(TerminalJobData *data) {
// on Nvim main loop if (data->on_stdout) {
static inline void push_job_event(Job *job, char *type, char *data, user_func_unref(data->on_stdout);
size_t count) }
if (data->on_stderr) {
user_func_unref(data->on_stderr);
}
if (data->on_exit) {
user_func_unref(data->on_exit);
}
dict_unref(data->self);
free(data);
}
// vimscript job callbacks must be executed on Nvim main loop
static inline void push_job_event(Job *job, ufunc_T *callback,
const char *type, char *data, size_t count, int status)
{ {
JobEvent *event_data = kmp_alloc(JobEventPool, job_event_pool); JobEvent *event_data = kmp_alloc(JobEventPool, job_event_pool);
event_data->received = NULL; event_data->received = NULL;
@@ -19890,10 +19980,12 @@ static inline void push_job_event(Job *job, char *type, char *data,
off++; off++;
} }
list_append_string(event_data->received, (uint8_t *)ptr, off); list_append_string(event_data->received, (uint8_t *)ptr, off);
} else {
event_data->status = status;
} }
TerminalJobData *d = job_data(job); event_data->job_id = job_id(job);
event_data->id = job_id(job); event_data->data = job_data(job);
event_data->name = d->autocmd_file; event_data->callback = callback;
event_data->type = type; event_data->type = type;
event_push((Event) { event_push((Event) {
.handler = on_job_event, .handler = on_job_event,
@@ -19901,17 +19993,20 @@ static inline void push_job_event(Job *job, char *type, char *data,
}, true); }, true);
} }
static void on_job_stdout(RStream *rstream, void *data, bool eof) static void on_job_stdout(RStream *rstream, void *job, bool eof)
{ {
on_job_output(rstream, data, eof, "stdout"); TerminalJobData *data = job_data(job);
on_job_output(rstream, job, eof, data->on_stdout, "stdout");
} }
static void on_job_stderr(RStream *rstream, void *data, bool eof) static void on_job_stderr(RStream *rstream, void *job, bool eof)
{ {
on_job_output(rstream, data, eof, "stderr"); TerminalJobData *data = job_data(job);
on_job_output(rstream, job, eof, data->on_stderr, "stderr");
} }
static void on_job_output(RStream *rstream, Job *job, bool eof, char *type) static void on_job_output(RStream *rstream, Job *job, bool eof,
ufunc_T *callback, const char *type)
{ {
if (eof) { if (eof) {
return; return;
@@ -19927,21 +20022,24 @@ static void on_job_output(RStream *rstream, Job *job, bool eof, char *type)
terminal_receive(data->term, ptr, len); terminal_receive(data->term, ptr, len);
} }
push_job_event(job, type, ptr, len); if (callback) {
push_job_event(job, callback, type, ptr, len, 0);
}
rbuffer_consumed(rstream_buffer(rstream), len); rbuffer_consumed(rstream_buffer(rstream), len);
} }
static void on_job_exit(Job *job, void *d) static void on_job_exit(Job *job, int status, void *d)
{ {
TerminalJobData *data = d; TerminalJobData *data = d;
push_job_event(job, "exit", NULL, 0);
if (data->term && !data->exited) { if (data->term && !data->exited) {
data->exited = true; data->exited = true;
terminal_close(data->term, terminal_close(data->term,
_("\r\n[Program exited, press any key to close]")); _("\r\n[Program exited, press any key to close]"));
} }
term_job_data_decref(data);
push_job_event(job, data->on_exit, "exit", NULL, 0, status);
} }
static void term_write(char *buf, size_t size, void *data) static void term_write(char *buf, size_t size, void *data)
@@ -19970,42 +20068,57 @@ static void term_close(void *d)
static void term_job_data_decref(TerminalJobData *data) static void term_job_data_decref(TerminalJobData *data)
{ {
if (!(--data->refcount)) { if (!(--data->refcount)) {
free(data); free_term_job_data(data);
} }
} }
static void on_job_event(Event event) static void on_job_event(Event event)
{ {
JobEvent *data = event.data; JobEvent *ev = event.data;
apply_job_autocmds(data->id, data->name, data->type, data->received);
kmp_free(JobEventPool, job_event_pool, data);
}
static void apply_job_autocmds(int id, char *name, char *type, if (!ev->callback) {
list_T *received) goto end;
{
// Create the list which will be set to v:job_data
list_T *list = list_alloc();
list_append_number(list, id);
list_append_string(list, (uint8_t *)type, -1);
if (received) {
listitem_T *str_slot = listitem_alloc();
str_slot->li_tv.v_type = VAR_LIST;
str_slot->li_tv.v_lock = 0;
str_slot->li_tv.vval.v_list = received;
str_slot->li_tv.vval.v_list->lv_refcount++;
list_append(list, str_slot);
} }
// Update v:job_data for the autocommands typval_T argv[3];
set_vim_var_list(VV_JOB_DATA, list); int argc = ev->callback->uf_args.ga_len;
// Call JobActivity autocommands
apply_autocmds(EVENT_JOBACTIVITY, (uint8_t *)name, NULL, TRUE, NULL);
if (!received) { if (argc > 0) {
// This must be the exit event. Free the name. argv[0].v_type = VAR_NUMBER;
free(name); argv[0].v_lock = 0;
argv[0].vval.v_number = ev->job_id;
}
if (argc > 1) {
if (ev->received) {
argv[1].v_type = VAR_LIST;
argv[1].v_lock = 0;
argv[1].vval.v_list = ev->received;
argv[1].vval.v_list->lv_refcount++;
} else {
argv[1].v_type = VAR_NUMBER;
argv[1].v_lock = 0;
argv[1].vval.v_number = ev->status;
}
}
if (argc > 2) {
argv[2].v_type = VAR_STRING;
argv[2].v_lock = 0;
argv[2].vval.v_string = (uint8_t *)ev->type;
}
typval_T rettv;
init_tv(&rettv);
call_user_func(ev->callback, argc, argv, &rettv, curwin->w_cursor.lnum,
curwin->w_cursor.lnum, ev->data->self);
clear_tv(&rettv);
end:
kmp_free(JobEventPool, job_event_pool, ev);
if (!ev->received) {
// exit event, safe to free job data now
term_job_data_decref(ev->data);
} }
} }

View File

@@ -63,7 +63,6 @@ enum {
VV_OLDFILES, VV_OLDFILES,
VV_WINDOWID, VV_WINDOWID,
VV_PROGPATH, VV_PROGPATH,
VV_JOB_DATA,
VV_COMMAND_OUTPUT, VV_COMMAND_OUTPUT,
VV_LEN, /* number of v: vars */ VV_LEN, /* number of v: vars */
}; };

View File

@@ -5187,7 +5187,6 @@ static struct event_name {
{"InsertEnter", EVENT_INSERTENTER}, {"InsertEnter", EVENT_INSERTENTER},
{"InsertLeave", EVENT_INSERTLEAVE}, {"InsertLeave", EVENT_INSERTLEAVE},
{"InsertCharPre", EVENT_INSERTCHARPRE}, {"InsertCharPre", EVENT_INSERTCHARPRE},
{"JobActivity", EVENT_JOBACTIVITY},
{"MenuPopup", EVENT_MENUPOPUP}, {"MenuPopup", EVENT_MENUPOPUP},
{"QuickFixCmdPost", EVENT_QUICKFIXCMDPOST}, {"QuickFixCmdPost", EVENT_QUICKFIXCMDPOST},
{"QuickFixCmdPre", EVENT_QUICKFIXCMDPRE}, {"QuickFixCmdPre", EVENT_QUICKFIXCMDPRE},
@@ -6595,7 +6594,6 @@ apply_autocmds_group (
|| event == EVENT_QUICKFIXCMDPRE || event == EVENT_QUICKFIXCMDPRE
|| event == EVENT_COLORSCHEME || event == EVENT_COLORSCHEME
|| event == EVENT_QUICKFIXCMDPOST || event == EVENT_QUICKFIXCMDPOST
|| event == EVENT_JOBACTIVITY
|| event == EVENT_TABCLOSED) || event == EVENT_TABCLOSED)
fname = vim_strsave(fname); fname = vim_strsave(fname);
else else

View File

@@ -63,7 +63,6 @@ typedef enum auto_event {
EVENT_INSERTCHANGE, /* when changing Insert/Replace mode */ EVENT_INSERTCHANGE, /* when changing Insert/Replace mode */
EVENT_INSERTENTER, /* when entering Insert mode */ EVENT_INSERTENTER, /* when entering Insert mode */
EVENT_INSERTLEAVE, /* when leaving Insert mode */ EVENT_INSERTLEAVE, /* when leaving Insert mode */
EVENT_JOBACTIVITY, /* when job sent some data */
EVENT_MENUPOPUP, /* just before popup menu is displayed */ EVENT_MENUPOPUP, /* just before popup menu is displayed */
EVENT_QUICKFIXCMDPOST, /* after :make, :grep etc. */ EVENT_QUICKFIXCMDPOST, /* after :make, :grep etc. */
EVENT_QUICKFIXCMDPRE, /* before :make, :grep etc. */ EVENT_QUICKFIXCMDPRE, /* before :make, :grep etc. */

View File

@@ -293,8 +293,8 @@ int main(int argc, char **argv)
"matchstr(expand(\"<amatch>\"), " "matchstr(expand(\"<amatch>\"), "
"'\\c\\mterm://\\%(.\\{-}//\\%(\\d\\+:\\)\\?\\)\\?\\zs.*'), " "'\\c\\mterm://\\%(.\\{-}//\\%(\\d\\+:\\)\\?\\)\\?\\zs.*'), "
// capture the working directory // capture the working directory
"get(matchlist(expand(\"<amatch>\"), " "{'cwd': get(matchlist(expand(\"<amatch>\"), "
"'\\c\\mterm://\\(.\\{-}\\)//'), 1, ''))"); "'\\c\\mterm://\\(.\\{-}\\)//'), 1, '')})");
/* Execute --cmd arguments. */ /* Execute --cmd arguments. */
exe_pre_commands(&params); exe_pre_commands(&params);

View File

@@ -347,7 +347,7 @@ static void job_err(RStream *rstream, void *data, bool eof)
} }
} }
static void job_exit(Job *job, void *data) static void job_exit(Job *job, int status, void *data)
{ {
decref(data); decref(data);
} }

View File

@@ -11,7 +11,7 @@ typedef struct job Job;
/// ///
/// @param id The job id /// @param id The job id
/// @param data Some data associated with the job by the caller /// @param data Some data associated with the job by the caller
typedef void (*job_exit_cb)(Job *job, void *data); typedef void (*job_exit_cb)(Job *job, int status, void *data);
// Job startup options // Job startup options
// job_exit_cb Callback that will be invoked when the job exits // job_exit_cb Callback that will be invoked when the job exits

View File

@@ -88,7 +88,7 @@ static inline void job_exit_callback(Job *job)
if (job->opts.exit_cb) { if (job->opts.exit_cb) {
// Invoke the exit callback // Invoke the exit callback
job->opts.exit_cb(job, job->opts.data); job->opts.exit_cb(job, job->status, job->opts.data);
} }
if (stop_requests && !--stop_requests) { if (stop_requests && !--stop_requests) {

View File

@@ -1,10 +1,11 @@
local helpers = require('test.functional.helpers') local helpers = require('test.functional.helpers')
local clear, nvim, eq, neq, ok, expect, eval, next_message, run, stop, session local clear, nvim, eq, neq, ok, expect, eval, next_msg, run, stop, session
= helpers.clear, helpers.nvim, helpers.eq, helpers.neq, helpers.ok, = helpers.clear, helpers.nvim, helpers.eq, helpers.neq, helpers.ok,
helpers.expect, helpers.eval, helpers.next_message, helpers.run, helpers.expect, helpers.eval, helpers.next_message, helpers.run,
helpers.stop, helpers.session helpers.stop, helpers.session
local nvim_dir, insert = helpers.nvim_dir, helpers.insert local nvim_dir, insert = helpers.nvim_dir, helpers.insert
local source = helpers.source
describe('jobs', function() describe('jobs', function()
@@ -13,46 +14,44 @@ describe('jobs', function()
before_each(function() before_each(function()
clear() clear()
channel = nvim('get_api_info')[1] channel = nvim('get_api_info')[1]
nvim('set_var', 'channel', channel)
source([[
function! s:OnEvent(id, data, event)
let userdata = get(self, 'user')
call rpcnotify(g:channel, a:event, userdata, a:data)
endfunction
let g:job_opts = {
\ 'on_stdout': function('s:OnEvent'),
\ 'on_stderr': function('s:OnEvent'),
\ 'on_exit': function('s:OnEvent'),
\ 'user': 0
\ }
]])
end) end)
-- Creates the string to make an autocmd to notify us.
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()
return "au! JobActivity xxx call rpcnotify("..channel..", 'j', v:job_data)"
end
it('returns 0 when it fails to start', function() it('returns 0 when it fails to start', function()
local status, rv = pcall(eval, "jobstart('', '')") local status, rv = pcall(eval, "jobstart([])")
eq(false, status) eq(false, status)
ok(rv ~= nil) ok(rv ~= nil)
end) end)
it('calls JobActivity when the job writes and exits', function() it('invokes callbacks when the job writes and exits', function()
nvim('command', notify_str('v:job_data[1]')) nvim('command', "call jobstart(['echo'], g:job_opts)")
nvim('command', "call jobstart('xxx', 'echo')") eq({'notification', 'stdout', {0, {'', ''}}}, next_msg())
eq({'notification', 'stdout', {}}, next_message()) eq({'notification', 'exit', {0, 0}}, next_msg())
eq({'notification', 'exit', {}}, next_message())
end) end)
it('allows interactive commands', function() it('allows interactive commands', function()
nvim('command', notify_str('v:job_data[1]', 'get(v:job_data, 2)')) nvim('command', "let j = jobstart(['cat', '-'], g:job_opts)")
nvim('command', "let j = jobstart('xxx', 'cat', ['-'])")
neq(0, eval('j')) neq(0, eval('j'))
nvim('command', 'call jobsend(j, "abc\\n")') nvim('command', 'call jobsend(j, "abc\\n")')
eq({'notification', 'stdout', {{'abc', ''}}}, next_message()) eq({'notification', 'stdout', {0, {'abc', ''}}}, next_msg())
nvim('command', 'call jobsend(j, "123\\nxyz\\n")') nvim('command', 'call jobsend(j, "123\\nxyz\\n")')
eq({'notification', 'stdout', {{'123', 'xyz', ''}}}, next_message()) eq({'notification', 'stdout', {0, {'123', 'xyz', ''}}}, next_msg())
nvim('command', 'call jobsend(j, [123, "xyz", ""])') nvim('command', 'call jobsend(j, [123, "xyz", ""])')
eq({'notification', 'stdout', {{'123', 'xyz', ''}}}, next_message()) eq({'notification', 'stdout', {0, {'123', 'xyz', ''}}}, next_msg())
nvim('command', "call jobstop(j)") nvim('command', "call jobstop(j)")
eq({'notification', 'exit', {0}}, next_message()) eq({'notification', 'exit', {0, 0}}, next_msg())
end) end)
it('preserves NULs', function() it('preserves NULs', function()
@@ -63,90 +62,130 @@ describe('jobs', function()
file:close() file:close()
-- v:job_data preserves NULs. -- v:job_data preserves NULs.
nvim('command', notify_str('v:job_data[1]', 'get(v:job_data, 2)')) nvim('command', "let j = jobstart(['cat', '"..filename.."'], g:job_opts)")
nvim('command', "let j = jobstart('xxx', 'cat', ['"..filename.."'])") eq({'notification', 'stdout', {0, {'abc\ndef', ''}}}, next_msg())
eq({'notification', 'stdout', {{'abc\ndef', ''}}}, next_message()) eq({'notification', 'exit', {0, 0}}, next_msg())
eq({'notification', 'exit', {0}}, next_message())
os.remove(filename) os.remove(filename)
-- jobsend() preserves NULs. -- jobsend() preserves NULs.
nvim('command', "let j = jobstart('xxx', 'cat', ['-'])") nvim('command', "let j = jobstart(['cat', '-'], g:job_opts)")
nvim('command', [[call jobsend(j, ["123\n456",""])]]) nvim('command', [[call jobsend(j, ["123\n456",""])]])
eq({'notification', 'stdout', {{'123\n456', ''}}}, next_message()) eq({'notification', 'stdout', {0, {'123\n456', ''}}}, next_msg())
nvim('command', "call jobstop(j)") nvim('command', "call jobstop(j)")
end) end)
it('will not buffer data if it doesnt end in newlines', function() it('will not buffer data if it doesnt end in newlines', function()
nvim('command', notify_str('v:job_data[1]', 'get(v:job_data, 2)')) nvim('command', "let j = jobstart(['cat', '-'], g:job_opts)")
nvim('command', "let j = jobstart('xxx', 'cat', ['-'])")
nvim('command', 'call jobsend(j, "abc\\nxyz")') nvim('command', 'call jobsend(j, "abc\\nxyz")')
eq({'notification', 'stdout', {{'abc', 'xyz'}}}, next_message()) eq({'notification', 'stdout', {0, {'abc', 'xyz'}}}, next_msg())
nvim('command', "call jobstop(j)") nvim('command', "call jobstop(j)")
eq({'notification', 'exit', {0}}, next_message()) eq({'notification', 'exit', {0, 0}}, next_msg())
end) end)
it('can preserve newlines', function() it('can preserve newlines', function()
nvim('command', notify_str('v:job_data[1]', 'get(v:job_data, 2)')) nvim('command', "let j = jobstart(['cat', '-'], g:job_opts)")
nvim('command', "let j = jobstart('xxx', 'cat', ['-'])")
nvim('command', 'call jobsend(j, "a\\n\\nc\\n\\n\\n\\nb\\n\\n")') nvim('command', 'call jobsend(j, "a\\n\\nc\\n\\n\\n\\nb\\n\\n")')
eq({'notification', 'stdout', {{'a', '', 'c', '', '', '', 'b', '', ''}}}, eq({'notification', 'stdout',
next_message()) {0, {'a', '', 'c', '', '', '', 'b', '', ''}}}, next_msg())
end) end)
it('can preserve nuls', function() it('can preserve nuls', function()
nvim('command', notify_str('v:job_data[1]', 'get(v:job_data, 2)')) nvim('command', "let j = jobstart(['cat', '-'], g:job_opts)")
nvim('command', "let j = jobstart('xxx', 'cat', ['-'])")
nvim('command', 'call jobsend(j, ["\n123\n", "abc\\nxyz\n", ""])') nvim('command', 'call jobsend(j, ["\n123\n", "abc\\nxyz\n", ""])')
eq({'notification', 'stdout', {{'\n123\n', 'abc\nxyz\n', ''}}}, eq({'notification', 'stdout', {0, {'\n123\n', 'abc\nxyz\n', ''}}},
next_message()) next_msg())
nvim('command', "call jobstop(j)") nvim('command', "call jobstop(j)")
eq({'notification', 'exit', {0}}, next_message()) eq({'notification', 'exit', {0, 0}}, next_msg())
end) end)
it('can avoid sending final newline', function() it('can avoid sending final newline', function()
nvim('command', notify_str('v:job_data[1]', 'get(v:job_data, 2)')) nvim('command', "let j = jobstart(['cat', '-'], g:job_opts)")
nvim('command', "let j = jobstart('xxx', 'cat', ['-'])")
nvim('command', 'call jobsend(j, ["some data", "without\nfinal nl"])') nvim('command', 'call jobsend(j, ["some data", "without\nfinal nl"])')
eq({'notification', 'stdout', {{'some data', 'without\nfinal nl'}}}, eq({'notification', 'stdout', {0, {'some data', 'without\nfinal nl'}}},
next_message()) next_msg())
nvim('command', "call jobstop(j)") nvim('command', "call jobstop(j)")
eq({'notification', 'exit', {0}}, next_message()) eq({'notification', 'exit', {0, 0}}, next_msg())
end) end)
it('will not allow jobsend/stop on a non-existent job', function() it('will not allow jobsend/stop on a non-existent job', function()
eq(false, pcall(eval, "jobsend(-1, 'lol')")) eq(false, pcall(eval, "jobsend(-1, 'lol')"))
eq(false, pcall(eval, "jobstop(-1)")) eq(false, pcall(eval, "jobstop(-1)"))
end) end)
it('will not allow jobstop twice on the same job', function() it('will not allow jobstop twice on the same job', function()
nvim('command', "let j = jobstart('xxx', 'cat', ['-'])") nvim('command', "let j = jobstart(['cat', '-'], g:job_opts)")
neq(0, eval('j')) neq(0, eval('j'))
eq(true, pcall(eval, "jobstop(j)")) eq(true, pcall(eval, "jobstop(j)"))
eq(false, pcall(eval, "jobstop(j)")) eq(false, pcall(eval, "jobstop(j)"))
end) end)
it('will not cause a memory leak if we leave a job running', function() it('will not cause a memory leak if we leave a job running', function()
nvim('command', "call jobstart('xxx', 'cat', ['-'])") nvim('command', "call jobstart(['cat', '-'], g:job_opts)")
end)
it('can pass user data to the callback', function()
nvim('command', 'let g:job_opts.user = {"n": 5, "s": "str", "l": [1]}')
nvim('command', "call jobstart(['echo'], g:job_opts)")
local data = {n = 5, s = 'str', l = {1}}
eq({'notification', 'stdout', {data, {'', ''}}}, next_msg())
eq({'notification', 'exit', {data, 0}}, next_msg())
end)
it('can omit options', function()
neq(0, nvim('eval', 'delete(".Xtestjob")'))
nvim('command', "call jobstart(['touch', '.Xtestjob'])")
nvim('command', "sleep 100m")
eq(0, nvim('eval', 'delete(".Xtestjob")'))
end)
it('can omit data callbacks', function()
nvim('command', 'unlet g:job_opts.on_stdout')
nvim('command', 'unlet g:job_opts.on_stderr')
nvim('command', 'let g:job_opts.user = 5')
nvim('command', "call jobstart(['echo'], g:job_opts)")
eq({'notification', 'exit', {5, 0}}, next_msg())
end)
it('can omit exit callback', function()
nvim('command', 'unlet g:job_opts.on_exit')
nvim('command', 'let g:job_opts.user = 5')
nvim('command', "call jobstart(['echo'], g:job_opts)")
eq({'notification', 'stdout', {5, {'', ''}}}, next_msg())
end)
it('will pass return code with the exit event', function()
nvim('command', 'let g:job_opts.user = 5')
nvim('command', "call jobstart([&sh, '-c', 'exit 55'], g:job_opts)")
eq({'notification', 'exit', {5, 55}}, next_msg())
end)
it('can receive dictionary functions', function()
source([[
let g:dict = {'id': 10}
function g:dict.on_exit(id, code, event)
call rpcnotify(g:channel, a:event, a:code, self.id)
endfunction
call jobstart([&sh, '-c', 'exit 45'], g:dict)
]])
eq({'notification', 'exit', {45, 10}}, next_msg())
end) end)
-- FIXME need to wait until jobsend succeeds before calling jobstop -- FIXME need to wait until jobsend succeeds before calling jobstop
pending('will only emit the "exit" event after "stdout" and "stderr"', function() pending('will only emit the "exit" event after "stdout" and "stderr"', function()
nvim('command', notify_job()) nvim('command', "let j = jobstart(['cat', '-'], g:job_opts)")
nvim('command', "let j = jobstart('xxx', 'cat', ['-'])")
local jobid = nvim('eval', 'j') local jobid = nvim('eval', 'j')
nvim('eval', 'jobsend(j, "abcdef")') nvim('eval', 'jobsend(j, "abcdef")')
nvim('eval', 'jobstop(j)') nvim('eval', 'jobstop(j)')
eq({'notification', 'j', {{jobid, 'stdout', {'abcdef'}}}}, next_message()) eq({'notification', 'j', {0, {jobid, 'stdout', {'abcdef'}}}}, next_msg())
eq({'notification', 'j', {{jobid, 'exit'}}}, next_message()) eq({'notification', 'j', {0, {jobid, 'exit'}}}, next_msg())
end) end)
describe('running tty-test program', function() describe('running tty-test program', function()
local function next_chunk() local function next_chunk()
local rv = '' local rv = ''
while true do while true do
local msg = next_message() local msg = next_msg()
local data = msg[3][1] local data = msg[3][2]
for i = 1, #data do for i = 1, #data do
data[i] = data[i]:gsub('\n', '\000') data[i] = data[i]:gsub('\n', '\000')
end end
@@ -166,9 +205,9 @@ describe('jobs', function()
before_each(function() before_each(function()
-- the full path to tty-test seems to be required when running on travis. -- the full path to tty-test seems to be required when running on travis.
insert(nvim_dir .. '/tty-test') insert(nvim_dir .. '/tty-test')
nvim('command', 'let exec = expand("<cfile>:p")') nvim('command', 'let g:job_opts.pty = 1')
nvim('command', notify_str('v:job_data[1]', 'get(v:job_data, 2)')) nvim('command', 'let exec = [expand("<cfile>:p")]')
nvim('command', "let j = jobstart('xxx', exec, [], {})") nvim('command', "let j = jobstart(exec, g:job_opts)")
eq('tty ready', next_chunk()) eq('tty ready', next_chunk())
end) end)