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

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