# HG changeset patch # User Christian Brabandt # Date 1520710204 -3600 # Node ID 33eea5ce5415d86e2b4a484aaa22a202993acae9 # Parent 02b3f719eacb9aa93e060b5e0801b5eda59c31cc patch 8.0.1593: :qall never exits with an active terminal window commit https://github.com/vim/vim/commit/25cdd9c33b21ddbd31321c075873bb225450d2d2 Author: Bram Moolenaar Date: Sat Mar 10 20:28:12 2018 +0100 patch 8.0.1593: :qall never exits with an active terminal window Problem: :qall never exits with an active terminal window. Solution: Add a way to kill a job in a terminal window. diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -1,4 +1,4 @@ -*eval.txt* For Vim version 8.0. Last change: 2018 Mar 09 +*eval.txt* For Vim version 8.0. Last change: 2018 Mar 10 VIM REFERENCE MANUAL by Bram Moolenaar @@ -2435,6 +2435,7 @@ term_gettty({buf}, [{input}]) String get term_list() List get the list of terminal buffers term_scrape({buf}, {row}) List get row of a terminal screen term_sendkeys({buf}, {keys}) none send keystrokes to a terminal +term_setkill({buf}, {how}) none set signal to stop job in terminal term_setrestore({buf}, {command}) none set command to restore terminal term_start({cmd}, {options}) Job open a terminal window and run a job term_wait({buf} [, {time}]) Number wait for screen to be updated @@ -8276,6 +8277,8 @@ term_getline({buf}, {row}) *term_getl The first line has {row} one. When {row} is "." the cursor line is used. When {row} is invalid an empty string is returned. + + To get attributes of each character use |term_scrape()|. {only available when compiled with the |+terminal| feature} term_getscrolled({buf}) *term_getscrolled()* @@ -8361,6 +8364,18 @@ term_sendkeys({buf}, {keys}) *term_se means the character CTRL-X. {only available when compiled with the |+terminal| feature} +term_setkill({buf}, {how}) *term_setkill()* + When exiting Vim or trying to close the terminal window in + another way, {how} defines whether the job in the terminal can + be stopped. + When {how} is empty (the default), the job will not be + stopped, trying to exit will result in |E947|. + Otherwise, {how} specifies what signal to send to the job. + See |job_stop()| for the values. + + After sending the signal Vim will wait for up to a second to + check that the job actually stopped. + term_setrestore({buf}, {command}) *term_setrestore()* Set the command to write in a session file to restore the job in this terminal. The line written in the session file is: > @@ -8416,6 +8431,8 @@ term_start({cmd}, {options}) *term_st "hidden" do not open a window "norestore" do not add the terminal window to a session file + "term_kill" what to do when trying to close the + terminal window, see |term_setkill()| "term_finish" What to do when the job is finished: "close": close any windows "open": open window if needed diff --git a/runtime/doc/terminal.txt b/runtime/doc/terminal.txt --- a/runtime/doc/terminal.txt +++ b/runtime/doc/terminal.txt @@ -1,4 +1,4 @@ -*terminal.txt* For Vim version 8.0. Last change: 2018 Mar 09 +*terminal.txt* For Vim version 8.0. Last change: 2018 Mar 10 VIM REFERENCE MANUAL by Bram Moolenaar @@ -166,6 +166,9 @@ Syntax ~ no window will be used. ++norestore Do not include this terminal window in a session file. + ++kill={how} When trying to close the terminal + window kill the job with {how}. See + |term_setkill()| for the values. ++rows={height} Use {height} for the terminal window height. If the terminal uses the full Vim height (no window above or below @@ -189,8 +192,12 @@ Syntax ~ If you want to use more options use the |term_start()| function. -When the buffer associated with the terminal is unloaded or wiped out the job -is killed, similar to calling `job_stop(job, "kill")` +When the buffer associated with the terminal is forcibly unloaded or wiped out +the job is killed, similar to calling `job_stop(job, "kill")` . +Closing the window normally results in |E947|. When a kill method was set +with "++kill={how}" or |term_setkill()| then closing the window will use that +way to kill or interrupt the job. For example: > + :term ++kill=term tail -f /tmp/log So long as the job is running the window behaves like it contains a modified buffer. Trying to close the window with `CTRL-W :quit` fails. When using diff --git a/src/channel.c b/src/channel.c --- a/src/channel.c +++ b/src/channel.c @@ -4746,50 +4746,57 @@ get_job_options(typval_T *tv, jobopt_T * { if (!(supported2 & JO2_TERM_ROWS)) break; - opt->jo_set |= JO2_TERM_ROWS; + opt->jo_set2 |= JO2_TERM_ROWS; opt->jo_term_rows = get_tv_number(item); } else if (STRCMP(hi->hi_key, "term_cols") == 0) { if (!(supported2 & JO2_TERM_COLS)) break; - opt->jo_set |= JO2_TERM_COLS; + opt->jo_set2 |= JO2_TERM_COLS; opt->jo_term_cols = get_tv_number(item); } else if (STRCMP(hi->hi_key, "vertical") == 0) { if (!(supported2 & JO2_VERTICAL)) break; - opt->jo_set |= JO2_VERTICAL; + opt->jo_set2 |= JO2_VERTICAL; opt->jo_vertical = get_tv_number(item); } else if (STRCMP(hi->hi_key, "curwin") == 0) { if (!(supported2 & JO2_CURWIN)) break; - opt->jo_set |= JO2_CURWIN; + opt->jo_set2 |= JO2_CURWIN; opt->jo_curwin = get_tv_number(item); } else if (STRCMP(hi->hi_key, "hidden") == 0) { if (!(supported2 & JO2_HIDDEN)) break; - opt->jo_set |= JO2_HIDDEN; + opt->jo_set2 |= JO2_HIDDEN; opt->jo_hidden = get_tv_number(item); } else if (STRCMP(hi->hi_key, "norestore") == 0) { if (!(supported2 & JO2_NORESTORE)) break; - opt->jo_set |= JO2_NORESTORE; + opt->jo_set2 |= JO2_NORESTORE; opt->jo_term_norestore = get_tv_number(item); } + else if (STRCMP(hi->hi_key, "term_kill") == 0) + { + if (!(supported2 & JO2_TERM_KILL)) + break; + opt->jo_set2 |= JO2_TERM_KILL; + opt->jo_term_kill = get_tv_string_chk(item); + } #endif else if (STRCMP(hi->hi_key, "env") == 0) { if (!(supported2 & JO2_ENV)) break; - opt->jo_set |= JO2_ENV; + opt->jo_set2 |= JO2_ENV; opt->jo_env = item->vval.v_dict; ++item->vval.v_dict->dv_refcount; } @@ -4803,7 +4810,7 @@ get_job_options(typval_T *tv, jobopt_T * EMSG2(_(e_invargval), "cwd"); return FAIL; } - opt->jo_set |= JO2_CWD; + opt->jo_set2 |= JO2_CWD; } else if (STRCMP(hi->hi_key, "waittime") == 0) { diff --git a/src/evalfunc.c b/src/evalfunc.c --- a/src/evalfunc.c +++ b/src/evalfunc.c @@ -867,6 +867,7 @@ static struct fst {"term_list", 0, 0, f_term_list}, {"term_scrape", 2, 2, f_term_scrape}, {"term_sendkeys", 2, 2, f_term_sendkeys}, + {"term_setkill", 2, 2, f_term_setkill}, {"term_setrestore", 2, 2, f_term_setrestore}, {"term_start", 1, 2, f_term_start}, {"term_wait", 1, 2, f_term_wait}, diff --git a/src/ex_cmds2.c b/src/ex_cmds2.c --- a/src/ex_cmds2.c +++ b/src/ex_cmds2.c @@ -2254,7 +2254,7 @@ add_bufnum(int *bufnrs, int *bufnump, in /* * Return TRUE if any buffer was changed and cannot be abandoned. * That changed buffer becomes the current buffer. - * When "unload" is true the current buffer is unloaded instead of making it + * When "unload" is TRUE the current buffer is unloaded instead of making it * hidden. This is used for ":q!". */ int @@ -2272,6 +2272,7 @@ check_changed_any( tabpage_T *tp; win_T *wp; + /* Make a list of all buffers, with the most important ones first. */ FOR_ALL_BUFFERS(buf) ++bufcount; @@ -2284,17 +2285,19 @@ check_changed_any( /* curbuf */ bufnrs[bufnum++] = curbuf->b_fnum; - /* buf in curtab */ + + /* buffers in current tab */ FOR_ALL_WINDOWS(wp) if (wp->w_buffer != curbuf) add_bufnum(bufnrs, &bufnum, wp->w_buffer->b_fnum); - /* buf in other tab */ + /* buffers in other tabs */ FOR_ALL_TABPAGES(tp) if (tp != curtab) for (wp = tp->tp_firstwin; wp != NULL; wp = wp->w_next) add_bufnum(bufnrs, &bufnum, wp->w_buffer->b_fnum); - /* any other buf */ + + /* any other buffer */ FOR_ALL_BUFFERS(buf) add_bufnum(bufnrs, &bufnum, buf->b_fnum); @@ -2308,6 +2311,14 @@ check_changed_any( bufref_T bufref; set_bufref(&bufref, buf); +#ifdef FEAT_TERMINAL + if (term_job_running(buf->b_term)) + { + if (term_try_stop_job(buf) == FAIL) + break; + } + else +#endif /* Try auto-writing the buffer. If this fails but the buffer no * longer exists it's not changed, that's OK. */ if (check_changed(buf, (p_awa ? CCGD_AW : 0) @@ -2320,6 +2331,7 @@ check_changed_any( if (i >= bufnum) goto theend; + /* Get here if "buf" cannot be abandoned. */ ret = TRUE; exiting = FALSE; #if defined(FEAT_GUI_DIALOG) || defined(FEAT_CON_DIALOG) diff --git a/src/proto/terminal.pro b/src/proto/terminal.pro --- a/src/proto/terminal.pro +++ b/src/proto/terminal.pro @@ -2,11 +2,11 @@ void ex_terminal(exarg_T *eap); int term_write_session(FILE *fd, win_T *wp); int term_should_restore(buf_T *buf); -void f_term_setrestore(typval_T *argvars, typval_T *rettv); void free_terminal(buf_T *buf); void write_to_term(buf_T *buffer, char_u *msg, channel_T *channel); int term_job_running(term_T *term); int term_none_open(term_T *term); +int term_try_stop_job(buf_T *buf); int term_in_normal_mode(void); void term_enter_job_mode(void); int send_keys_to_term(term_T *term, int c, int typed); @@ -41,6 +41,8 @@ void f_term_gettty(typval_T *argvars, ty void f_term_list(typval_T *argvars, typval_T *rettv); void f_term_scrape(typval_T *argvars, typval_T *rettv); void f_term_sendkeys(typval_T *argvars, typval_T *rettv); +void f_term_setrestore(typval_T *argvars, typval_T *rettv); +void f_term_setkill(typval_T *argvars, typval_T *rettv); void f_term_start(typval_T *argvars, typval_T *rettv); void f_term_wait(typval_T *argvars, typval_T *rettv); void term_send_eof(channel_T *ch); diff --git a/src/structs.h b/src/structs.h --- a/src/structs.h +++ b/src/structs.h @@ -1707,7 +1707,8 @@ struct channel_S { #define JO2_TERM_OPENCMD 0x0800 /* "term_opencmd" */ #define JO2_EOF_CHARS 0x1000 /* "eof_chars" */ #define JO2_NORESTORE 0x2000 /* "norestore" */ -#define JO2_ALL 0x2FFF +#define JO2_TERM_KILL 0x4000 /* "term_kill" */ +#define JO2_ALL 0x7FFF #define JO_MODE_ALL (JO_MODE + JO_IN_MODE + JO_OUT_MODE + JO_ERR_MODE) #define JO_CB_ALL \ @@ -1775,6 +1776,7 @@ typedef struct char_u *jo_term_opencmd; int jo_term_finish; char_u *jo_eof_chars; + char_u *jo_term_kill; #endif } jobopt_T; diff --git a/src/terminal.c b/src/terminal.c --- a/src/terminal.c +++ b/src/terminal.c @@ -137,6 +137,7 @@ struct terminal_S { #if defined(FEAT_SESSION) char_u *tl_command; #endif + char_u *tl_kill; /* last known vterm size */ int tl_rows; @@ -535,6 +536,13 @@ term_start(typval_T *argvar, jobopt_T *o } #endif + if (opt->jo_term_kill != NULL) + { + char_u *p = skiptowhite(opt->jo_term_kill); + + term->tl_kill = vim_strnsave(opt->jo_term_kill, p - opt->jo_term_kill); + } + /* System dependent: setup the vterm and maybe start the job in it. */ if (argvar->v_type == VAR_STRING && argvar->vval.v_string != NULL @@ -611,6 +619,13 @@ ex_terminal(exarg_T *eap) opt.jo_hidden = 1; else if ((int)(p - cmd) == 9 && STRNICMP(cmd, "norestore", 9) == 0) opt.jo_term_norestore = 1; + else if ((int)(p - cmd) == 4 && STRNICMP(cmd, "kill", 4) == 0 + && ep != NULL) + { + opt.jo_set2 |= JO2_TERM_KILL; + opt.jo_term_kill = ep + 1; + p = skiptowhite(cmd); + } else if ((int)(p - cmd) == 4 && STRNICMP(cmd, "rows", 4) == 0 && ep != NULL && isdigit(ep[1])) { @@ -644,7 +659,7 @@ ex_terminal(exarg_T *eap) if (*p) *p = NUL; EMSG2(_("E181: Invalid attribute: %s"), cmd); - return; + goto theend; } cmd = skipwhite(p); } @@ -667,6 +682,8 @@ ex_terminal(exarg_T *eap) argvar[1].v_type = VAR_UNKNOWN; term_start(argvar, &opt, FALSE, eap->forceit); vim_free(tofree); + +theend: vim_free(opt.jo_eof_chars); } @@ -758,6 +775,7 @@ free_terminal(buf_T *buf) #ifdef FEAT_SESSION vim_free(term->tl_command); #endif + vim_free(term->tl_kill); vim_free(term->tl_status_text); vim_free(term->tl_opencmd); vim_free(term->tl_eof_chars); @@ -1081,6 +1099,56 @@ term_none_open(term_T *term) } /* + * Used when exiting: kill the job in "buf" if so desired. + * Return OK when the job finished. + * Return FAIL when the job is still running. + */ + int +term_try_stop_job(buf_T *buf) +{ + int count; + char *how = (char *)buf->b_term->tl_kill; + +#if defined(FEAT_GUI_DIALOG) || defined(FEAT_CON_DIALOG) + if ((how == NULL || *how == NUL) && (p_confirm || cmdmod.confirm)) + { + char_u buff[DIALOG_MSG_SIZE]; + int ret; + + dialog_msg(buff, _("Kill job in \"%s\"?"), buf->b_fname); + ret = vim_dialog_yesnocancel(VIM_QUESTION, NULL, buff, 1); + if (ret == VIM_YES) + how = "kill"; + else if (ret == VIM_CANCEL) + return FAIL; + } +#endif + if (how == NULL || *how == NUL) + return FAIL; + + job_stop(buf->b_term->tl_job, NULL, how); + + /* wait for up to a second for the job to die */ + for (count = 0; count < 100; ++count) + { + /* buffer, terminal and job may be cleaned up while waiting */ + if (!buf_valid(buf) + || buf->b_term == NULL + || buf->b_term->tl_job == NULL) + return OK; + + /* call job_status() to update jv_status */ + job_status(buf->b_term->tl_job); + if (buf->b_term->tl_job->jv_status >= JOB_ENDED) + return OK; + ui_delay(10L, FALSE); + mch_check_messages(); + parse_queued_messages(); + } + return FAIL; +} + +/* * Add the last line of the scrollback buffer to the buffer in the window. */ static void @@ -2922,10 +2990,11 @@ set_terminal_default_colors(int cterm_fg /* * Get the buffer from the first argument in "argvars". - * Returns NULL when the buffer is not for a terminal window. + * Returns NULL when the buffer is not for a terminal window and logs a message + * with "where". */ static buf_T * -term_get_buf(typval_T *argvars) +term_get_buf(typval_T *argvars, char *where) { buf_T *buf; @@ -2934,7 +3003,10 @@ term_get_buf(typval_T *argvars) buf = get_buf_tv(&argvars[0], FALSE); --emsg_off; if (buf == NULL || buf->b_term == NULL) + { + ch_log(NULL, "%s: invalid buffer argument", where); return NULL; + } return buf; } @@ -2980,7 +3052,7 @@ dump_term_color(FILE *fd, VTermColor *co void f_term_dumpwrite(typval_T *argvars, typval_T *rettv UNUSED) { - buf_T *buf = term_get_buf(argvars); + buf_T *buf = term_get_buf(argvars, "term_dumpwrite()"); term_T *term; char_u *fname; int max_height = 0; @@ -3719,7 +3791,7 @@ f_term_dumpload(typval_T *argvars, typva void f_term_getaltscreen(typval_T *argvars, typval_T *rettv) { - buf_T *buf = term_get_buf(argvars); + buf_T *buf = term_get_buf(argvars, "term_getaltscreen()"); if (buf == NULL) return; @@ -3766,7 +3838,7 @@ f_term_getattr(typval_T *argvars, typval void f_term_getcursor(typval_T *argvars, typval_T *rettv) { - buf_T *buf = term_get_buf(argvars); + buf_T *buf = term_get_buf(argvars, "term_getcursor()"); term_T *term; list_T *l; dict_T *d; @@ -3800,7 +3872,7 @@ f_term_getcursor(typval_T *argvars, typv void f_term_getjob(typval_T *argvars, typval_T *rettv) { - buf_T *buf = term_get_buf(argvars); + buf_T *buf = term_get_buf(argvars, "term_getjob()"); rettv->v_type = VAR_JOB; rettv->vval.v_job = NULL; @@ -3828,7 +3900,7 @@ get_row_number(typval_T *tv, term_T *ter void f_term_getline(typval_T *argvars, typval_T *rettv) { - buf_T *buf = term_get_buf(argvars); + buf_T *buf = term_get_buf(argvars, "term_getline()"); term_T *term; int row; @@ -3875,7 +3947,7 @@ f_term_getline(typval_T *argvars, typval void f_term_getscrolled(typval_T *argvars, typval_T *rettv) { - buf_T *buf = term_get_buf(argvars); + buf_T *buf = term_get_buf(argvars, "term_getscrolled()"); if (buf == NULL) return; @@ -3888,7 +3960,7 @@ f_term_getscrolled(typval_T *argvars, ty void f_term_getsize(typval_T *argvars, typval_T *rettv) { - buf_T *buf = term_get_buf(argvars); + buf_T *buf = term_get_buf(argvars, "term_getsize()"); list_T *l; if (rettv_list_alloc(rettv) == FAIL) @@ -3907,7 +3979,7 @@ f_term_getsize(typval_T *argvars, typval void f_term_getstatus(typval_T *argvars, typval_T *rettv) { - buf_T *buf = term_get_buf(argvars); + buf_T *buf = term_get_buf(argvars, "term_getstatus()"); term_T *term; char_u val[100]; @@ -3931,7 +4003,7 @@ f_term_getstatus(typval_T *argvars, typv void f_term_gettitle(typval_T *argvars, typval_T *rettv) { - buf_T *buf = term_get_buf(argvars); + buf_T *buf = term_get_buf(argvars, "term_gettitle()"); rettv->v_type = VAR_STRING; if (buf == NULL) @@ -3947,7 +4019,7 @@ f_term_gettitle(typval_T *argvars, typva void f_term_gettty(typval_T *argvars, typval_T *rettv) { - buf_T *buf = term_get_buf(argvars); + buf_T *buf = term_get_buf(argvars, "term_gettty()"); char_u *p; int num = 0; @@ -4005,7 +4077,7 @@ f_term_list(typval_T *argvars UNUSED, ty void f_term_scrape(typval_T *argvars, typval_T *rettv) { - buf_T *buf = term_get_buf(argvars); + buf_T *buf = term_get_buf(argvars, "term_scrape()"); VTermScreen *screen = NULL; VTermPos pos; list_T *l; @@ -4114,7 +4186,7 @@ f_term_scrape(typval_T *argvars, typval_ void f_term_sendkeys(typval_T *argvars, typval_T *rettv) { - buf_T *buf = term_get_buf(argvars); + buf_T *buf = term_get_buf(argvars, "term_sendkeys()"); char_u *msg; term_T *term; @@ -4143,7 +4215,7 @@ f_term_sendkeys(typval_T *argvars, typva f_term_setrestore(typval_T *argvars UNUSED, typval_T *rettv UNUSED) { #if defined(FEAT_SESSION) - buf_T *buf = term_get_buf(argvars); + buf_T *buf = term_get_buf(argvars, "term_setrestore()"); term_T *term; char_u *cmd; @@ -4160,6 +4232,27 @@ f_term_setrestore(typval_T *argvars UNUS } /* + * "term_setkill(buf, how)" function + */ + void +f_term_setkill(typval_T *argvars UNUSED, typval_T *rettv UNUSED) +{ + buf_T *buf = term_get_buf(argvars, "term_setkill()"); + term_T *term; + char_u *how; + + if (buf == NULL) + return; + term = buf->b_term; + vim_free(term->tl_kill); + how = get_tv_string_chk(&argvars[1]); + if (how != NULL) + term->tl_kill = vim_strsave(how); + else + term->tl_kill = NULL; +} + +/* * "term_start(command, options)" function */ void @@ -4177,7 +4270,7 @@ f_term_start(typval_T *argvars, typval_T JO2_TERM_NAME + JO2_TERM_FINISH + JO2_HIDDEN + JO2_TERM_OPENCMD + JO2_TERM_COLS + JO2_TERM_ROWS + JO2_VERTICAL + JO2_CURWIN + JO2_CWD + JO2_ENV + JO2_EOF_CHARS - + JO2_NORESTORE) == FAIL) + + JO2_NORESTORE + JO2_TERM_KILL) == FAIL) return; if (opt.jo_vertical) @@ -4194,13 +4287,10 @@ f_term_start(typval_T *argvars, typval_T void f_term_wait(typval_T *argvars, typval_T *rettv UNUSED) { - buf_T *buf = term_get_buf(argvars); + buf_T *buf = term_get_buf(argvars, "term_wait()"); if (buf == NULL) - { - ch_log(NULL, "term_wait(): invalid argument"); return; - } if (buf->b_term->tl_job == NULL) { ch_log(NULL, "term_wait(): no job to wait for"); diff --git a/src/testdir/test_terminal.vim b/src/testdir/test_terminal.vim --- a/src/testdir/test_terminal.vim +++ b/src/testdir/test_terminal.vim @@ -5,6 +5,7 @@ if !has('terminal') endif source shared.vim +source screendump.vim let s:python = PythonProg() @@ -839,3 +840,48 @@ func Test_terminal_response_to_control_s call delete('Xescape') unlet g:job endfunc + +" Run Vim in a terminal, then start a terminal in that Vim with a kill +" argument, check that :qall works. +func Test_terminal_qall_kill_arg() + if !CanRunVimInTerminal() + return + endif + let buf = RunVimInTerminal('', {}) + + " Open a terminal window and wait for the prompt to appear + call term_sendkeys(buf, ":term ++kill=kill\") + call WaitFor({-> term_getline(buf, 10) =~ '\[running]'}) + call WaitFor({-> term_getline(buf, 1) !~ '^\s*$'}) + + " make Vim exit, it will kill the shell + call term_sendkeys(buf, "\:qall\") + call WaitFor({-> term_getstatus(buf) == "finished"}) + + " close the terminal window where Vim was running + quit +endfunc + +" Run Vim in a terminal, then start a terminal in that Vim with a kill +" argument, check that :qall works. +func Test_terminal_qall_kill_func() + if !CanRunVimInTerminal() + return + endif + let buf = RunVimInTerminal('', {}) + + " Open a terminal window and wait for the prompt to appear + call term_sendkeys(buf, ":term\") + call WaitFor({-> term_getline(buf, 10) =~ '\[running]'}) + call WaitFor({-> term_getline(buf, 1) !~ '^\s*$'}) + + " set kill using term_setkill() + call term_sendkeys(buf, "\:call term_setkill(bufnr('%'), 'kill')\") + + " make Vim exit, it will kill the shell + call term_sendkeys(buf, "\:qall\") + call WaitFor({-> term_getstatus(buf) == "finished"}) + + " close the terminal window where Vim was running + quit +endfunc diff --git a/src/version.c b/src/version.c --- a/src/version.c +++ b/src/version.c @@ -767,6 +767,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ /**/ + 1593, +/**/ 1592, /**/ 1591,