Merge #6142 from justinmk/term-modifiable

terminal: 'modifiable'; 'scrollback'; follow output only if cursor is on last line
This commit is contained in:
Justin M. Keyes 2017-02-27 09:59:58 +01:00 committed by GitHub
commit e502cca010
20 changed files with 549 additions and 451 deletions

View File

@ -4,28 +4,19 @@
NVIM REFERENCE MANUAL by Thiago de Arruda NVIM REFERENCE MANUAL by Thiago de Arruda
Embedded terminal emulator *terminal-emulator* Terminal emulator *terminal-emulator*
1. Introduction |terminal-emulator-intro| Nvim embeds a VT220/xterm terminal emulator based on libvterm. The terminal is
2. Spawning |terminal-emulator-spawning| presented as a special buffer type, asynchronously updated from the virtual
3. Input |terminal-emulator-input| terminal as data is received from the program connected to it.
4. Configuration |terminal-emulator-configuration|
5. Status Variables |terminal-emulator-status| Terminal buffers behave mostly like normal 'nomodifiable' buffers, except:
- Plugins can set 'modifiable' to modify text, but lines cannot be deleted.
- 'scrollback' controls how many off-screen lines are kept.
- Terminal output is followed if the cursor is on the last line.
============================================================================== ==============================================================================
1. Introduction *terminal-emulator-intro* Spawning *terminal-emulator-spawning*
Nvim offers a mostly complete VT220/xterm terminal emulator. The terminal is
presented as a special buffer type, asynchronously updated to mirror the
virtual terminal display as data is received from the program connected to it.
For most purposes, terminal buffers behave a lot like normal buffers with
'nomodifiable' set.
The implementation is powered by libvterm, a powerful abstract terminal
emulation library. http://www.leonerd.org.uk/code/libvterm/
==============================================================================
2. Spawning *terminal-emulator-spawning*
There are 3 ways to create a terminal buffer: There are 3 ways to create a terminal buffer:
@ -40,34 +31,27 @@ There are 3 ways to create a terminal buffer:
Note: The "term://" pattern is handled by a BufReadCmd handler, so the Note: The "term://" pattern is handled by a BufReadCmd handler, so the
|autocmd-nested| modifier is required to use it in an autocmd. > |autocmd-nested| modifier is required to use it in an autocmd. >
autocmd VimEnter * nested split term://sh autocmd VimEnter * nested split term://sh
< This is only mentioned for reference; you should use the |:terminal| < This is only mentioned for reference; use |:terminal| instead.
command instead.
When the terminal spawns the program, the buffer will start to mirror the When the terminal spawns the program, the buffer will start to mirror the
terminal display and change its name to `term://$CWD//$PID:$COMMAND`. terminal display and change its name to `term://{cwd}//{pid}:{cmd}`.
Note that |:mksession| will "save" the terminal buffers by restarting all The "term://..." scheme enables |:mksession| to "restore" a terminal buffer by
programs when the session is restored. restarting the {cmd} when the session is loaded.
============================================================================== ==============================================================================
3. Input *terminal-emulator-input* Input *terminal-emulator-input*
Sending input is possible by entering terminal mode, which is achieved by To send input, enter terminal mode using any command that would enter "insert
pressing any key that would enter insert mode in a normal buffer (|i| or |a| mode" in a normal buffer, such as |i| or |:startinsert|. In this mode all keys
for example). The |:terminal| ex command will automatically enter terminal except <C-\><C-N> are sent to the underlying program. Use <C-\><C-N> to return
mode once it's spawned. While in terminal mode, Nvim will forward all keys to to normal mode. |CTRL-\_CTRL-N|
the underlying program. The only exception is the <C-\><C-n> key combo,
which will exit back to normal mode.
Terminal mode has its own namespace for mappings, which is accessed with the Terminal mode has its own |:tnoremap| namespace for mappings, this can be used
"t" prefix. It's possible to use terminal mappings to customize interaction to automate any terminal interaction. To map <Esc> to exit terminal mode: >
with the terminal. For example, here's how to map <Esc> to exit terminal mode:
>
:tnoremap <Esc> <C-\><C-n> :tnoremap <Esc> <C-\><C-n>
< <
Navigating to other windows is only possible by exiting to normal mode, which Navigating to other windows is only possible in normal mode. For convenience,
can be cumbersome with <C-\><C-n> keys. To improve the navigation experience, you could use these mappings: >
you could use the following mappings:
>
:tnoremap <A-h> <C-\><C-n><C-w>h :tnoremap <A-h> <C-\><C-n><C-w>h
:tnoremap <A-j> <C-\><C-n><C-w>j :tnoremap <A-j> <C-\><C-n><C-w>j
:tnoremap <A-k> <C-\><C-n><C-w>k :tnoremap <A-k> <C-\><C-n><C-w>k
@ -77,11 +61,9 @@ you could use the following mappings:
:nnoremap <A-k> <C-w>k :nnoremap <A-k> <C-w>k
:nnoremap <A-l> <C-w>l :nnoremap <A-l> <C-w>l
< <
This configuration allows using `Alt+{h,j,k,l}` to navigate between windows no Then you can use `Alt+{h,j,k,l}` to navigate between windows from any mode.
matter if they are displaying a normal buffer or a terminal buffer in terminal
mode.
Mouse input is also fully supported, and has the following behavior: Mouse input is supported, and has the following behavior:
- If the program has enabled mouse events, the corresponding events will be - If the program has enabled mouse events, the corresponding events will be
forwarded to the program. forwarded to the program.
@ -93,28 +75,23 @@ Mouse input is also fully supported, and has the following behavior:
the terminal wont lose focus and the hovered window will be scrolled. the terminal wont lose focus and the hovered window will be scrolled.
============================================================================== ==============================================================================
4. Configuration *terminal-emulator-configuration* Configuration *terminal-emulator-configuration*
Terminal buffers can be customized through the following global/buffer-local Options: 'scrollback'
variables (set via the |TermOpen| autocmd): Events: |TermOpen|, |TermClose|
Highlight groups: |hl-TermCursor|, |hl-TermCursorNC|
Terminal colors can be customized with these variables:
- `{g,b}:terminal_scrollback_buffer_size`: Scrollback buffer size, between 1
and 100000 inclusive. The default is 1000.
- `{g,b}:terminal_color_$NUM`: The terminal color palette, where `$NUM` is the - `{g,b}:terminal_color_$NUM`: The terminal color palette, where `$NUM` is the
color index, between 0 and 255 inclusive. This setting only affects UIs with color index, between 0 and 255 inclusive. This setting only affects UIs with
RGB capabilities; for normal terminals the color index is simply forwarded. RGB capabilities; for normal terminals the color index is simply forwarded.
The configuration variables are only processed when the terminal starts, which The `{g,b}:terminal_color_$NUM` variables are processed only when the terminal
is why it needs to be done with the |TermOpen| autocmd or setting global starts (after |TermOpen|).
variables before the terminal is started.
There is also a corresponding |TermClose| event.
The terminal cursor can be highlighted via |hl-TermCursor| and
|hl-TermCursorNC|.
============================================================================== ==============================================================================
5. Status Variables *terminal-emulator-status* Status Variables *terminal-emulator-status*
Terminal buffers maintain some information about the terminal in buffer-local Terminal buffers maintain some information about the terminal in buffer-local
variables: variables:
@ -127,11 +104,8 @@ variables:
- *b:terminal_job_pid* The PID of the top-level process running in the - *b:terminal_job_pid* The PID of the top-level process running in the
terminal. terminal.
These variables will have a value by the time the TermOpen autocmd runs, and These variables are initialized before TermOpen, so you can use them in
will continue to have a value for the lifetime of the terminal buffer, making a local 'statusline'. Example: >
them suitable for use in 'statusline'. For example, to show the terminal title
as the status line:
>
:autocmd TermOpen * setlocal statusline=%{b:term_title} :autocmd TermOpen * setlocal statusline=%{b:term_title}
< <
============================================================================== ==============================================================================

View File

@ -4949,6 +4949,16 @@ A jump table for the options with a short description can be found at |Q_op|.
be used as the new value for 'scroll'. Reset to half the window be used as the new value for 'scroll'. Reset to half the window
height with ":set scroll=0". height with ":set scroll=0".
*'scrollback'* *'scbk'*
'scrollback' 'scbk' number (default: 1000
in normal buffers: -1)
local to buffer
Maximum number of lines kept beyond the visible screen. Lines at the
top are deleted if new lines exceed this limit.
Only in |terminal-emulator| buffers. 'buftype'
-1 means "unlimited" for normal buffers, 100000 otherwise.
Minimum is 1.
*'scrollbind'* *'scb'* *'noscrollbind'* *'noscb'* *'scrollbind'* *'scb'* *'noscrollbind'* *'noscb'*
'scrollbind' 'scb' boolean (default off) 'scrollbind' 'scb' boolean (default off)
local to window local to window

View File

@ -207,23 +207,15 @@ g8 Print the hex values of the bytes used in the
:sh[ell] Removed. |vim-differences| {Nvim} :sh[ell] Removed. |vim-differences| {Nvim}
*:terminal* *:te* *:terminal* *:te*
:te[rminal][!] {cmd} Spawns {cmd} using the current value of 'shell' and :te[rminal][!] {cmd} Execute {cmd} with 'shell' in a |terminal-emulator|
'shellcmdflag' in a new terminal buffer. This is buffer. Equivalent to: >
equivalent to: >
:enew :enew
:call termopen('{cmd}') :call termopen('{cmd}')
:startinsert :startinsert
< <
If no {cmd} is given, 'shellcmdflag' will not be sent See |jobstart()|.
to |termopen()|.
Like |:enew|, it will fail if the current buffer is To enter terminal mode automatically: >
modified, but can be forced with "!". See |termopen()|
and |terminal-emulator|.
To switch to terminal mode automatically:
>
autocmd BufEnter term://* startinsert autocmd BufEnter term://* startinsert
< <
*:!cmd* *:!* *E34* *:!cmd* *:!* *E34*

View File

@ -10,7 +10,7 @@
Memcheck:Leak Memcheck:Leak
fun:malloc fun:malloc
fun:uv_spawn fun:uv_spawn
fun:pipe_process_spawn fun:libuv_process_spawn
fun:process_spawn fun:process_spawn
fun:job_start fun:job_start
} }

View File

@ -36,7 +36,7 @@ typedef struct {
// for Map(K, V) // for Map(K, V)
#include "nvim/map.h" #include "nvim/map.h"
#define MODIFIABLE(buf) (!buf->terminal && buf->b_p_ma) #define MODIFIABLE(buf) (buf->b_p_ma)
/* /*
* Flags for w_valid. * Flags for w_valid.
@ -91,32 +91,22 @@ typedef struct frame_S frame_T;
// for struct memline (it needs memfile_T) // for struct memline (it needs memfile_T)
#include "nvim/memline_defs.h" #include "nvim/memline_defs.h"
// for struct memfile, bhdr_T, blocknr_T... (it needs buf_T) // for struct memfile, bhdr_T, blocknr_T... (it needs buf_T)
#include "nvim/memfile_defs.h" #include "nvim/memfile_defs.h"
/* // for regprog_T. Needs win_T and buf_T.
* This is here because regexp_defs.h needs win_T and buf_T. regprog_T is
* used below.
*/
#include "nvim/regexp_defs.h" #include "nvim/regexp_defs.h"
// for synstate_T (needs reg_extmatch_T, win_T, buf_T)
// for synstate_T (needs reg_extmatch_T, win_T and buf_T)
#include "nvim/syntax_defs.h" #include "nvim/syntax_defs.h"
// for signlist_T // for signlist_T
#include "nvim/sign_defs.h" #include "nvim/sign_defs.h"
// for bufhl_*_T // for bufhl_*_T
#include "nvim/bufhl_defs.h" #include "nvim/bufhl_defs.h"
typedef Map(linenr_T, bufhl_vec_T) bufhl_info_T; typedef Map(linenr_T, bufhl_vec_T) bufhl_info_T;
// for FileID #include "nvim/os/fs_defs.h" // for FileID
#include "nvim/os/fs_defs.h" #include "nvim/terminal.h" // for Terminal
// for Terminal
#include "nvim/terminal.h"
/* /*
* The taggy struct is used to store the information about a :tag command. * The taggy struct is used to store the information about a :tag command.
@ -661,6 +651,7 @@ struct file_buffer {
char_u *b_p_qe; ///< 'quoteescape' char_u *b_p_qe; ///< 'quoteescape'
int b_p_ro; ///< 'readonly' int b_p_ro; ///< 'readonly'
long b_p_sw; ///< 'shiftwidth' long b_p_sw; ///< 'shiftwidth'
long b_p_scbk; ///< 'scrollback'
int b_p_si; ///< 'smartindent' int b_p_si; ///< 'smartindent'
long b_p_sts; ///< 'softtabstop' long b_p_sts; ///< 'softtabstop'
long b_p_sts_nopaste; ///< b_p_sts saved for paste mode long b_p_sts_nopaste; ///< b_p_sts saved for paste mode

View File

@ -5845,8 +5845,8 @@ bool garbage_collect(bool testing)
garbage_collect_at_exit = false; garbage_collect_at_exit = false;
} }
// We advance by two because we add one for items referenced through // We advance by two (COPYID_INC) because we add one for items referenced
// previous_funccal. // through previous_funccal.
const int copyID = get_copyID(); const int copyID = get_copyID();
// 1. Go through all accessible variables and mark all lists and dicts // 1. Go through all accessible variables and mark all lists and dicts

View File

@ -321,14 +321,15 @@ static void parse_msgpack(Stream *stream, RBuffer *rbuf, size_t c, void *data,
if (eof) { if (eof) {
close_channel(channel); close_channel(channel);
call_set_error(channel, "Channel was closed by the client"); char buf[256];
snprintf(buf, sizeof(buf), "channel %" PRIu64 " was closed by the client",
channel->id);
call_set_error(channel, buf);
goto end; goto end;
} }
size_t count = rbuffer_size(rbuf); size_t count = rbuffer_size(rbuf);
DLOG("Feeding the msgpack parser with %u bytes of data from Stream(%p)", DLOG("parsing %u bytes of msgpack data from Stream(%p)", count, stream);
count,
stream);
// Feed the unpacker with data // Feed the unpacker with data
msgpack_unpacker_reserve_buffer(channel->unpacker, count); msgpack_unpacker_reserve_buffer(channel->unpacker, count);
@ -350,11 +351,9 @@ static void parse_msgpack(Stream *stream, RBuffer *rbuf, size_t c, void *data,
complete_call(&unpacked.data, channel); complete_call(&unpacked.data, channel);
} else { } else {
char buf[256]; char buf[256];
snprintf(buf, snprintf(buf, sizeof(buf),
sizeof(buf), "channel %" PRIu64 " sent a response without a matching "
"Channel %" PRIu64 " returned a response that doesn't have " "request id. Ensure the client is properly synchronized",
"a matching request id. Ensure the client is properly "
"synchronized",
channel->id); channel->id);
call_set_error(channel, buf); call_set_error(channel, buf);
} }
@ -406,7 +405,7 @@ static void handle_request(Channel *channel, msgpack_object *request)
&out_buffer))) { &out_buffer))) {
char buf[256]; char buf[256];
snprintf(buf, sizeof(buf), snprintf(buf, sizeof(buf),
"Channel %" PRIu64 " sent an invalid message, closed.", "channel %" PRIu64 " sent an invalid message, closed.",
channel->id); channel->id);
call_set_error(channel, buf); call_set_error(channel, buf);
} }
@ -716,7 +715,7 @@ static void complete_call(msgpack_object *obj, Channel *channel)
static void call_set_error(Channel *channel, char *msg) static void call_set_error(Channel *channel, char *msg)
{ {
ELOG("msgpack-rpc: %s", msg); ELOG("RPC: %s", msg);
for (size_t i = 0; i < kv_size(channel->call_stack); i++) { for (size_t i = 0; i < kv_size(channel->call_stack); i++) {
ChannelCallFrame *frame = kv_A(channel->call_stack, i); ChannelCallFrame *frame = kv_A(channel->call_stack, i);
frame->returned = true; frame->returned = true;
@ -789,10 +788,10 @@ static void decref(Channel *channel)
} }
#if MIN_LOG_LEVEL <= DEBUG_LOG_LEVEL #if MIN_LOG_LEVEL <= DEBUG_LOG_LEVEL
#define REQ "[request] " #define REQ "[request] "
#define RES "[response] " #define RES "[response] "
#define NOT "[notification] " #define NOT "[notify] "
#define ERR "[error] " #define ERR "[error] "
// Cannot define array with negative offsets, so this one is needed to be added // Cannot define array with negative offsets, so this one is needed to be added
// to MSGPACK_UNPACK_\* values. // to MSGPACK_UNPACK_\* values.
@ -810,7 +809,7 @@ static void log_server_msg(uint64_t channel_id,
{ {
msgpack_unpacked unpacked; msgpack_unpacked unpacked;
msgpack_unpacked_init(&unpacked); msgpack_unpacked_init(&unpacked);
DLOGN("[msgpack-rpc] nvim -> client(%" PRIu64 ") ", channel_id); DLOGN("RPC ->ch %" PRIu64 ": ", channel_id);
const msgpack_unpack_return result = const msgpack_unpack_return result =
msgpack_unpack_next(&unpacked, packed->data, packed->size, NULL); msgpack_unpack_next(&unpacked, packed->data, packed->size, NULL);
switch (result) { switch (result) {
@ -847,7 +846,7 @@ static void log_client_msg(uint64_t channel_id,
bool is_request, bool is_request,
msgpack_object msg) msgpack_object msg)
{ {
DLOGN("[msgpack-rpc] client(%" PRIu64 ") -> nvim ", channel_id); DLOGN("RPC <-ch %" PRIu64 ": ", channel_id);
log_lock(); log_lock();
FILE *f = open_log_file(); FILE *f = open_log_file();
fprintf(f, is_request ? REQ : RES); fprintf(f, is_request ? REQ : RES);

View File

@ -7418,27 +7418,23 @@ static void nv_esc(cmdarg_T *cap)
restart_edit = 'a'; restart_edit = 'a';
} }
/* /// Handle "A", "a", "I", "i" and <Insert> commands.
* Handle "A", "a", "I", "i" and <Insert> commands.
*/
static void nv_edit(cmdarg_T *cap) static void nv_edit(cmdarg_T *cap)
{ {
/* <Insert> is equal to "i" */ // <Insert> is equal to "i"
if (cap->cmdchar == K_INS || cap->cmdchar == K_KINS) if (cap->cmdchar == K_INS || cap->cmdchar == K_KINS) {
cap->cmdchar = 'i'; cap->cmdchar = 'i';
}
/* in Visual mode "A" and "I" are an operator */ // in Visual mode "A" and "I" are an operator
if (VIsual_active && (cap->cmdchar == 'A' || cap->cmdchar == 'I')) if (VIsual_active && (cap->cmdchar == 'A' || cap->cmdchar == 'I')) {
v_visop(cap); v_visop(cap);
// in Visual mode and after an operator "a" and "i" are for text objects
/* in Visual mode and after an operator "a" and "i" are for text objects */ } else if ((cap->cmdchar == 'a' || cap->cmdchar == 'i')
else if ((cap->cmdchar == 'a' || cap->cmdchar == 'i') && (cap->oap->op_type != OP_NOP || VIsual_active)) {
&& (cap->oap->op_type != OP_NOP
|| VIsual_active
)) {
nv_object(cap); nv_object(cap);
} else if (!curbuf->b_p_ma && !p_im) { } else if (!curbuf->b_p_ma && !p_im && !curbuf->terminal) {
/* Only give this error when 'insertmode' is off. */ // Only give this error when 'insertmode' is off.
EMSG(_(e_modifiable)); EMSG(_(e_modifiable));
clearop(cap->oap); clearop(cap->oap);
} else if (!checkclearopq(cap->oap)) { } else if (!checkclearopq(cap->oap)) {

View File

@ -1,24 +1,18 @@
/* // User-settable options. Checklist for adding a new option:
* Code to handle user-settable options. This is all pretty much table- // - Put it in options.lua
* driven. Checklist for adding a new option: // - For a global option: Add a variable for it in option_defs.h.
* - Put it in the options array below (copy an existing entry). // - For a buffer or window local option:
* - For a global option: Add a variable for it in option_defs.h. // - Add a BV_XX or WV_XX entry to option_defs.h
* - For a buffer or window local option: // - Add a variable to the window or buffer struct in buffer_defs.h.
* - Add a PV_XX entry to the enum below. // - For a window option, add some code to copy_winopt().
* - Add a variable to the window or buffer struct in buffer_defs.h. // - For a buffer option, add some code to buf_copy_options().
* - For a window option, add some code to copy_winopt(). // - For a buffer string option, add code to check_buf_options().
* - For a buffer option, add some code to buf_copy_options(). // - If it's a numeric option, add any necessary bounds checks to do_set().
* - For a buffer string option, add code to check_buf_options(). // - If it's a list of flags, add some code in do_set(), search for WW_ALL.
* - If it's a numeric option, add any necessary bounds checks to do_set(). // - When adding an option with expansion (P_EXPAND), but with a different
* - If it's a list of flags, add some code in do_set(), search for WW_ALL. // default for Vi and Vim (no P_VI_DEF), add some code at VIMEXP.
* - When adding an option with expansion (P_EXPAND), but with a different // - Add documentation! doc/options.txt, and any other related places.
* default for Vi and Vim (no P_VI_DEF), add some code at VIMEXP. // - Add an entry in runtime/optwin.vim.
* - Add documentation! One line in doc/help.txt, full description in
* options.txt, and any other related places.
* - Add an entry in runtime/optwin.vim.
* When making changes:
* - Adjust the help for the option in doc/option.txt.
*/
#define IN_OPTION_C #define IN_OPTION_C
#include <assert.h> #include <assert.h>
@ -161,6 +155,7 @@ static long p_ts;
static long p_tw; static long p_tw;
static int p_udf; static int p_udf;
static long p_wm; static long p_wm;
static long p_scbk;
static char_u *p_keymap; static char_u *p_keymap;
/* Saved values for when 'bin' is set. */ /* Saved values for when 'bin' is set. */
@ -4201,7 +4196,19 @@ set_num_option (
FOR_ALL_TAB_WINDOWS(tp, wp) { FOR_ALL_TAB_WINDOWS(tp, wp) {
check_colorcolumn(wp); check_colorcolumn(wp);
} }
} else if (pp == &curbuf->b_p_scbk) {
// 'scrollback'
if (!curbuf->terminal) {
errmsg = e_invarg;
curbuf->b_p_scbk = -1;
} else {
if (curbuf->b_p_scbk < -1 || curbuf->b_p_scbk > 100000) {
errmsg = e_invarg;
curbuf->b_p_scbk = 1000;
}
// Force the scrollback to take effect.
terminal_resize(curbuf->terminal, UINT16_MAX, UINT16_MAX);
}
} }
/* /*
@ -5426,6 +5433,7 @@ static char_u *get_varp(vimoption_T *p)
case PV_PI: return (char_u *)&(curbuf->b_p_pi); case PV_PI: return (char_u *)&(curbuf->b_p_pi);
case PV_QE: return (char_u *)&(curbuf->b_p_qe); case PV_QE: return (char_u *)&(curbuf->b_p_qe);
case PV_RO: return (char_u *)&(curbuf->b_p_ro); case PV_RO: return (char_u *)&(curbuf->b_p_ro);
case PV_SCBK: return (char_u *)&(curbuf->b_p_scbk);
case PV_SI: return (char_u *)&(curbuf->b_p_si); case PV_SI: return (char_u *)&(curbuf->b_p_si);
case PV_STS: return (char_u *)&(curbuf->b_p_sts); case PV_STS: return (char_u *)&(curbuf->b_p_sts);
case PV_SUA: return (char_u *)&(curbuf->b_p_sua); case PV_SUA: return (char_u *)&(curbuf->b_p_sua);
@ -5636,6 +5644,7 @@ void buf_copy_options(buf_T *buf, int flags)
buf->b_p_ai = p_ai; buf->b_p_ai = p_ai;
buf->b_p_ai_nopaste = p_ai_nopaste; buf->b_p_ai_nopaste = p_ai_nopaste;
buf->b_p_sw = p_sw; buf->b_p_sw = p_sw;
buf->b_p_scbk = -1;
buf->b_p_tw = p_tw; buf->b_p_tw = p_tw;
buf->b_p_tw_nopaste = p_tw_nopaste; buf->b_p_tw_nopaste = p_tw_nopaste;
buf->b_p_tw_nobin = p_tw_nobin; buf->b_p_tw_nobin = p_tw_nobin;

View File

@ -739,6 +739,7 @@ enum {
, BV_PI , BV_PI
, BV_QE , BV_QE
, BV_RO , BV_RO
, BV_SCBK
, BV_SI , BV_SI
, BV_SMC , BV_SMC
, BV_SYN , BV_SYN

View File

@ -1912,6 +1912,14 @@ return {
pv_name='p_scroll', pv_name='p_scroll',
defaults={if_true={vi=12}} defaults={if_true={vi=12}}
}, },
{
full_name='scrollback', abbreviation='scbk',
type='number', scope={'buffer'},
vi_def=true,
varname='p_scbk',
redraw={'current_buffer'},
defaults={if_true={vi=-1}}
},
{ {
full_name='scrollbind', abbreviation='scb', full_name='scrollbind', abbreviation='scb',
type='bool', scope={'window'}, type='bool', scope={'window'},

View File

@ -7113,8 +7113,9 @@ void showruler(int always)
} }
if ((*p_stl != NUL || *curwin->w_p_stl != NUL) && curwin->w_status_height) { if ((*p_stl != NUL || *curwin->w_p_stl != NUL) && curwin->w_status_height) {
redraw_custom_statusline(curwin); redraw_custom_statusline(curwin);
} else } else {
win_redr_ruler(curwin, always); win_redr_ruler(curwin, always);
}
if (need_maketitle if (need_maketitle
|| (p_icon && (stl_syntax & STL_IN_ICON)) || (p_icon && (stl_syntax & STL_IN_ICON))

View File

@ -1,18 +1,17 @@
// VT220/xterm-like terminal emulator implementation for nvim. Powered by // VT220/xterm-like terminal emulator.
// libvterm (http://www.leonerd.org.uk/code/libvterm/). // Powered by libvterm http://www.leonerd.org.uk/code/libvterm
// //
// libvterm is a pure C99 terminal emulation library with abstract input and // libvterm is a pure C99 terminal emulation library with abstract input and
// display. This means that the library needs to read data from the master fd // display. This means that the library needs to read data from the master fd
// and feed VTerm instances, which will invoke user callbacks with screen // and feed VTerm instances, which will invoke user callbacks with screen
// update instructions that must be mirrored to the real display. // update instructions that must be mirrored to the real display.
// //
// Keys are pressed in VTerm instances by calling // Keys are sent to VTerm instances by calling
// vterm_keyboard_key/vterm_keyboard_unichar, which generates byte streams that // vterm_keyboard_key/vterm_keyboard_unichar, which generates byte streams that
// must be fed back to the master fd. // must be fed back to the master fd.
// //
// This implementation uses nvim buffers as the display mechanism for both // Nvim buffers are used as the display mechanism for both the visible screen
// the visible screen and the scrollback buffer. When focused, the window // and the scrollback buffer.
// "pins" to the bottom of the buffer and mirrors libvterm screen state.
// //
// When a line becomes invisible due to a decrease in screen height or because // When a line becomes invisible due to a decrease in screen height or because
// a line was pushed up during normal terminal output, we store the line // a line was pushed up during normal terminal output, we store the line
@ -23,18 +22,17 @@
// scrollback buffer, which is mirrored in the nvim buffer displaying lines // scrollback buffer, which is mirrored in the nvim buffer displaying lines
// that were previously invisible. // that were previously invisible.
// //
// The vterm->nvim synchronization is performed in intervals of 10 // The vterm->nvim synchronization is performed in intervals of 10 milliseconds,
// milliseconds. This is done to minimize screen updates when receiving large // to minimize screen updates when receiving large bursts of data.
// bursts of data.
// //
// This module is decoupled from the processes that normally feed it data, so // This module is decoupled from the processes that normally feed it data, so
// it's possible to use it as a general purpose console buffer (possibly as a // it's possible to use it as a general purpose console buffer (possibly as a
// log/display mechanism for nvim in the future) // log/display mechanism for nvim in the future)
// //
// Inspired by vimshell (http://www.wana.at/vimshell/) and // Inspired by: vimshell http://www.wana.at/vimshell
// Conque (https://code.google.com/p/conque/). Libvterm usage instructions (plus // Conque https://code.google.com/p/conque
// some extra code) were taken from // Some code from pangoterm http://www.leonerd.org.uk/code/pangoterm
// pangoterm (http://www.leonerd.org.uk/code/pangoterm/)
#include <assert.h> #include <assert.h>
#include <stdio.h> #include <stdio.h>
#include <stdint.h> #include <stdint.h>
@ -87,10 +85,10 @@ typedef struct terminal_state {
# include "terminal.c.generated.h" # include "terminal.c.generated.h"
#endif #endif
#define SCROLLBACK_BUFFER_DEFAULT_SIZE 1000 #define SB_MAX 100000 // Maximum 'scrollback' value.
// Delay for refreshing the terminal buffer after receiving updates from // Delay for refreshing the terminal buffer after receiving updates from
// libvterm. This is greatly improves performance when receiving large bursts // libvterm. Improves performance when receiving large bursts of data.
// of data.
#define REFRESH_DELAY 10 #define REFRESH_DELAY 10
static TimeWatcher refresh_timer; static TimeWatcher refresh_timer;
@ -102,27 +100,23 @@ typedef struct {
} ScrollbackLine; } ScrollbackLine;
struct terminal { struct terminal {
// options passed to terminal_open TerminalOptions opts; // options passed to terminal_open
TerminalOptions opts;
// libvterm structures
VTerm *vt; VTerm *vt;
VTermScreen *vts; VTermScreen *vts;
// buffer used to: // buffer used to:
// - convert VTermScreen cell arrays into utf8 strings // - convert VTermScreen cell arrays into utf8 strings
// - receive data from libvterm as a result of key presses. // - receive data from libvterm as a result of key presses.
char textbuf[0x1fff]; char textbuf[0x1fff];
// Scrollback buffer storage for libvterm.
// TODO(tarruda): Use a doubly-linked list ScrollbackLine **sb_buffer; // Scrollback buffer storage for libvterm
ScrollbackLine **sb_buffer; size_t sb_current; // number of rows pushed to sb_buffer
// number of rows pushed to sb_buffer size_t sb_size; // sb_buffer size
size_t sb_current;
// sb_buffer size;
size_t sb_size;
// "virtual index" that points to the first sb_buffer row that we need to // "virtual index" that points to the first sb_buffer row that we need to
// push to the terminal buffer when refreshing the scrollback. When negative, // push to the terminal buffer when refreshing the scrollback. When negative,
// it actually points to entries that are no longer in sb_buffer (because the // it actually points to entries that are no longer in sb_buffer (because the
// window height has increased) and must be deleted from the terminal buffer // window height has increased) and must be deleted from the terminal buffer
int sb_pending; int sb_pending;
// buf_T instance that acts as a "drawing surface" for libvterm // buf_T instance that acts as a "drawing surface" for libvterm
// we can't store a direct reference to the buffer because the // we can't store a direct reference to the buffer because the
// refresh_timer_cb may be called after the buffer was freed, and there's // refresh_timer_cb may be called after the buffer was freed, and there's
@ -130,20 +124,18 @@ struct terminal {
handle_T buf_handle; handle_T buf_handle;
// program exited // program exited
bool closed, destroy; bool closed, destroy;
// some vterm properties // some vterm properties
bool forward_mouse; bool forward_mouse;
// invalid rows libvterm screen int invalid_start, invalid_end; // invalid rows in libvterm screen
int invalid_start, invalid_end;
struct { struct {
int row, col; int row, col;
bool visible; bool visible;
} cursor; } cursor;
// which mouse button is pressed int pressed_button; // which mouse button is pressed
int pressed_button; bool pending_resize; // pending width/height
// pending width/height
bool pending_resize; size_t refcount; // reference count
// With a reference count of 0 the terminal can be freed.
size_t refcount;
}; };
static VTermScreenCallbacks vterm_screen_callbacks = { static VTermScreenCallbacks vterm_screen_callbacks = {
@ -237,25 +229,22 @@ Terminal *terminal_open(TerminalOptions opts)
rv->invalid_end = opts.height; rv->invalid_end = opts.height;
refresh_screen(rv, curbuf); refresh_screen(rv, curbuf);
set_option_value((uint8_t *)"buftype", 0, (uint8_t *)"terminal", OPT_LOCAL); set_option_value((uint8_t *)"buftype", 0, (uint8_t *)"terminal", OPT_LOCAL);
// some sane settings for terminal buffers
// Default settings for terminal buffers
curbuf->b_p_ma = false; // 'nomodifiable'
curbuf->b_p_ul = -1; // disable undo
curbuf->b_p_scbk = 1000; // 'scrollback'
set_option_value((uint8_t *)"wrap", false, NULL, OPT_LOCAL); set_option_value((uint8_t *)"wrap", false, NULL, OPT_LOCAL);
set_option_value((uint8_t *)"number", false, NULL, OPT_LOCAL); set_option_value((uint8_t *)"number", false, NULL, OPT_LOCAL);
set_option_value((uint8_t *)"relativenumber", false, NULL, OPT_LOCAL); set_option_value((uint8_t *)"relativenumber", false, NULL, OPT_LOCAL);
buf_set_term_title(curbuf, (char *)curbuf->b_ffname); buf_set_term_title(curbuf, (char *)curbuf->b_ffname);
RESET_BINDING(curwin); RESET_BINDING(curwin);
// Apply TermOpen autocmds so the user can configure the terminal
// Apply TermOpen autocmds _before_ configuring the scrollback buffer.
apply_autocmds(EVENT_TERMOPEN, NULL, NULL, false, curbuf); apply_autocmds(EVENT_TERMOPEN, NULL, NULL, false, curbuf);
// Configure the scrollback buffer. Try to get the size from: // Configure the scrollback buffer.
// rv->sb_size = curbuf->b_p_scbk < 0 ? SB_MAX : (size_t)curbuf->b_p_scbk;;
// - b:terminal_scrollback_buffer_size
// - g:terminal_scrollback_buffer_size
// - SCROLLBACK_BUFFER_DEFAULT_SIZE
//
// but limit to 100k.
int size = get_config_int("terminal_scrollback_buffer_size");
rv->sb_size = size > 0 ? (size_t)size : SCROLLBACK_BUFFER_DEFAULT_SIZE;
rv->sb_size = MIN(rv->sb_size, 100000);
rv->sb_buffer = xmalloc(sizeof(ScrollbackLine *) * rv->sb_size); rv->sb_buffer = xmalloc(sizeof(ScrollbackLine *) * rv->sb_size);
if (!true_color) { if (!true_color) {
@ -334,22 +323,22 @@ void terminal_close(Terminal *term, char *msg)
void terminal_resize(Terminal *term, uint16_t width, uint16_t height) void terminal_resize(Terminal *term, uint16_t width, uint16_t height)
{ {
if (term->closed) { if (term->closed) {
// will be called after exited if two windows display the same terminal and // If two windows display the same terminal and one is closed by keypress.
// one of the is closed as a consequence of pressing a key.
return; return;
} }
bool force = width == UINT16_MAX || height == UINT16_MAX;
int curwidth, curheight; int curwidth, curheight;
vterm_get_size(term->vt, &curheight, &curwidth); vterm_get_size(term->vt, &curheight, &curwidth);
if (!width) { if (force || !width) {
width = (uint16_t)curwidth; width = (uint16_t)curwidth;
} }
if (!height) { if (force || !height) {
height = (uint16_t)curheight; height = (uint16_t)curheight;
} }
if (curheight == height && curwidth == width) { if (!force && curheight == height && curwidth == width) {
return; return;
} }
@ -381,8 +370,7 @@ void terminal_enter(void)
State = TERM_FOCUS; State = TERM_FOCUS;
mapped_ctrl_c |= TERM_FOCUS; // Always map CTRL-C to avoid interrupt. mapped_ctrl_c |= TERM_FOCUS; // Always map CTRL-C to avoid interrupt.
RedrawingDisabled = false; RedrawingDisabled = false;
// go to the bottom when the terminal is focused adjust_topline(s->term, buf, 0); // scroll to end
adjust_topline(s->term, buf, false);
// erase the unfocused cursor // erase the unfocused cursor
invalidate_terminal(s->term, s->term->cursor.row, s->term->cursor.row + 1); invalidate_terminal(s->term, s->term->cursor.row, s->term->cursor.row + 1);
showmode(); showmode();
@ -667,10 +655,15 @@ static int term_bell(void *data)
return 1; return 1;
} }
// the scrollback push/pop handlers were copied almost verbatim from pangoterm // Scrollback push handler (from pangoterm).
static int term_sb_push(int cols, const VTermScreenCell *cells, void *data) static int term_sb_push(int cols, const VTermScreenCell *cells, void *data)
{ {
Terminal *term = data; Terminal *term = data;
if (!term->sb_size) {
return 0;
}
// copy vterm cells into sb_buffer // copy vterm cells into sb_buffer
size_t c = (size_t)cols; size_t c = (size_t)cols;
ScrollbackLine *sbrow = NULL; ScrollbackLine *sbrow = NULL;
@ -682,10 +675,12 @@ static int term_sb_push(int cols, const VTermScreenCell *cells, void *data)
xfree(term->sb_buffer[term->sb_current - 1]); xfree(term->sb_buffer[term->sb_current - 1]);
} }
// Make room at the start by shifting to the right.
memmove(term->sb_buffer + 1, term->sb_buffer, memmove(term->sb_buffer + 1, term->sb_buffer,
sizeof(term->sb_buffer[0]) * (term->sb_current - 1)); sizeof(term->sb_buffer[0]) * (term->sb_current - 1));
} else if (term->sb_current > 0) { } else if (term->sb_current > 0) {
// Make room at the start by shifting to the right.
memmove(term->sb_buffer + 1, term->sb_buffer, memmove(term->sb_buffer + 1, term->sb_buffer,
sizeof(term->sb_buffer[0]) * term->sb_current); sizeof(term->sb_buffer[0]) * term->sb_current);
} }
@ -695,6 +690,7 @@ static int term_sb_push(int cols, const VTermScreenCell *cells, void *data)
sbrow->cols = c; sbrow->cols = c;
} }
// New row is added at the start of the storage buffer.
term->sb_buffer[0] = sbrow; term->sb_buffer[0] = sbrow;
if (term->sb_current < term->sb_size) { if (term->sb_current < term->sb_size) {
term->sb_current++; term->sb_current++;
@ -710,6 +706,11 @@ static int term_sb_push(int cols, const VTermScreenCell *cells, void *data)
return 1; return 1;
} }
/// Scrollback pop handler (from pangoterm).
///
/// @param cols
/// @param cells VTerm state to update.
/// @param data Terminal
static int term_sb_pop(int cols, VTermScreenCell *cells, void *data) static int term_sb_pop(int cols, VTermScreenCell *cells, void *data)
{ {
Terminal *term = data; Terminal *term = data;
@ -722,24 +723,24 @@ static int term_sb_pop(int cols, VTermScreenCell *cells, void *data)
term->sb_pending--; term->sb_pending--;
} }
// restore vterm state
size_t c = (size_t)cols;
ScrollbackLine *sbrow = term->sb_buffer[0]; ScrollbackLine *sbrow = term->sb_buffer[0];
term->sb_current--; term->sb_current--;
// Forget the "popped" row by shifting the rest onto it.
memmove(term->sb_buffer, term->sb_buffer + 1, memmove(term->sb_buffer, term->sb_buffer + 1,
sizeof(term->sb_buffer[0]) * (term->sb_current)); sizeof(term->sb_buffer[0]) * (term->sb_current));
size_t cols_to_copy = c; size_t cols_to_copy = (size_t)cols;
if (cols_to_copy > sbrow->cols) { if (cols_to_copy > sbrow->cols) {
cols_to_copy = sbrow->cols; cols_to_copy = sbrow->cols;
} }
// copy to vterm state // copy to vterm state
memcpy(cells, sbrow->cells, sizeof(cells[0]) * cols_to_copy); memcpy(cells, sbrow->cells, sizeof(cells[0]) * cols_to_copy);
for (size_t col = cols_to_copy; col < c; col++) { for (size_t col = cols_to_copy; col < (size_t)cols; col++) {
cells[col].chars[0] = 0; cells[col].chars[0] = 0;
cells[col].width = 1; cells[col].width = 1;
} }
xfree(sbrow); xfree(sbrow);
pmap_put(ptr_t)(invalidated_terminals, term, NULL); pmap_put(ptr_t)(invalidated_terminals, term, NULL);
@ -885,7 +886,7 @@ static bool send_mouse_event(Terminal *term, int c)
// terminal buffer refresh & misc {{{ // terminal buffer refresh & misc {{{
void fetch_row(Terminal *term, int row, int end_col) static void fetch_row(Terminal *term, int row, int end_col)
{ {
int col = 0; int col = 0;
size_t line_len = 0; size_t line_len = 0;
@ -958,23 +959,23 @@ static void refresh_terminal(Terminal *term)
buf_T *buf = handle_get_buffer(term->buf_handle); buf_T *buf = handle_get_buffer(term->buf_handle);
bool valid = true; bool valid = true;
if (!buf || !(valid = buf_valid(buf))) { if (!buf || !(valid = buf_valid(buf))) {
// destroyed by `close_buffer`. Dont do anything else // Destroyed by `close_buffer`. Do not do anything else.
if (!valid) { if (!valid) {
term->buf_handle = 0; term->buf_handle = 0;
} }
return; return;
} }
bool pending_resize = term->pending_resize; long ml_before = buf->b_ml.ml_line_count;
WITH_BUFFER(buf, { WITH_BUFFER(buf, {
refresh_size(term, buf); refresh_size(term, buf);
refresh_scrollback(term, buf); refresh_scrollback(term, buf);
refresh_screen(term, buf); refresh_screen(term, buf);
redraw_buf_later(buf, NOT_VALID); redraw_buf_later(buf, NOT_VALID);
}); });
adjust_topline(term, buf, pending_resize); long ml_added = buf->b_ml.ml_line_count - ml_before;
adjust_topline(term, buf, ml_added);
} }
// libuv timer callback. This will enqueue on_refresh to be processed as an // Calls refresh_terminal() on all invalidated_terminals.
// event.
static void refresh_timer_cb(TimeWatcher *watcher, void *data) static void refresh_timer_cb(TimeWatcher *watcher, void *data)
{ {
if (exiting) { // Cannot redraw (requires event loop) during teardown/exit. if (exiting) { // Cannot redraw (requires event loop) during teardown/exit.
@ -1008,7 +1009,37 @@ static void refresh_size(Terminal *term, buf_T *buf)
term->opts.resize_cb((uint16_t)width, (uint16_t)height, term->opts.data); term->opts.resize_cb((uint16_t)width, (uint16_t)height, term->opts.data);
} }
// Refresh the scrollback of a invalidated terminal /// Adjusts scrollback storage after 'scrollback' option changed.
static void on_scrollback_option_changed(Terminal *term, buf_T *buf)
{
const size_t scbk = curbuf->b_p_scbk < 0
? SB_MAX : (size_t)MAX(1, curbuf->b_p_scbk);
assert(term->sb_current < SIZE_MAX);
if (term->sb_pending > 0) { // Pending rows must be processed first.
abort();
}
// Delete lines exceeding the new 'scrollback' limit.
if (scbk < term->sb_current) {
size_t diff = term->sb_current - scbk;
for (size_t i = 0; i < diff; i++) {
ml_delete(1, false);
term->sb_current--;
xfree(term->sb_buffer[term->sb_current]);
}
deleted_lines(1, (long)diff);
}
// Resize the scrollback storage.
size_t sb_region = sizeof(ScrollbackLine *) * scbk;
if (scbk != term->sb_size) {
term->sb_buffer = xrealloc(term->sb_buffer, sb_region);
}
term->sb_size = scbk;
}
// Refresh the scrollback of an invalidated terminal.
static void refresh_scrollback(Terminal *term, buf_T *buf) static void refresh_scrollback(Terminal *term, buf_T *buf)
{ {
int width, height; int width, height;
@ -1037,9 +1068,11 @@ static void refresh_scrollback(Terminal *term, buf_T *buf)
ml_delete(buf->b_ml.ml_line_count, false); ml_delete(buf->b_ml.ml_line_count, false);
deleted_lines(buf->b_ml.ml_line_count, 1); deleted_lines(buf->b_ml.ml_line_count, 1);
} }
on_scrollback_option_changed(term, buf);
} }
// Refresh the screen(visible part of the buffer when the terminal is // Refresh the screen (visible part of the buffer when the terminal is
// focused) of a invalidated terminal // focused) of a invalidated terminal
static void refresh_screen(Terminal *term, buf_T *buf) static void refresh_screen(Terminal *term, buf_T *buf)
{ {
@ -1048,8 +1081,7 @@ static void refresh_screen(Terminal *term, buf_T *buf)
int height; int height;
int width; int width;
vterm_get_size(term->vt, &height, &width); vterm_get_size(term->vt, &height, &width);
// It's possible that the terminal height decreased and `term->invalid_end` // Terminal height may have decreased before `invalid_end` reflects it.
// doesn't reflect it yet
term->invalid_end = MIN(term->invalid_end, height); term->invalid_end = MIN(term->invalid_end, height);
for (int r = term->invalid_start, linenr = row_to_linenr(term, r); for (int r = term->invalid_start, linenr = row_to_linenr(term, r);
@ -1094,14 +1126,6 @@ static void redraw(bool restore_cursor)
update_screen(0); update_screen(0);
} }
redraw_statuslines();
if (need_maketitle) {
maketitle();
}
showruler(false);
if (term && is_focused(term)) { if (term && is_focused(term)) {
curwin->w_wrow = term->cursor.row; curwin->w_wrow = term->cursor.row;
curwin->w_wcol = term->cursor.col + win_col_off(curwin); curwin->w_wcol = term->cursor.col + win_col_off(curwin);
@ -1121,21 +1145,21 @@ static void redraw(bool restore_cursor)
ui_flush(); ui_flush();
} }
static void adjust_topline(Terminal *term, buf_T *buf, bool force) static void adjust_topline(Terminal *term, buf_T *buf, long added)
{ {
int height, width; int height, width;
vterm_get_size(term->vt, &height, &width); vterm_get_size(term->vt, &height, &width);
FOR_ALL_WINDOWS_IN_TAB(wp, curtab) { FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
if (wp->w_buffer == buf) { if (wp->w_buffer == buf) {
// for every window that displays a terminal, ensure the cursor is in a linenr_T ml_end = buf->b_ml.ml_line_count;
// valid line bool following = ml_end == wp->w_cursor.lnum + added; // cursor at end?
wp->w_cursor.lnum = MIN(wp->w_cursor.lnum, buf->b_ml.ml_line_count); if (following || (wp == curwin && is_focused(term))) {
if (force || curbuf != buf || is_focused(term)) { // "Follow" the terminal output
// if the terminal is not in the current window or if it's focused, wp->w_cursor.lnum = ml_end;
// adjust topline/cursor so the window will "follow" the terminal
// output
wp->w_cursor.lnum = buf->b_ml.ml_line_count;
set_topline(wp, MAX(wp->w_cursor.lnum - height + 1, 1)); set_topline(wp, MAX(wp->w_cursor.lnum - height + 1, 1));
} else {
// Ensure valid cursor for each window displaying this terminal.
wp->w_cursor.lnum = MIN(wp->w_cursor.lnum, ml_end);
} }
} }
} }
@ -1178,17 +1202,6 @@ static char *get_config_string(char *key)
return NULL; return NULL;
} }
static int get_config_int(char *key)
{
Object obj;
GET_CONFIG_VALUE(key, obj);
if (obj.type == kObjectTypeInteger) {
return (int)obj.data.integer;
}
api_free_object(obj);
return 0;
}
// }}} // }}}
// vim: foldmethod=marker // vim: foldmethod=marker

View File

@ -305,16 +305,12 @@ bool undo_allowed(void)
return true; return true;
} }
/* /// Get the 'undolevels' value for the current buffer.
* Get the undolevle value for the current buffer.
*/
static long get_undolevel(void) static long get_undolevel(void)
{ {
if (curbuf->terminal) { if (curbuf->b_p_ul == NO_LOCAL_UNDOLEVEL) {
return -1;
}
if (curbuf->b_p_ul == NO_LOCAL_UNDOLEVEL)
return p_ul; return p_ul;
}
return curbuf->b_p_ul; return curbuf->b_p_ul;
} }

View File

@ -31,45 +31,41 @@ describe(':edit term://*', function()
eq(termopen_runs[1], termopen_runs[1]:match('^term://.//%d+:$')) eq(termopen_runs[1], termopen_runs[1]:match('^term://.//%d+:$'))
end) end)
it('runs TermOpen early enough to respect terminal_scrollback_buffer_size', function() it("runs TermOpen early enough to set buffer-local 'scrollback'", function()
local columns, lines = 20, 4 local columns, lines = 20, 4
local scr = get_screen(columns, lines) local scr = get_screen(columns, lines)
local rep = 'a' local rep = 'a'
meths.set_option('shellcmdflag', 'REP ' .. rep) meths.set_option('shellcmdflag', 'REP ' .. rep)
local rep_size = rep:byte() local rep_size = rep:byte() -- 'a' => 97
local sb = 10 local sb = 10
local gsb = 20 command('autocmd TermOpen * :setlocal scrollback='..tostring(sb)
meths.set_var('terminal_scrollback_buffer_size', gsb) ..'|call feedkeys("G", "n")')
command('autocmd TermOpen * :let b:terminal_scrollback_buffer_size = '
.. tostring(sb))
command('edit term://foobar') command('edit term://foobar')
local bufcontents = {} local bufcontents = {}
local winheight = curwinmeths.get_height() local winheight = curwinmeths.get_height()
-- I have no idea why there is + 4 needed. But otherwise it works fine with local buf_cont_start = rep_size - sb - winheight + 2
-- different scrollbacks. local function bufline (i)
local shift = -4 return ('%d: foobar'):format(i)
local buf_cont_start = rep_size - 1 - sb - winheight - shift end
local bufline = function(i) return ('%d: foobar'):format(i) end
for i = buf_cont_start,(rep_size - 1) do for i = buf_cont_start,(rep_size - 1) do
bufcontents[#bufcontents + 1] = bufline(i) bufcontents[#bufcontents + 1] = bufline(i)
end end
bufcontents[#bufcontents + 1] = '' bufcontents[#bufcontents + 1] = ''
bufcontents[#bufcontents + 1] = '[Process exited 0]' bufcontents[#bufcontents + 1] = '[Process exited 0]'
-- Do not ask me why displayed screen is one line *before* buffer
-- contents: buffer starts with 87:, screen with 86:.
local exp_screen = '\n' local exp_screen = '\n'
local did_cursor = false for i = 1,(winheight - 1) do
for i = 0,(winheight - 1) do local line = bufcontents[#bufcontents - winheight + i]
local line = bufline(buf_cont_start + i - 1)
exp_screen = (exp_screen exp_screen = (exp_screen
.. (did_cursor and '' or '^')
.. line .. line
.. (' '):rep(columns - #line) .. (' '):rep(columns - #line)
.. '|\n') .. '|\n')
did_cursor = true
end end
exp_screen = exp_screen .. (' '):rep(columns) .. '|\n' exp_screen = exp_screen..'^[Process exited 0] |\n'
exp_screen = exp_screen..(' '):rep(columns)..'|\n'
scr:expect(exp_screen) scr:expect(exp_screen)
eq(bufcontents, curbufmeths.get_lines(1, -1, true)) eq(bufcontents, curbufmeths.get_lines(0, -1, true))
end) end)
end) end)

View File

@ -20,26 +20,34 @@ describe(':terminal', function()
source([[ source([[
echomsg "msg1" echomsg "msg1"
echomsg "msg2" echomsg "msg2"
echomsg "msg3"
]]) ]])
-- Invoke a command that emits frequent terminal activity. -- Invoke a command that emits frequent terminal activity.
execute([[terminal while true; do echo X; done]]) execute([[terminal while true; do echo X; done]])
helpers.feed([[<C-\><C-N>]]) helpers.feed([[<C-\><C-N>]])
screen:expect([[ wait()
X |
X |
^X |
|
]])
helpers.sleep(10) -- Let some terminal activity happen. helpers.sleep(10) -- Let some terminal activity happen.
execute("messages") execute("messages")
screen:expect([[ screen:expect([[
X |
msg1 | msg1 |
msg2 | msg2 |
msg3 |
Press ENTER or type command to continue^ | Press ENTER or type command to continue^ |
]]) ]])
end) end)
it("in normal-mode :split does not move cursor", function()
execute([[terminal while true; do echo foo; sleep .1; done]])
helpers.feed([[<C-\><C-N>M]]) -- move cursor away from last line
wait()
eq(3, eval("line('$')")) -- window height
eq(2, eval("line('.')")) -- cursor is in the middle
execute('vsplit')
eq(2, eval("line('.')")) -- cursor stays where we put it
execute('split')
eq(2, eval("line('.')")) -- cursor stays where we put it
end)
end) end)
describe(':terminal (with fake shell)', function() describe(':terminal (with fake shell)', function()

View File

@ -1,7 +1,7 @@
local helpers = require('test.functional.helpers')(nil) local helpers = require('test.functional.helpers')(nil)
local Screen = require('test.functional.ui.screen') local Screen = require('test.functional.ui.screen')
local nvim_dir = helpers.nvim_dir local nvim_dir = helpers.nvim_dir
local execute, nvim, wait = helpers.execute, helpers.nvim, helpers.wait local execute, nvim = helpers.execute, helpers.nvim
local function feed_data(data) local function feed_data(data)
nvim('set_var', 'term_data', data) nvim('set_var', 'term_data', data)
@ -34,13 +34,15 @@ local function disable_mouse() feed_termcode('[?1002l') end
local default_command = '["'..nvim_dir..'/tty-test'..'"]' local default_command = '["'..nvim_dir..'/tty-test'..'"]'
local function screen_setup(extra_height, command) local function screen_setup(extra_rows, command, cols)
extra_rows = extra_rows and extra_rows or 0
command = command and command or default_command
cols = cols and cols or 50
nvim('command', 'highlight TermCursor cterm=reverse') nvim('command', 'highlight TermCursor cterm=reverse')
nvim('command', 'highlight TermCursorNC ctermbg=11') nvim('command', 'highlight TermCursorNC ctermbg=11')
nvim('set_var', 'terminal_scrollback_buffer_size', 10)
if not extra_height then extra_height = 0 end local screen = Screen.new(cols, 7 + extra_rows)
if not command then command = default_command end
local screen = Screen.new(50, 7 + extra_height)
screen:set_default_attr_ids({ screen:set_default_attr_ids({
[1] = {reverse = true}, -- focused cursor [1] = {reverse = true}, -- focused cursor
[2] = {background = 11}, -- unfocused cursor [2] = {background = 11}, -- unfocused cursor
@ -55,31 +57,42 @@ local function screen_setup(extra_height, command)
}) })
screen:attach({rgb=false}) screen:attach({rgb=false})
-- tty-test puts the terminal into raw mode and echoes all input. tests are
-- done by feeding it with terminfo codes to control the display and execute('enew | call termopen('..command..')')
-- verifying output with screen:expect. nvim('input', '<CR>')
execute('enew | call termopen('..command..') | startinsert') local vim_errmsg = nvim('eval', 'v:errmsg')
if vim_errmsg and "" ~= vim_errmsg then
error(vim_errmsg)
end
execute('setlocal scrollback=10')
execute('startinsert')
-- tty-test puts the terminal into raw mode and echoes input. Tests work by
-- feeding termcodes to control the display and asserting by screen:expect.
if command == default_command then if command == default_command then
-- wait for "tty ready" to be printed before each test or the terminal may -- Wait for "tty ready" to be printed before each test or the terminal may
-- still be in canonical mode(will echo characters for example) -- still be in canonical mode (will echo characters for example).
-- local empty_line = (' '):rep(cols + 1)
local empty_line = ' '
local expected = { local expected = {
'tty ready ', 'tty ready'..(' '):rep(cols - 8),
'{1: } ', '{1: }' ..(' '):rep(cols),
empty_line, empty_line,
empty_line, empty_line,
empty_line, empty_line,
empty_line, empty_line,
} }
for _ = 1, extra_height do for _ = 1, extra_rows do
table.insert(expected, empty_line) table.insert(expected, empty_line)
end end
table.insert(expected, '{3:-- TERMINAL --} ') table.insert(expected, '{3:-- TERMINAL --}' .. ((' '):rep(cols - 13)))
screen:expect(table.concat(expected, '\n')) screen:expect(table.concat(expected, '\n'))
else else
wait() -- This eval also acts as a wait().
if 0 == nvim('eval', "exists('b:terminal_job_id')") then
error("terminal job failed to start")
end
end end
return screen return screen
end end

View File

@ -3,7 +3,11 @@ local helpers = require('test.functional.helpers')(after_each)
local thelpers = require('test.functional.terminal.helpers') local thelpers = require('test.functional.terminal.helpers')
local clear, eq, curbuf = helpers.clear, helpers.eq, helpers.curbuf local clear, eq, curbuf = helpers.clear, helpers.eq, helpers.curbuf
local feed, nvim_dir, execute = helpers.feed, helpers.nvim_dir, helpers.execute local feed, nvim_dir, execute = helpers.feed, helpers.nvim_dir, helpers.execute
local eval = helpers.eval
local command = helpers.command
local wait = helpers.wait local wait = helpers.wait
local retry = helpers.retry
local curbufmeths = helpers.curbufmeths
local feed_data = thelpers.feed_data local feed_data = thelpers.feed_data
if helpers.pending_win32(pending) then return end if helpers.pending_win32(pending) then return end
@ -13,14 +17,14 @@ describe('terminal scrollback', function()
before_each(function() before_each(function()
clear() clear()
screen = thelpers.screen_setup() screen = thelpers.screen_setup(nil, nil, 30)
end) end)
after_each(function() after_each(function()
screen:detach() screen:detach()
end) end)
describe('when the limit is crossed', function() describe('when the limit is exceeded', function()
before_each(function() before_each(function()
local lines = {} local lines = {}
for i = 1, 30 do for i = 1, 30 do
@ -29,26 +33,26 @@ describe('terminal scrollback', function()
table.insert(lines, '') table.insert(lines, '')
feed_data(lines) feed_data(lines)
screen:expect([[ screen:expect([[
line26 | line26 |
line27 | line27 |
line28 | line28 |
line29 | line29 |
line30 | line30 |
{1: } | {1: } |
{3:-- TERMINAL --} | {3:-- TERMINAL --} |
]]) ]])
end) end)
it('will delete extra lines at the top', function() it('will delete extra lines at the top', function()
feed('<c-\\><c-n>gg') feed('<c-\\><c-n>gg')
screen:expect([[ screen:expect([[
^line16 | ^line16 |
line17 | line17 |
line18 | line18 |
line19 | line19 |
line20 | line20 |
line21 | line21 |
| |
]]) ]])
end) end)
end) end)
@ -57,13 +61,13 @@ describe('terminal scrollback', function()
before_each(function() before_each(function()
feed_data({'line1', 'line2', 'line3', 'line4', ''}) feed_data({'line1', 'line2', 'line3', 'line4', ''})
screen:expect([[ screen:expect([[
tty ready | tty ready |
line1 | line1 |
line2 | line2 |
line3 | line3 |
line4 | line4 |
{1: } | {1: } |
{3:-- TERMINAL --} | {3:-- TERMINAL --} |
]]) ]])
end) end)
@ -72,13 +76,13 @@ describe('terminal scrollback', function()
it('will hide the top line', function() it('will hide the top line', function()
screen:expect([[ screen:expect([[
line1 | line1 |
line2 | line2 |
line3 | line3 |
line4 | line4 |
line5 | line5 |
{1: } | {1: } |
{3:-- TERMINAL --} | {3:-- TERMINAL --} |
]]) ]])
eq(7, curbuf('line_count')) eq(7, curbuf('line_count'))
end) end)
@ -88,46 +92,46 @@ describe('terminal scrollback', function()
it('will hide the top 4 lines', function() it('will hide the top 4 lines', function()
screen:expect([[ screen:expect([[
line3 | line3 |
line4 | line4 |
line5 | line5 |
line6 | line6 |
line7 | line7 |
line8{1: } | line8{1: } |
{3:-- TERMINAL --} | {3:-- TERMINAL --} |
]]) ]])
feed('<c-\\><c-n>6k') feed('<c-\\><c-n>6k')
screen:expect([[ screen:expect([[
^line2 | ^line2 |
line3 | line3 |
line4 | line4 |
line5 | line5 |
line6 | line6 |
line7 | line7 |
| |
]]) ]])
feed('gg') feed('gg')
screen:expect([[ screen:expect([[
^tty ready | ^tty ready |
line1 | line1 |
line2 | line2 |
line3 | line3 |
line4 | line4 |
line5 | line5 |
| |
]]) ]])
feed('G') feed('G')
screen:expect([[ screen:expect([[
line3 | line3 |
line4 | line4 |
line5 | line5 |
line6 | line6 |
line7 | line7 |
^line8{2: } | ^line8{2: } |
| |
]]) ]])
end) end)
end) end)
@ -138,12 +142,12 @@ describe('terminal scrollback', function()
local function will_hide_top_line() local function will_hide_top_line()
screen:try_resize(screen._width, screen._height - 1) screen:try_resize(screen._width, screen._height - 1)
screen:expect([[ screen:expect([[
line2 | line2 |
line3 | line3 |
line4 | line4 |
rows: 5, cols: 50 | rows: 5, cols: 30 |
{1: } | {1: } |
{3:-- TERMINAL --} | {3:-- TERMINAL --} |
]]) ]])
end end
@ -157,18 +161,18 @@ describe('terminal scrollback', function()
it('will hide the top 3 lines', function() it('will hide the top 3 lines', function()
screen:expect([[ screen:expect([[
rows: 5, cols: 50 | rows: 5, cols: 30 |
rows: 3, cols: 50 | rows: 3, cols: 30 |
{1: } | {1: } |
{3:-- TERMINAL --} | {3:-- TERMINAL --} |
]]) ]])
eq(8, curbuf('line_count')) eq(8, curbuf('line_count'))
feed('<c-\\><c-n>3k') feed('<c-\\><c-n>3k')
screen:expect([[ screen:expect([[
^line4 | ^line4 |
rows: 5, cols: 50 | rows: 5, cols: 30 |
rows: 3, cols: 50 | rows: 3, cols: 30 |
| |
]]) ]])
end) end)
end) end)
@ -183,11 +187,11 @@ describe('terminal scrollback', function()
local function will_delete_last_two_lines() local function will_delete_last_two_lines()
screen:expect([[ screen:expect([[
tty ready | tty ready |
rows: 4, cols: 50 | rows: 4, cols: 30 |
{1: } | {1: } |
| |
{3:-- TERMINAL --} | {3:-- TERMINAL --} |
]]) ]])
eq(4, curbuf('line_count')) eq(4, curbuf('line_count'))
end end
@ -202,25 +206,25 @@ describe('terminal scrollback', function()
it('will delete the last line and hide the first', function() it('will delete the last line and hide the first', function()
screen:expect([[ screen:expect([[
rows: 4, cols: 50 | rows: 4, cols: 30 |
rows: 3, cols: 50 | rows: 3, cols: 30 |
{1: } | {1: } |
{3:-- TERMINAL --} | {3:-- TERMINAL --} |
]]) ]])
eq(4, curbuf('line_count')) eq(4, curbuf('line_count'))
feed('<c-\\><c-n>gg') feed('<c-\\><c-n>gg')
screen:expect([[ screen:expect([[
^tty ready | ^tty ready |
rows: 4, cols: 50 | rows: 4, cols: 30 |
rows: 3, cols: 50 | rows: 3, cols: 30 |
| |
]]) ]])
feed('a') feed('a')
screen:expect([[ screen:expect([[
rows: 4, cols: 50 | rows: 4, cols: 30 |
rows: 3, cols: 50 | rows: 3, cols: 30 |
{1: } | {1: } |
{3:-- TERMINAL --} | {3:-- TERMINAL --} |
]]) ]])
end) end)
end) end)
@ -231,20 +235,20 @@ describe('terminal scrollback', function()
before_each(function() before_each(function()
feed_data({'line1', 'line2', 'line3', 'line4', ''}) feed_data({'line1', 'line2', 'line3', 'line4', ''})
screen:expect([[ screen:expect([[
tty ready | tty ready |
line1 | line1 |
line2 | line2 |
line3 | line3 |
line4 | line4 |
{1: } | {1: } |
{3:-- TERMINAL --} | {3:-- TERMINAL --} |
]]) ]])
screen:try_resize(screen._width, screen._height - 3) screen:try_resize(screen._width, screen._height - 3)
screen:expect([[ screen:expect([[
line4 | line4 |
rows: 3, cols: 50 | rows: 3, cols: 30 |
{1: } | {1: } |
{3:-- TERMINAL --} | {3:-- TERMINAL --} |
]]) ]])
eq(7, curbuf('line_count')) eq(7, curbuf('line_count'))
end) end)
@ -253,11 +257,11 @@ describe('terminal scrollback', function()
local function pop_then_push() local function pop_then_push()
screen:try_resize(screen._width, screen._height + 1) screen:try_resize(screen._width, screen._height + 1)
screen:expect([[ screen:expect([[
line4 | line4 |
rows: 3, cols: 50 | rows: 3, cols: 30 |
rows: 4, cols: 50 | rows: 4, cols: 30 |
{1: } | {1: } |
{3:-- TERMINAL --} | {3:-- TERMINAL --} |
]]) ]])
end end
@ -272,26 +276,26 @@ describe('terminal scrollback', function()
local function pop3_then_push1() local function pop3_then_push1()
screen:expect([[ screen:expect([[
line2 | line2 |
line3 | line3 |
line4 | line4 |
rows: 3, cols: 50 | rows: 3, cols: 30 |
rows: 4, cols: 50 | rows: 4, cols: 30 |
rows: 7, cols: 50 | rows: 7, cols: 30 |
{1: } | {1: } |
{3:-- TERMINAL --} | {3:-- TERMINAL --} |
]]) ]])
eq(9, curbuf('line_count')) eq(9, curbuf('line_count'))
feed('<c-\\><c-n>gg') feed('<c-\\><c-n>gg')
screen:expect([[ screen:expect([[
^tty ready | ^tty ready |
line1 | line1 |
line2 | line2 |
line3 | line3 |
line4 | line4 |
rows: 3, cols: 50 | rows: 3, cols: 30 |
rows: 4, cols: 50 | rows: 4, cols: 30 |
| |
]]) ]])
end end
@ -306,18 +310,18 @@ describe('terminal scrollback', function()
it('will show all lines and leave a blank one at the end', function() it('will show all lines and leave a blank one at the end', function()
screen:expect([[ screen:expect([[
tty ready | tty ready |
line1 | line1 |
line2 | line2 |
line3 | line3 |
line4 | line4 |
rows: 3, cols: 50 | rows: 3, cols: 30 |
rows: 4, cols: 50 | rows: 4, cols: 30 |
rows: 7, cols: 50 | rows: 7, cols: 30 |
rows: 11, cols: 50 | rows: 11, cols: 30 |
{1: } | {1: } |
| |
{3:-- TERMINAL --} | {3:-- TERMINAL --} |
]]) ]])
-- since there's an empty line after the cursor, the buffer line -- since there's an empty line after the cursor, the buffer line
-- count equals the terminal screen height -- count equals the terminal screen height
@ -332,30 +336,115 @@ end)
describe('terminal prints more lines than the screen height and exits', function() describe('terminal prints more lines than the screen height and exits', function()
it('will push extra lines to scrollback', function() it('will push extra lines to scrollback', function()
clear() clear()
local screen = Screen.new(50, 7) local screen = Screen.new(30, 7)
screen:attach({rgb=false}) screen:attach({rgb=false})
execute('call termopen(["'..nvim_dir..'/tty-test", "10"]) | startinsert') execute('call termopen(["'..nvim_dir..'/tty-test", "10"]) | startinsert')
wait() wait()
screen:expect([[ screen:expect([[
line6 | line6 |
line7 | line7 |
line8 | line8 |
line9 | line9 |
| |
[Process exited 0] | [Process exited 0] |
-- TERMINAL -- | -- TERMINAL -- |
]]) ]])
feed('<cr>') feed('<cr>')
-- closes the buffer correctly after pressing a key -- closes the buffer correctly after pressing a key
screen:expect([[ screen:expect([[
^ | ^ |
~ | ~ |
~ | ~ |
~ | ~ |
~ | ~ |
~ | ~ |
| |
]]) ]])
end) end)
end) end)
describe("'scrollback' option", function()
before_each(function()
clear()
end)
local function expect_lines(expected)
local actual = eval("line('$')")
if expected ~= actual then
error('expected: '..expected..', actual: '..tostring(actual))
end
end
it('set to 0 behaves as 1', function()
local screen = thelpers.screen_setup(nil, "['sh']", 30)
curbufmeths.set_option('scrollback', 0)
feed_data('for i in $(seq 1 30); do echo "line$i"; done\n')
screen:expect('line30 ', nil, nil, nil, true)
retry(nil, nil, function() expect_lines(7) end)
screen:detach()
end)
it('deletes lines (only) if necessary', function()
local screen = thelpers.screen_setup(nil, "['sh']", 30)
curbufmeths.set_option('scrollback', 200)
-- Wait for prompt.
screen:expect('$', nil, nil, nil, true)
wait()
feed_data('for i in $(seq 1 30); do echo "line$i"; done\n')
screen:expect('line30 ', nil, nil, nil, true)
retry(nil, nil, function() expect_lines(33) end)
curbufmeths.set_option('scrollback', 10)
wait()
retry(nil, nil, function() expect_lines(16) end)
curbufmeths.set_option('scrollback', 10000)
eq(16, eval("line('$')"))
-- Terminal job data is received asynchronously, may happen before the
-- 'scrollback' option is synchronized with the internal sb_buffer.
command('sleep 100m')
feed_data('for i in $(seq 1 40); do echo "line$i"; done\n')
screen:expect('line40 ', nil, nil, nil, true)
retry(nil, nil, function() expect_lines(58) end)
-- Verify off-screen state
eq('line35', eval("getline(line('w0') - 1)"))
eq('line26', eval("getline(line('w0') - 10)"))
screen:detach()
end)
it('defaults to 1000', function()
execute('terminal')
eq(1000, curbufmeths.get_option('scrollback'))
end)
it('error if set to invalid values', function()
local status, rv = pcall(command, 'set scrollback=-2')
eq(false, status) -- assert failure
eq('E474:', string.match(rv, "E%d*:"))
status, rv = pcall(command, 'set scrollback=100001')
eq(false, status) -- assert failure
eq('E474:', string.match(rv, "E%d*:"))
end)
it('defaults to -1 on normal buffers', function()
execute('new')
eq(-1, curbufmeths.get_option('scrollback'))
end)
it('error if set on a normal buffer', function()
command('new')
execute('set scrollback=42')
feed('<CR>')
eq('E474:', string.match(eval("v:errmsg"), "E%d*:"))
end)
end)

View File

@ -28,14 +28,14 @@ describe('terminal', function()
feed('<c-\\><c-n>') feed('<c-\\><c-n>')
execute('2split') execute('2split')
screen:expect([[ screen:expect([[
tty ready | rows: 2, cols: 50 |
^rows: 2, cols: 50 | {2:^ } |
========== | ========== |
tty ready |
rows: 2, cols: 50 | rows: 2, cols: 50 |
{2: } | {2: } |
{4:~ }| {4:~ }|
{4:~ }| {4:~ }|
{4:~ }|
========== | ========== |
:2split | :2split |
]]) ]])
@ -54,14 +54,14 @@ describe('terminal', function()
]]) ]])
execute('wincmd p') execute('wincmd p')
screen:expect([[ screen:expect([[
rows: 5, cols: 50 | rows: 2, cols: 50 |
^rows: 2, cols: 50 | {2:^ } |
========== | ========== |
rows: 5, cols: 50 |
rows: 2, cols: 50 | rows: 2, cols: 50 |
{2: } | {2: } |
{4:~ }| {4:~ }|
{4:~ }| {4:~ }|
{4:~ }|
========== | ========== |
:wincmd p | :wincmd p |
]]) ]])

View File

@ -207,7 +207,9 @@ local function check_cores(app)
out:write(('\nTests covered by this check: %u\n'):format(tests_skipped + 1)) out:write(('\nTests covered by this check: %u\n'):format(tests_skipped + 1))
end end
tests_skipped = 0 tests_skipped = 0
assert(0 == found_cores) if found_cores > 0 then
error("crash detected (see above)")
end
end end
return { return {