# HG changeset patch # User Christian Brabandt # Date 1501426804 -7200 # Node ID be40c8a9240d0db1e54804483e6c8a7991258735 # Parent 6bdac11ae12a096466b4542ec605d6facc9e8b3b patch 8.0.0813: cannot use a terminal window while the job is running commit https://github.com/vim/vim/commit/423802d1a282df35078539970eabf559186e1ec8 Author: Bram Moolenaar Date: Sun Jul 30 16:52:24 2017 +0200 patch 8.0.0813: cannot use a terminal window while the job is running Problem: Cannot use Vim commands in a terminal window while the job is running. Solution: Implement Terminal Normal mode. 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: 2017 Jul 28 +*terminal.txt* For Vim version 8.0. Last change: 2017 Jul 30 VIM REFERENCE MANUAL by Bram Moolenaar @@ -33,24 +33,39 @@ Or to run a debugger: > The job runs asynchronously from Vim, the window will be updated to show output from the job, also while editing in any other window. + Typing ~ When the keyboard focus is in the terminal window, typed keys will be send to the job. This uses a pty when possible. You can click outside of the terminal window to move keyboard focus elsewhere. -Navigate between windows with CTRL-W commands. E.g. CTRL-W CTRL-W moves focus -to the next window. Use "CTRL-W :" to edit an Ex command. Use "CTRL-W ." to -send a CTRL-W to the job in the terminal. +CTRL-W can be used to navigate between windows and other CTRL-W commands, e.g.: + CTRL-W CTRL-W move focus to the next window + CTRL-W : enter an Ex command +See |CTRL-W| for more commands. + +Special in the terminal window: *CTRL-W_.* *CTRL-W_N* + CTRL-W . send a CTRL-W to the job in the terminal + CTRL-W N go to Terminal Normal mode, see |Terminal-mode| -See option 'termkey' for specifying another key that precedes a Vim command. -Typing 'termkey' twice sends 'termkey' to the job. +See option 'termkey' for specifying another key instead of CTRL-W that +will work like CTRL-W. However, typing 'termkey' twice sends 'termkey' to +the job. For example: + 'termkey' CTRL-W move focus to the next window + 'termkey' : enter an Ex command + 'termkey' 'termkey' send 'termkey' to the job in the terminal + 'termkey' . send a CTRL-W to the job in the terminal + 'termkey' N go to terminal Normal mode, see below + 'termkey' CTRL-N same as CTRL-W N + Size ~ See option 'termsize' for controlling the size of the terminal window. (TODO: scrolling when the terminal is larger than the window) + Syntax ~ :ter[minal] [command] *:ter* *:terminal* @@ -99,6 +114,25 @@ terminal. |term_setsize()| can be used not when 'termsize' is "rowsXcols". +Terminal Normal mode ~ + *Terminal-mode* +When the job is running the contents of the terminal is under control of the +job. That includes the cursor position. The terminal contents can change at +any time. + +Use CTRL-W N (or 'termkey' N) to go to Terminal Normal mode. Now the contents +of the terminal window is under control of Vim, the job output is suspended. + *E946* +In this mode you can move the cursor around with the usual Vim commands, +Visually mark text, yank text, etc. But you cannot change the contents of the +buffer. The commands that would start insert mode, such as 'i' and 'a', +return control of the window to the job. Any pending output will now be +displayed. + +In Terminal mode the statusline and window title show "(Terminal)". If the +job ends while in Terminal mode this changes to "(Terminal-finished)". + + Unix ~ On Unix a pty is used to make it possible to run all kinds of commands. You diff --git a/src/main.c b/src/main.c --- a/src/main.c +++ b/src/main.c @@ -1356,11 +1356,17 @@ main_loop( else { #ifdef FEAT_TERMINAL - if (curbuf->b_term != NULL && oa.op_type == OP_NOP - && oa.regname == NUL) - terminal_loop(); + if (term_use_loop() && oa.op_type == OP_NOP && oa.regname == NUL) + { + /* If terminal_loop() returns OK we got a key that is handled + * in Normal model. With FAIL the terminal was closed and the + * screen needs to be redrawn. */ + if (terminal_loop() == OK) + normal_cmd(&oa, TRUE); + } + else #endif - normal_cmd(&oa, TRUE); + normal_cmd(&oa, TRUE); } } } diff --git a/src/normal.c b/src/normal.c --- a/src/normal.c +++ b/src/normal.c @@ -9037,6 +9037,14 @@ nv_esc(cmdarg_T *cap) static void nv_edit(cmdarg_T *cap) { +#ifdef FEAT_TERMINAL + if (term_in_terminal_mode()) + { + term_leave_terminal_mode(); + return; + } +#endif + /* is equal to "i" */ if (cap->cmdchar == K_INS || cap->cmdchar == K_KINS) cap->cmdchar = 'i'; diff --git a/src/option.c b/src/option.c --- a/src/option.c +++ b/src/option.c @@ -8222,12 +8222,22 @@ set_bool_option( } #endif -#ifdef FEAT_TITLE /* when 'modifiable' is changed, redraw the window title */ else if ((int *)varp == &curbuf->b_p_ma) { +# ifdef FEAT_TERMINAL + /* Cannot set 'modifiable' when in Terminal mode. */ + if (term_in_terminal_mode()) + { + curbuf->b_p_ma = FALSE; + return (char_u *)N_("E946: Cannot make a terminal with running job modifiable"); + } +# endif +# ifdef FEAT_TITLE redraw_titles(); - } +# endif + } +#ifdef FEAT_TITLE /* when 'endofline' is changed, redraw the window title */ else if ((int *)varp == &curbuf->b_p_eol) { 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,15 @@ void ex_terminal(exarg_T *eap); void free_terminal(buf_T *buf); void write_to_term(buf_T *buffer, char_u *msg, channel_T *channel); +int term_in_terminal_mode(void); +void term_leave_terminal_mode(void); +int term_use_loop(void); int terminal_loop(void); void term_job_ended(job_T *job); void term_channel_closed(channel_T *ch); int term_update_window(win_T *wp); int term_is_finished(buf_T *buf); +int term_show_buffer(buf_T *buf); void term_change_in_curbuf(void); int term_get_attr(buf_T *buf, linenr_T lnum, int col); char_u *term_get_status_text(term_T *term); @@ -16,8 +20,8 @@ void f_term_getjob(typval_T *argvars, ty void f_term_getline(typval_T *argvars, typval_T *rettv); void f_term_getsize(typval_T *argvars, typval_T *rettv); void f_term_list(typval_T *argvars, typval_T *rettv); -void f_term_start(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_start(typval_T *argvars, typval_T *rettv); void f_term_wait(typval_T *argvars, typval_T *rettv); /* vim: set ft=c : */ diff --git a/src/screen.c b/src/screen.c --- a/src/screen.c +++ b/src/screen.c @@ -3245,7 +3245,7 @@ win_line( #endif #ifdef FEAT_TERMINAL - if (term_is_finished(wp->w_buffer)) + if (term_show_buffer(wp->w_buffer)) { extra_check = TRUE; get_term_attr = TRUE; diff --git a/src/terminal.c b/src/terminal.c --- a/src/terminal.c +++ b/src/terminal.c @@ -36,13 +36,23 @@ * that buffer, attributes come from the scrollback buffer tl_scrollback. * * TODO: + * - Problem with statusline (Zyx, Christian) + * - Make CTRL-W "" paste register content to the job? + * - in bash mouse clicks are inserting characters. + * - mouse scroll: when over other window, scroll that window. * - For the scrollback buffer store lines in the buffer, only attributes in * tl_scrollback. + * - Add term_status(): "" if not a terminal, "running" if job running, + * "finished" if finished, "running,vim" when job is running and in + * Terminal mode, "running,vim,pending" when job output is pending. * - When the job ends: * - Need an option or argument to drop the window+buffer right away, to be - * used for a shell or Vim. + * used for a shell or Vim. 'termfinish'; "close", "open" (open window when + * job finishes). + * - add option values to the command: + * :term <24x80> vim notes.txt * - To set BS correctly, check get_stty(); Pass the fd of the pty. - * - do not store terminal buffer in viminfo. Or prefix term:// ? + * - do not store terminal window in viminfo. Or prefix term:// ? * - add a character in :ls output * - when closing window and job has not ended, make terminal hidden? * - when closing window and job has ended, make buffer hidden? @@ -53,6 +63,8 @@ * - support minimal size when 'termsize' is empty? * - implement "term" for job_start(): more job options when starting a * terminal. + * - if the job in the terminal does not support the mouse, we can use the + * mouse in the Terminal window for copy/paste. * - when 'encoding' is not utf-8, or the job is using another encoding, setup * conversions. * - In the GUI use a terminal emulator for :!cmd. @@ -78,13 +90,17 @@ typedef struct sb_line_S { struct terminal_S { term_T *tl_next; + VTerm *tl_vterm; + job_T *tl_job; + buf_T *tl_buffer; + + int tl_terminal_mode; + int tl_channel_closed; + #ifdef WIN3264 void *tl_winpty_config; void *tl_winpty; #endif - VTerm *tl_vterm; - job_T *tl_job; - buf_T *tl_buffer; /* last known vterm size */ int tl_rows; @@ -553,6 +569,205 @@ term_job_running(term_T *term) } /* + * Add the last line of the scrollback buffer to the buffer in the window. + */ + static void +add_scrollback_line_to_buffer(term_T *term) +{ + linenr_T lnum = term->tl_scrollback.ga_len - 1; + sb_line_T *line = (sb_line_T *)term->tl_scrollback.ga_data + lnum; + garray_T ga; + int c; + int col; + int i; + + ga_init2(&ga, 1, 100); + for (col = 0; col < line->sb_cols; col += line->sb_cells[col].width) + { + if (ga_grow(&ga, MB_MAXBYTES) == FAIL) + goto failed; + for (i = 0; (c = line->sb_cells[col].chars[i]) > 0 || i == 0; ++i) + ga.ga_len += mb_char2bytes(c == NUL ? ' ' : c, + (char_u *)ga.ga_data + ga.ga_len); + } + if (ga_grow(&ga, 1) == FAIL) + goto failed; + *((char_u *)ga.ga_data + ga.ga_len) = NUL; + ml_append_buf(term->tl_buffer, lnum, ga.ga_data, ga.ga_len + 1, FALSE); + + if (lnum == 0) + { + /* Delete the empty line that was in the empty buffer. */ + curbuf = term->tl_buffer; + ml_delete(2, FALSE); + curbuf = curwin->w_buffer; + } + +failed: + ga_clear(&ga); +} + +/* + * Add the current lines of the terminal to scrollback and to the buffer. + * Called after the job has ended and when switching to Terminal mode. + */ + static void +move_terminal_to_buffer(term_T *term) +{ + win_T *wp; + int len; + int lines_skipped = 0; + VTermPos pos; + VTermScreenCell cell; + VTermScreenCell *p; + VTermScreen *screen = vterm_obtain_screen(term->tl_vterm); + + for (pos.row = 0; pos.row < term->tl_rows; ++pos.row) + { + len = 0; + for (pos.col = 0; pos.col < term->tl_cols; ++pos.col) + if (vterm_screen_get_cell(screen, pos, &cell) != 0 + && cell.chars[0] != NUL) + len = pos.col + 1; + + if (len == 0) + ++lines_skipped; + else + { + while (lines_skipped > 0) + { + /* Line was skipped, add an empty line. */ + --lines_skipped; + if (ga_grow(&term->tl_scrollback, 1) == OK) + { + sb_line_T *line = (sb_line_T *)term->tl_scrollback.ga_data + + term->tl_scrollback.ga_len; + + line->sb_cols = 0; + line->sb_cells = NULL; + ++term->tl_scrollback.ga_len; + + add_scrollback_line_to_buffer(term); + } + } + + p = (VTermScreenCell *)alloc((int)sizeof(VTermScreenCell) * len); + if (p != NULL && ga_grow(&term->tl_scrollback, 1) == OK) + { + sb_line_T *line = (sb_line_T *)term->tl_scrollback.ga_data + + term->tl_scrollback.ga_len; + + for (pos.col = 0; pos.col < len; ++pos.col) + { + if (vterm_screen_get_cell(screen, pos, &cell) == 0) + vim_memset(p + pos.col, 0, sizeof(cell)); + else + p[pos.col] = cell; + } + line->sb_cols = len; + line->sb_cells = p; + ++term->tl_scrollback.ga_len; + + add_scrollback_line_to_buffer(term); + } + else + vim_free(p); + } + } + + FOR_ALL_WINDOWS(wp) + { + if (wp->w_buffer == term->tl_buffer) + { + wp->w_cursor.lnum = term->tl_buffer->b_ml.ml_line_count; + wp->w_cursor.col = 0; + wp->w_valid = 0; + redraw_win_later(wp, NOT_VALID); + } + } +} + + static void +set_terminal_mode(term_T *term, int on) +{ + term->tl_terminal_mode = on; + vim_free(term->tl_status_text); + term->tl_status_text = NULL; + if (term->tl_buffer == curbuf) + maketitle(); +} + +/* + * Called after the job if finished and Terminal mode is not active: + * Move the vterm contents into the scrollback buffer and free the vterm. + */ + static void +cleanup_vterm(term_T *term) +{ + move_terminal_to_buffer(term); + term_free_vterm(term); + set_terminal_mode(term, FALSE); +} + +/* + * Switch from sending keys to the job to Terminal-Normal mode. + * Suspends updating the terminal window. + */ + static void +term_enter_terminal_mode() +{ + term_T *term = curbuf->b_term; + + /* Append the current terminal contents to the buffer. */ + move_terminal_to_buffer(term); + + set_terminal_mode(term, TRUE); +} + +/* + * Returns TRUE if the current window contains a terminal and we are in + * Terminal-Normal mode. + */ + int +term_in_terminal_mode() +{ + term_T *term = curbuf->b_term; + + return term != NULL && term->tl_terminal_mode; +} + +/* + * Switch from Terminal-Normal mode to sending keys to the job. + * Restores updating the terminal window. + */ + void +term_leave_terminal_mode() +{ + term_T *term = curbuf->b_term; + sb_line_T *line; + garray_T *gap; + + /* Remove the terminal contents from the scrollback and the buffer. */ + gap = &term->tl_scrollback; + while (curbuf->b_ml.ml_line_count > term->tl_scrollback_scrolled) + { + ml_delete(curbuf->b_ml.ml_line_count, FALSE); + line = (sb_line_T *)gap->ga_data + gap->ga_len - 1; + vim_free(line->sb_cells); + --gap->ga_len; + if (gap->ga_len == 0) + break; + } + check_cursor(); + + set_terminal_mode(term, FALSE); + + if (term->tl_channel_closed) + cleanup_vterm(term); + redraw_buf_and_status_later(curbuf, NOT_VALID); +} + +/* * Get a key from the user without mapping. * TODO: use terminal mode mappings. */ @@ -641,6 +856,21 @@ send_keys_to_term(term_T *term, int c, i } /* + * Returns TRUE if the current window contains a terminal and we are sending + * keys to the job. + */ + int +term_use_loop() +{ + term_T *term = curbuf->b_term; + + return term != NULL + && !term->tl_terminal_mode + && term->tl_vterm != NULL + && term_job_running(term); +} + +/* * Wait for input and send it to the job. * Return when the start of a CTRL-W command is typed or anything else that * should be handled as a Normal mode command. @@ -653,10 +883,6 @@ terminal_loop(void) int c; int termkey = 0; - if (curbuf->b_term->tl_vterm == NULL || !term_job_running(curbuf->b_term)) - /* job finished */ - return OK; - if (*curwin->w_p_tk != NUL) termkey = string_to_key(curwin->w_p_tk, TRUE); @@ -665,6 +891,7 @@ terminal_loop(void) /* TODO: skip screen update when handling a sequence of keys. */ update_screen(0); update_cursor(curbuf->b_term, FALSE); + c = term_vgetc(); if (curbuf->b_term->tl_vterm == NULL || !term_job_running(curbuf->b_term)) @@ -687,8 +914,15 @@ terminal_loop(void) break; if (termkey == 0 && c == '.') + { /* "CTRL-W .": send CTRL-W to the job */ c = Ctrl_W; + } + else if (termkey == 0 && c == 'N') + { + term_enter_terminal_mode(); + return FAIL; + } else if (termkey == 0 || c != termkey) { stuffcharReadbuff(Ctrl_W); @@ -704,6 +938,8 @@ terminal_loop(void) /* * Called when a job has finished. + * This updates the title and status, but does not close the vter, because + * there might still be pending output in the channel. */ void term_job_ended(job_T *job) @@ -891,120 +1127,12 @@ handle_pushline(int cols, const VTermScr line->sb_cells = p; ++term->tl_scrollback.ga_len; ++term->tl_scrollback_scrolled; + + add_scrollback_line_to_buffer(term); } return 0; /* ignored */ } -/* - * Fill the buffer with the scrollback lines and current lines of the terminal. - * Called after the job has ended. - */ - static void -move_scrollback_to_buffer(term_T *term) -{ - linenr_T lnum; - garray_T ga; - int c; - int col; - int i; - win_T *wp; - int len; - int lines_skipped = 0; - VTermPos pos; - VTermScreenCell cell; - VTermScreenCell *p; - VTermScreen *screen = vterm_obtain_screen(term->tl_vterm); - - /* Append the the visible lines to the scrollback. */ - for (pos.row = 0; pos.row < term->tl_rows; ++pos.row) - { - len = 0; - for (pos.col = 0; pos.col < term->tl_cols; ++pos.col) - if (vterm_screen_get_cell(screen, pos, &cell) != 0 - && cell.chars[0] != NUL) - len = pos.col + 1; - - if (len == 0) - ++lines_skipped; - else - { - while (lines_skipped > 0) - { - /* Line was skipped, add an empty line. */ - --lines_skipped; - if (ga_grow(&term->tl_scrollback, 1) == OK) - { - sb_line_T *line = (sb_line_T *)term->tl_scrollback.ga_data - + term->tl_scrollback.ga_len; - - line->sb_cols = 0; - line->sb_cells = NULL; - ++term->tl_scrollback.ga_len; - } - } - - p = (VTermScreenCell *)alloc((int)sizeof(VTermScreenCell) * len); - if (p != NULL && ga_grow(&term->tl_scrollback, 1) == OK) - { - sb_line_T *line = (sb_line_T *)term->tl_scrollback.ga_data - + term->tl_scrollback.ga_len; - - for (pos.col = 0; pos.col < len; ++pos.col) - { - if (vterm_screen_get_cell(screen, pos, &cell) == 0) - vim_memset(p + pos.col, 0, sizeof(cell)); - else - p[pos.col] = cell; - } - line->sb_cols = len; - line->sb_cells = p; - ++term->tl_scrollback.ga_len; - } - else - vim_free(p); - } - } - - /* Add the text to the buffer. */ - ga_init2(&ga, 1, 100); - for (lnum = 0; lnum < term->tl_scrollback.ga_len; ++lnum) - { - sb_line_T *line = (sb_line_T *)term->tl_scrollback.ga_data + lnum; - - ga.ga_len = 0; - for (col = 0; col < line->sb_cols; ++col) - { - if (ga_grow(&ga, MB_MAXBYTES) == FAIL) - goto failed; - for (i = 0; (c = line->sb_cells[col].chars[i]) > 0 || i == 0; ++i) - ga.ga_len += mb_char2bytes(c == NUL ? ' ' : c, - (char_u *)ga.ga_data + ga.ga_len); - } - if (ga_grow(&ga, 1) == FAIL) - goto failed; - *((char_u *)ga.ga_data + ga.ga_len) = NUL; - ml_append_buf(term->tl_buffer, lnum, ga.ga_data, ga.ga_len + 1, FALSE); - } - - /* Delete the empty line that was in the empty buffer. */ - curbuf = term->tl_buffer; - ml_delete(lnum + 1, FALSE); - curbuf = curwin->w_buffer; - -failed: - ga_clear(&ga); - - FOR_ALL_WINDOWS(wp) - { - if (wp->w_buffer == term->tl_buffer) - { - wp->w_cursor.lnum = term->tl_buffer->b_ml.ml_line_count; - wp->w_cursor.col = 0; - wp->w_valid = 0; - } - } -} - static VTermScreenCallbacks screen_callbacks = { handle_damage, /* damage */ handle_moverect, /* moverect */ @@ -1029,14 +1157,16 @@ term_channel_closed(channel_T *ch) for (term = first_term; term != NULL; term = term->tl_next) if (term->tl_job == ch->ch_job) { + term->tl_channel_closed = TRUE; + vim_free(term->tl_title); term->tl_title = NULL; vim_free(term->tl_status_text); term->tl_status_text = NULL; - /* move the lines into the buffer and free the vterm */ - move_scrollback_to_buffer(term); - term_free_vterm(term); + /* Unless in Terminal-Normal mode: clear the vterm. */ + if (!term->tl_terminal_mode) + cleanup_vterm(term); redraw_buf_and_status_later(term->tl_buffer, NOT_VALID); did_one = TRUE; @@ -1227,8 +1357,9 @@ term_update_window(win_T *wp) VTermState *state; VTermPos pos; - if (term == NULL || term->tl_vterm == NULL) + if (term == NULL || term->tl_vterm == NULL || term->tl_terminal_mode) return FAIL; + vterm = term->tl_vterm; screen = vterm_obtain_screen(vterm); state = vterm_obtain_state(vterm); @@ -1347,6 +1478,18 @@ term_is_finished(buf_T *buf) } /* + * Return TRUE if "wp" is a terminal window where the job has finished or we + * are in Terminal-Normal mode. + */ + int +term_show_buffer(buf_T *buf) +{ + term_T *term = buf->b_term; + + return term != NULL && (term->tl_vterm == NULL || term->tl_terminal_mode); +} + +/* * The current buffer is going to be changed. If there is terminal * highlighting remove it now. */ @@ -1450,7 +1593,14 @@ term_get_status_text(term_T *term) char_u *txt; size_t len; - if (term->tl_title != NULL) + if (term->tl_terminal_mode) + { + if (term_job_running(term)) + txt = (char_u *)_("Terminal"); + else + txt = (char_u *)_("Terminal-finished"); + } + else if (term->tl_title != NULL) txt = term->tl_title; else if (term_job_running(term)) txt = (char_u *)_("running"); diff --git a/src/version.c b/src/version.c --- a/src/version.c +++ b/src/version.c @@ -770,6 +770,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ /**/ + 813, +/**/ 812, /**/ 811,