terminal: 'scrollback'

Closes #2637
This commit is contained in:
Justin M. Keyes 2017-02-21 15:16:48 +01:00
parent 300eca3d30
commit e7bbd35c81
15 changed files with 282 additions and 208 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,27 +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:
- 'scrollback' option: Scrollback lines (output history) limit.
- `{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:
@ -126,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,9 +4949,15 @@ 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'* *'noscrollback'* *'noscbk'* *'scrollback'* *'scbk'*
'scrollback' 'scbk' boolean (default: 1000) 'scrollback' 'scbk' number (default: 1000
global or local to buffer |global-local| 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)

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

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

@ -7420,21 +7420,19 @@ 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 && !curbuf->terminal) { } else if (!curbuf->b_p_ma && !p_im && !curbuf->terminal) {

View File

@ -3994,16 +3994,7 @@ set_num_option (
/* /*
* Number options that need some action when changed * Number options that need some action when changed
*/ */
if (pp == &p_scbk) { if (pp == &p_wh || pp == &p_hh) {
// 'scrollback'
if (p_scbk < 1) {
errmsg = e_invarg;
p_scbk = 0;
} else if (p_scbk > 100000) {
errmsg = e_invarg;
p_scbk = 100000;
}
} else if (pp == &p_wh || pp == &p_hh) {
if (p_wh < 1) { if (p_wh < 1) {
errmsg = e_positive; errmsg = e_positive;
p_wh = 1; p_wh = 1;
@ -4205,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);
}
} }
/* /*
@ -5641,7 +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 = p_scbk; 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

@ -1918,7 +1918,7 @@ return {
vi_def=true, vi_def=true,
varname='p_scbk', varname='p_scbk',
redraw={'current_buffer'}, redraw={'current_buffer'},
defaults={if_true={vi=1000}} defaults={if_true={vi=-1}}
}, },
{ {
full_name='scrollbind', abbreviation='scb', full_name='scrollbind', abbreviation='scb',

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 = {
@ -238,28 +230,21 @@ Terminal *terminal_open(TerminalOptions opts)
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_ma = false; // 'nomodifiable'
curbuf->b_p_ul = -1; // disable undo 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) {
@ -338,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;
} }
@ -671,10 +656,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;
@ -686,10 +676,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);
} }
@ -699,6 +691,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++;
@ -714,6 +707,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;
@ -726,24 +724,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);
@ -889,7 +887,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;
@ -977,8 +975,7 @@ static void refresh_terminal(Terminal *term)
}); });
adjust_topline(term, buf, pending_resize); adjust_topline(term, buf, pending_resize);
} }
// 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.
@ -1012,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;
@ -1041,6 +1068,8 @@ 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
@ -1052,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);
@ -1182,17 +1210,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

@ -308,8 +308,9 @@ bool undo_allowed(void)
/// Get the 'undolevels' value for the current buffer. /// Get the 'undolevels' value for the current buffer.
static long get_undolevel(void) static long get_undolevel(void)
{ {
if (curbuf->b_p_ul == NO_LOCAL_UNDOLEVEL) 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,40 @@ 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)
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,22 +20,18 @@ 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)

View File

@ -37,7 +37,6 @@ local default_command = '["'..nvim_dir..'/tty-test'..'"]'
local function screen_setup(extra_height, command) local function screen_setup(extra_height, command)
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 if not extra_height then extra_height = 0 end
if not command then command = default_command end if not command then command = default_command end
local screen = Screen.new(50, 7 + extra_height) local screen = Screen.new(50, 7 + extra_height)
@ -58,7 +57,9 @@ local function screen_setup(extra_height, command)
-- tty-test puts the terminal into raw mode and echoes all input. tests are -- 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 -- done by feeding it with terminfo codes to control the display and
-- verifying output with screen:expect. -- verifying output with screen:expect.
execute('enew | call termopen('..command..') | startinsert') execute('enew | call termopen('..command..')')
execute('setlocal scrollback=10')
execute('startinsert')
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)

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
@ -20,7 +24,7 @@ describe('terminal scrollback', 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
@ -359,3 +363,88 @@ describe('terminal prints more lines than the screen height and exits', function
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)