Commit e7bbd35c authored by Justin M. Keyes's avatar Justin M. Keyes

terminal: 'scrollback'

Closes #2637
parent 300eca3d
......@@ -4,28 +4,19 @@
NVIM REFERENCE MANUAL by Thiago de Arruda
Embedded terminal emulator *terminal-emulator*
Terminal emulator *terminal-emulator*
1. Introduction |terminal-emulator-intro|
2. Spawning |terminal-emulator-spawning|
3. Input |terminal-emulator-input|
4. Configuration |terminal-emulator-configuration|
5. Status Variables |terminal-emulator-status|
Nvim embeds a VT220/xterm terminal emulator based on libvterm. The terminal is
presented as a special buffer type, asynchronously updated from the virtual
terminal as data is received from the program connected to it.
==============================================================================
1. Introduction *terminal-emulator-intro*
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/
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.
==============================================================================
2. Spawning *terminal-emulator-spawning*
Spawning *terminal-emulator-spawning*
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
|autocmd-nested| modifier is required to use it in an autocmd. >
autocmd VimEnter * nested split term://sh
< This is only mentioned for reference; you should use the |:terminal|
command instead.
< This is only mentioned for reference; use |:terminal| instead.
When the terminal spawns the program, the buffer will start to mirror the
terminal display and change its name to `term://$CWD//$PID:$COMMAND`.
Note that |:mksession| will "save" the terminal buffers by restarting all
programs when the session is restored.
terminal display and change its name to `term://{cwd}//{pid}:{cmd}`.
The "term://..." scheme enables |:mksession| to "restore" a terminal buffer by
restarting the {cmd} when the session is loaded.
==============================================================================
3. Input *terminal-emulator-input*
Sending input is possible by entering terminal mode, which is achieved by
pressing any key that would enter insert mode in a normal buffer (|i| or |a|
for example). The |:terminal| ex command will automatically enter terminal
mode once it's spawned. While in terminal mode, Nvim will forward all keys to
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
"t" prefix. It's possible to use terminal mappings to customize interaction
with the terminal. For example, here's how to map <Esc> to exit terminal mode:
>
Input *terminal-emulator-input*
To send input, enter terminal mode using any command that would enter "insert
mode" in a normal buffer, such as |i| or |:startinsert|. In this mode all keys
except <C-\><C-N> are sent to the underlying program. Use <C-\><C-N> to return
to normal mode. |CTRL-\_CTRL-N|
Terminal mode has its own |:tnoremap| namespace for mappings, this can be used
to automate any terminal interaction. To map <Esc> to exit terminal mode: >
:tnoremap <Esc> <C-\><C-n>
<
Navigating to other windows is only possible by exiting to normal mode, which
can be cumbersome with <C-\><C-n> keys. To improve the navigation experience,
you could use the following mappings:
>
Navigating to other windows is only possible in normal mode. For convenience,
you could use these mappings: >
:tnoremap <A-h> <C-\><C-n><C-w>h
:tnoremap <A-j> <C-\><C-n><C-w>j
: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-l> <C-w>l
<
This configuration allows using `Alt+{h,j,k,l}` to navigate between windows no
matter if they are displaying a normal buffer or a terminal buffer in terminal
mode.
Then you can use `Alt+{h,j,k,l}` to navigate between windows from any 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
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.
==============================================================================
4. Configuration *terminal-emulator-configuration*
Configuration *terminal-emulator-configuration*
Options: 'scrollback'
Events: |TermOpen|, |TermClose|
Highlight groups: |hl-TermCursor|, |hl-TermCursorNC|
Terminal buffers can be customized through the following global/buffer-local
variables (set via the |TermOpen| autocmd):
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
color index, between 0 and 255 inclusive. This setting only affects UIs with
RGB capabilities; for normal terminals the color index is simply forwarded.
The configuration variables are only processed when the terminal starts, which
is why it needs to be done with the |TermOpen| autocmd or setting global
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|.
The `{g,b}:terminal_color_$NUM` variables are processed only when the terminal
starts (after |TermOpen|).
==============================================================================
5. Status Variables *terminal-emulator-status*
Status Variables *terminal-emulator-status*
Terminal buffers maintain some information about the terminal in buffer-local
variables:
......@@ -126,11 +104,8 @@ variables:
- *b:terminal_job_pid* The PID of the top-level process running in the
terminal.
These variables will have a value by the time the TermOpen autocmd runs, and
will continue to have a value for the lifetime of the terminal buffer, making
them suitable for use in 'statusline'. For example, to show the terminal title
as the status line:
>
These variables are initialized before TermOpen, so you can use them in
a local 'statusline'. Example: >
:autocmd TermOpen * setlocal statusline=%{b:term_title}
<
==============================================================================
......
......@@ -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
height with ":set scroll=0".
*'scrollback'* *'scbk'* *'noscrollback'* *'noscbk'*
'scrollback' 'scbk' boolean (default: 1000)
global or local to buffer |global-local|
*'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' boolean (default off)
......
......@@ -207,23 +207,15 @@ g8 Print the hex values of the bytes used in the
:sh[ell] Removed. |vim-differences| {Nvim}
*:terminal* *:te*
:te[rminal][!] {cmd} Spawns {cmd} using the current value of 'shell' and
'shellcmdflag' in a new terminal buffer. This is
equivalent to: >
:te[rminal][!] {cmd} Execute {cmd} with 'shell' in a |terminal-emulator|
buffer. Equivalent to: >
:enew
:call termopen('{cmd}')
:startinsert
<
If no {cmd} is given, 'shellcmdflag' will not be sent
to |termopen()|.
Like |:enew|, it will fail if the current buffer is
modified, but can be forced with "!". See |termopen()|
and |terminal-emulator|.
See |jobstart()|.
To switch to terminal mode automatically:
>
To enter terminal mode automatically: >
autocmd BufEnter term://* startinsert
<
*:!cmd* *:!* *E34*
......
......@@ -10,7 +10,7 @@
Memcheck:Leak
fun:malloc
fun:uv_spawn
fun:pipe_process_spawn
fun:libuv_process_spawn
fun:process_spawn
fun:job_start
}
......@@ -5845,8 +5845,8 @@ bool garbage_collect(bool testing)
garbage_collect_at_exit = false;
}
// We advance by two because we add one for items referenced through
// previous_funccal.
// We advance by two (COPYID_INC) because we add one for items referenced
// through previous_funccal.
const int copyID = get_copyID();
// 1. Go through all accessible variables and mark all lists and dicts
......
......@@ -7420,22 +7420,20 @@ static void nv_esc(cmdarg_T *cap)
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)
{
/* <Insert> is equal to "i" */
if (cap->cmdchar == K_INS || cap->cmdchar == K_KINS)
// <Insert> is equal to "i"
if (cap->cmdchar == K_INS || cap->cmdchar == K_KINS) {
cap->cmdchar = 'i';
}
/* in Visual mode "A" and "I" are an operator */
if (VIsual_active && (cap->cmdchar == 'A' || cap->cmdchar == 'I'))
// in Visual mode "A" and "I" are an operator
if (VIsual_active && (cap->cmdchar == 'A' || cap->cmdchar == 'I')) {
v_visop(cap);
/* in Visual mode and after an operator "a" and "i" are for text objects */
else if ((cap->cmdchar == 'a' || cap->cmdchar == 'i')
&& (cap->oap->op_type != OP_NOP || VIsual_active)) {
// in Visual mode and after an operator "a" and "i" are for text objects
} else if ((cap->cmdchar == 'a' || cap->cmdchar == 'i')
&& (cap->oap->op_type != OP_NOP || VIsual_active)) {
nv_object(cap);
} else if (!curbuf->b_p_ma && !p_im && !curbuf->terminal) {
// Only give this error when 'insertmode' is off.
......
......@@ -3994,16 +3994,7 @@ set_num_option (
/*
* Number options that need some action when changed
*/
if (pp == &p_scbk) {
// '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 (pp == &p_wh || pp == &p_hh) {
if (p_wh < 1) {
errmsg = e_positive;
p_wh = 1;
......@@ -4205,7 +4196,19 @@ set_num_option (
FOR_ALL_TAB_WINDOWS(tp, 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_nopaste = p_ai_nopaste;
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_nopaste = p_tw_nopaste;
buf->b_p_tw_nobin = p_tw_nobin;
......
......@@ -1918,7 +1918,7 @@ return {
vi_def=true,
varname='p_scbk',
redraw={'current_buffer'},
defaults={if_true={vi=1000}}
defaults={if_true={vi=-1}}
},
{
full_name='scrollbind', abbreviation='scb',
......
......@@ -7113,8 +7113,9 @@ void showruler(int always)
}
if ((*p_stl != NUL || *curwin->w_p_stl != NUL) && curwin->w_status_height) {
redraw_custom_statusline(curwin);
} else
} else {
win_redr_ruler(curwin, always);
}
if (need_maketitle
|| (p_icon && (stl_syntax & STL_IN_ICON))
......
This diff is collapsed.
......@@ -308,8 +308,9 @@ bool undo_allowed(void)
/// Get the 'undolevels' value for the current buffer.
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 curbuf->b_p_ul;
}
......
......@@ -31,45 +31,40 @@ describe(':edit term://*', function()
eq(termopen_runs[1], termopen_runs[1]:match('^term://.//%d+:$'))
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 scr = get_screen(columns, lines)
local rep = 'a'
meths.set_option('shellcmdflag', 'REP ' .. rep)
local rep_size = rep:byte()
local rep_size = rep:byte() -- 'a' => 97
local sb = 10
local gsb = 20
meths.set_var('terminal_scrollback_buffer_size', gsb)
command('autocmd TermOpen * :let b:terminal_scrollback_buffer_size = '
.. tostring(sb))
command('autocmd TermOpen * :setlocal scrollback='..tostring(sb))
command('edit term://foobar')
local bufcontents = {}
local winheight = curwinmeths.get_height()
-- I have no idea why there is + 4 needed. But otherwise it works fine with
-- different scrollbacks.
local shift = -4
local buf_cont_start = rep_size - 1 - sb - winheight - shift
local bufline = function(i) return ('%d: foobar'):format(i) end
local buf_cont_start = rep_size - sb - winheight + 2
local function bufline (i)
return ('%d: foobar'):format(i)
end
for i = buf_cont_start,(rep_size - 1) do
bufcontents[#bufcontents + 1] = bufline(i)
end
bufcontents[#bufcontents + 1] = ''
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 did_cursor = false
for i = 0,(winheight - 1) do
local line = bufline(buf_cont_start + i - 1)
for i = 1,(winheight - 1) do
local line = bufcontents[#bufcontents - winheight + i]
exp_screen = (exp_screen
.. (did_cursor and '' or '^')
.. line
.. (' '):rep(columns - #line)
.. '|\n')
did_cursor = true
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)
eq(bufcontents, curbufmeths.get_lines(1, -1, true))
eq(bufcontents, curbufmeths.get_lines(0, -1, true))
end)
end)
......@@ -20,22 +20,18 @@ describe(':terminal', function()
source([[
echomsg "msg1"
echomsg "msg2"
echomsg "msg3"
]])
-- Invoke a command that emits frequent terminal activity.
execute([[terminal while true; do echo X; done]])
helpers.feed([[<C-\><C-N>]])
screen:expect([[
X |
X |
^X |
|
]])
wait()
helpers.sleep(10) -- Let some terminal activity happen.
execute("messages")
screen:expect([[
X |
msg1 |
msg2 |
msg3 |
Press ENTER or type command to continue^ |
]])
end)
......
......@@ -37,7 +37,6 @@ local default_command = '["'..nvim_dir..'/tty-test'..'"]'
local function screen_setup(extra_height, command)
nvim('command', 'highlight TermCursor cterm=reverse')
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 command then command = default_command end
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
-- done by feeding it with terminfo codes to control the display and
-- 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
-- wait for "tty ready" to be printed before each test or the terminal may
-- still be in canonical mode(will echo characters for example)
......
......@@ -3,7 +3,11 @@ local helpers = require('test.functional.helpers')(after_each)
local thelpers = require('test.functional.terminal.helpers')
local clear, eq, curbuf = helpers.clear, helpers.eq, helpers.curbuf
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 retry = helpers.retry
local curbufmeths = helpers.curbufmeths
local feed_data = thelpers.feed_data
if helpers.pending_win32(pending) then return end
......@@ -20,7 +24,7 @@ describe('terminal scrollback', function()
screen:detach()
end)
describe('when the limit is crossed', function()
describe('when the limit is exceeded', function()
before_each(function()
local lines = {}
for i = 1, 30 do
......@@ -359,3 +363,88 @@ describe('terminal prints more lines than the screen height and exits', function
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)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment