0
0
mirror of https://github.com/vim/vim.git synced 2025-09-27 04:14:06 -04:00

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.
This commit is contained in:
Bram Moolenaar
2017-07-30 16:52:24 +02:00
parent 68c4bdd53c
commit 423802d1a2
8 changed files with 351 additions and 137 deletions

View File

@@ -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 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
See option 'termkey' for specifying another key that precedes a Vim command.
Typing 'termkey' twice sends 'termkey' to the job.
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 only when in the first or second mode,
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

View File

@@ -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);
}
}
}

View File

@@ -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
/* <Insert> is equal to "i" */
if (cap->cmdchar == K_INS || cap->cmdchar == K_KINS)
cap->cmdchar = 'i';

View File

@@ -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)
{

View File

@@ -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, typval_T *rettv);
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 : */

View File

@@ -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;

View File

@@ -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> <close> 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;
@@ -552,6 +568,205 @@ term_job_running(term_T *term)
&& channel_is_open(term->tl_job->jv_channel);
}
/*
* 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.
@@ -640,6 +855,21 @@ send_keys_to_term(term_T *term, int c, int typed)
return OK;
}
/*
* 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
@@ -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 VTermScreenCell *cells, void *user)
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);
@@ -1346,6 +1477,18 @@ term_is_finished(buf_T *buf)
return buf->b_term != NULL && buf->b_term->tl_vterm == NULL;
}
/*
* 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");

View File

@@ -769,6 +769,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
/**/
813,
/**/
812,
/**/