Files
neovim/test/functional/terminal/tui_spec.lua
Kyle 5cbb9d613b fix(startup): wait for bg detection before user config #37075
Problem:
Automatic background detection sets the background option too late,
which loads colorschemes twice and causes problems when the user's
terminal background doesn't match the default (#32109, #36211, #36416).

Solution:
Use a DA1 query to determine whether the TTY supports OSC 11. Wait for
background detection and setting to complete before processing user
config.

Note: To preserve the existing behavior as much as possible, this triggers
OptionSet manually on VimEnter (since it won't trigger automatically if
we set bg during startup). However, I'm unsure if this behavior is
truly desired given that the documentation says OptionSet is triggered
"After setting an option (except during |startup|)."

Also fixes flickering issue #28667. To check for flickering:

    nvim --clean --cmd "set termguicolors" --cmd "echo \"foo\"" --cmd "sleep 10"

On master, this gives me a black screen for 10 seconds, but on this
branch, the background is dark or light depending on the terminal
background (since the option is now set during startup rather than after
VimEnter).
2026-02-27 04:52:52 -05:00

4490 lines
167 KiB
Lua

-- TUI acceptance tests.
-- Uses :terminal as a way to send keys and assert screen state.
--
-- "bracketed paste" terminal feature:
-- http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Bracketed-Paste-Mode
local t = require('test.testutil')
local n = require('test.functional.testnvim')()
local Screen = require('test.functional.ui.screen')
local tt = require('test.functional.testterm')
local eq = t.eq
local feed_data = tt.feed_data
local clear = n.clear
local command = n.command
local exec = n.exec
local exec_lua = n.exec_lua
local testprg = n.testprg
local retry = t.retry
local nvim_prog = n.nvim_prog
local nvim_set = n.nvim_set
local ok = t.ok
local read_file = t.read_file
local fn = n.fn
local api = n.api
local is_ci = t.is_ci
local is_os = t.is_os
local new_pipename = n.new_pipename
local set_session = n.set_session
local write_file = t.write_file
local eval = n.eval
local assert_log = t.assert_log
local testlog = 'Xtest-tui-log'
-- Using this to have 'notermguicolors' in Nvim instances without starting a timer
-- that causes delay on exit with ASAN/TSAN.
local env_notermguicolors = { COLORTERM = 'xterm-256color' }
describe('TUI', function()
it('exit status 1 and error message with server --listen error #34365', function()
clear()
local addr_in_use = api.nvim_get_vvar('servername')
local screen = tt.setup_child_nvim(
{ '--listen', addr_in_use, '-u', 'NONE', '-i', 'NONE' },
{ extra_rows = 10, cols = 60, env = { NVIM_LOG_FILE = testlog } }
)
finally(function()
os.remove(testlog)
end)
screen:expect({ any = vim.pesc('[Process exited 1]') })
-- When the address is very long, the error message may be only partly visible.
if #addr_in_use <= 600 then
screen:expect({
any = vim.pesc(
('%s: Failed to --listen: address already in use:'):format(
is_os('win') and 'nvim.exe' or 'nvim'
)
),
unchanged = true,
})
end
assert_log(
vim.pesc('Failed to start server: address already in use: ' .. addr_in_use),
testlog,
100
)
end)
it('suspending does not crash or hang', function()
clear()
local screen = tt.setup_child_nvim({ '--clean' }, { env = env_notermguicolors })
local s0 = [[
^ |
~ |*3
{2:[No Name] 0,0-1 All}|
|
{5:-- TERMINAL --} |
]]
screen:expect(s0)
feed_data(':')
local s1 = [[
|
~ |*3
{2:[No Name] 0,0-1 All}|
:^ |
{5:-- TERMINAL --} |
]]
screen:expect(s1)
feed_data('suspend\r')
if is_os('win') then -- no-op on Windows
screen:expect([[
^ |
~ |*3
{2:[No Name] 0,0-1 All}|
:suspend |
{5:-- TERMINAL --} |
]])
else -- resuming works on other platforms
screen:expect([[
|*5
^[Process suspended] |
{5:-- TERMINAL --} |
]])
n.feed('<Space>')
screen:expect(s0)
end
feed_data(':')
screen:expect(s1)
end)
end)
describe('TUI :detach', function()
it('does not stop server', function()
local job_opts = { env = t.shallowcopy(env_notermguicolors) }
if is_os('win') then
-- TODO(justinmk): on Windows,
-- - tt.setup_child_nvim() is broken.
-- - session.lua is broken after the pipe closes.
-- So this test currently just exercises __NVIM_DETACH + :detach, without asserting anything.
-- TODO(justinmk): temporary hack for Windows.
job_opts.env['__NVIM_DETACH'] = '1'
n.clear(job_opts)
local screen = Screen.new(50, 10)
n.feed('iHello, World')
screen:expect([[
Hello, World^ |
{1:~ }|*8
{5:-- INSERT --} |
]])
-- local addr = api.nvim_get_vvar('servername')
eq(1, #n.api.nvim_list_uis())
-- TODO(justinmk): test util should not freak out when the pipe closes.
n.expect_exit(n.command, 'detach')
-- n.get_session():close() -- XXX: hangs
-- n.set_session(n.connect(addr)) -- XXX: hangs
-- eq(0, #n.api.nvim_list_uis()) -- XXX: hangs
-- Avoid a dangling process.
n.get_session():close('kill')
-- n.expect_exit(n.command, 'qall!')
return
end
n.clear()
finally(function()
n.check_close()
end)
local child_server = new_pipename()
local screen = tt.setup_child_nvim({
'--listen',
child_server,
'-u',
'NONE',
'-i',
'NONE',
'--cmd',
'colorscheme vim',
'--cmd',
nvim_set .. ' laststatus=2 background=dark',
}, job_opts)
tt.feed_data('iHello, World')
screen:expect([[
Hello, World^ |
{100:~ }|*3
{3:[No Name] [+] }|
{5:-- INSERT --} |
{5:-- TERMINAL --} |
]])
local child_session = n.connect(child_server)
finally(function()
-- Avoid a dangling process after :detach.
child_session:request('nvim_command', 'qall!')
end)
local status, child_uis = child_session:request('nvim_list_uis')
assert(status)
eq(1, #child_uis)
eq(
{ false, { 0, 'Vim(detach):E477: No ! allowed: detach!' } },
{ child_session:request('nvim_command', 'detach!') }
)
eq(
{ false, { 0, 'Vim(detach):E481: No range allowed: 1detach' } },
{ child_session:request('nvim_command', '1detach') }
)
eq(
{ false, { 0, 'Vim(detach):E488: Trailing characters: foo: detach foo' } },
{ child_session:request('nvim_command', 'detach foo') }
)
tt.feed_data('\027\027:detach\013')
-- Note: "Process exited" message is misleading; tt.setup_child_nvim() sees the foreground
-- process (client) exited, and doesn't know the server is still running?
screen:expect {
any = [[Process exited 0]],
}
child_uis --[[@type any[] ]] = ({ child_session:request('nvim_list_uis') })[2]
eq(0, #child_uis)
-- NOTE: The tt.setup_child_nvim() screen just wraps :terminal, it's not connected to the child.
-- To use it again, we need to detach the old one.
screen:detach()
-- Edit some text on the headless server.
status = (child_session:request('nvim_input', 'ddiWe did it, pooky.<Esc><Esc>'))
assert(status)
-- Test reattach by connecting a new TUI.
local screen_reattached = tt.setup_child_nvim({
'--remote-ui',
'--server',
child_server,
}, job_opts)
screen_reattached:expect([[
We did it, pooky^. |
{100:~ }|*3
{3:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
end)
end)
describe('TUI :restart', function()
it('validation', function()
clear()
eq('Vim(restart):E481: No range allowed: :1restart', t.pcall_err(n.command, ':1restart'))
end)
it('works', function()
clear()
finally(function()
n.check_close()
end)
local server_pipe = new_pipename()
local screen = tt.setup_child_nvim({
'-u',
'NONE',
'-i',
'NONE',
'--listen',
server_pipe,
'--cmd',
'colorscheme vim',
'--cmd',
nvim_set .. ' notermguicolors laststatus=2 background=dark',
'--cmd',
'echo getpid()',
}, { env = env_notermguicolors })
--- FIXME: On Windows spaces at the end of a screen line may have wrong attrs.
--- Remove this function when that's fixed.
---
--- @param s string
local function screen_expect(s)
if is_os('win') then
s = s:gsub(' *%} +%|\n', '{MATCH: *}}{MATCH: *}|\n')
s = s:gsub('%}%^ +%|\n', '{MATCH:[ ^]*}}{MATCH:[ ^]*}|\n')
end
screen:expect(s)
end
-- The value of has("gui_running") should be 0 before and after :restart.
local function assert_no_gui_running()
tt.feed_data(':echo "GUI Running: " .. has("gui_running")\013')
screen:expect({ any = 'GUI Running: 0' })
end
local s0 = [[
^ |
{100:~ }|*3
{3:[No Name] }|
{MATCH:%d+ +}|
{5:-- TERMINAL --} |
]]
screen_expect(s0)
assert_no_gui_running()
local server_session = n.connect(server_pipe)
local _, server_pid = server_session:request('nvim_call_function', 'getpid', {})
local function assert_new_pid()
server_session:close()
server_session = n.connect(server_pipe)
local _, new_pid = server_session:request('nvim_call_function', 'getpid', {})
t.neq(server_pid, new_pid)
server_pid = new_pid
end
--- Gets the last `argn` items in v:argv as a joined string.
local function get_argv(argn)
local argv = ({ server_session:request('nvim_eval', 'v:argv') })[2] --[[@type table]]
return table.concat(argv, ' ', #argv - argn, #argv)
end
local s1 = [[
|
^Hello1 |
{100:~ }|*2
{3:[No Name] [+] }|
{MATCH:%d+ +}|
{5:-- TERMINAL --} |
]]
tt.feed_data(':set nomodified\013')
-- Command is added as "-c" arg.
tt.feed_data(":restart put ='Hello1'\013")
screen_expect(s1)
tt.feed_data('\013')
assert_new_pid()
assert_no_gui_running()
eq("--cmd echo getpid() +::: -c put ='Hello1'", get_argv(4))
-- Complex command following +cmd.
tt.feed_data(":restart +qall! put ='Hello2' | put ='World2'\013")
screen_expect([[
|
Hello2 |
^World2 |
{100:~ }|
{3:[No Name] [+] }|
{MATCH:%d+ +}|
{5:-- TERMINAL --} |
]])
assert_new_pid()
assert_no_gui_running()
eq("--cmd echo getpid() +::: -c put ='Hello2' | put ='World2'", get_argv(4))
-- Check ":restart" on an unmodified buffer.
tt.feed_data(':set nomodified\013')
tt.feed_data(':restart\013')
screen_expect(s0)
assert_new_pid()
assert_no_gui_running()
-- Check ":restart +qall!" on an unmodified buffer.
tt.feed_data(':restart +qall!\013')
screen_expect(s0)
assert_new_pid()
assert_no_gui_running()
eq('--cmd echo getpid()', get_argv(1))
-- Check ":restart +echo" cannot restart server.
tt.feed_data(':restart +echo\013')
screen:expect({ any = vim.pesc('+cmd did not quit the server') })
tt.feed_data('ithis will be removed\027')
screen_expect([[
this will be remove^d |
{100:~ }|*3
{3:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
-- Check ":confirm restart" on a modified buffer.
tt.feed_data(':confirm restart\013')
screen:expect({ any = vim.pesc('Save changes to "Untitled"?') })
-- Cancel the operation (abandons restart).
tt.feed_data('C\013')
screen:expect({ any = vim.pesc('[No Name]') })
-- Check :restart respects 'confirm' option.
tt.feed_data(':set confirm\013')
tt.feed_data(':restart\013')
screen:expect({ any = vim.pesc('Save changes to "Untitled"?') })
tt.feed_data('C\013')
screen:expect({ any = vim.pesc('[No Name]') })
tt.feed_data(':set noconfirm\013')
-- Check ":confirm restart <cmd>" on a modified buffer.
tt.feed_data(":confirm restart put ='Hello3'\013")
screen:expect({ any = vim.pesc('Save changes to "Untitled"?') })
tt.feed_data('N\013')
screen:expect({ any = '%^Hello3' })
assert_new_pid()
assert_no_gui_running()
eq("--cmd echo getpid() +::: -c put ='Hello3'", get_argv(4))
-- Check ":confirm restart +echo" correctly ignores ":confirm"
tt.feed_data(':confirm restart +echo\013')
screen:expect({ any = vim.pesc('+cmd did not quit the server') })
-- Check ":restart" on a modified buffer.
tt.feed_data('ithis will be removed\027')
tt.feed_data(':restart\013')
screen:expect({ any = vim.pesc('Vim(qall):E37: No write since last change') })
-- Check ":restart +qall!" on a modified buffer.
tt.feed_data('ithis will be removed\027')
tt.feed_data(':restart +qall!\013')
screen_expect(s0)
assert_new_pid()
assert_no_gui_running()
-- No --listen conflict when server exit is delayed.
feed_data(':lua vim.schedule(function() vim.wait(100) end); vim.cmd.restart()\n')
screen_expect(s0)
assert_new_pid()
assert_no_gui_running()
screen:try_resize(60, 6)
screen_expect([[
^ |
{100:~ }|*2
{3:[No Name] }|
|
{5:-- TERMINAL --} |
]])
--- Check that ":restart" uses the updated size after terminal resize.
tt.feed_data(':restart\013')
screen_expect([[
^ |
{100:~ }|*2
{3:[No Name] }|
{MATCH:%d+ +}|
{5:-- TERMINAL --} |
]])
assert_new_pid()
assert_no_gui_running()
end)
it('drops "-" and "-- [files…]" from v:argv #34417', function()
t.skip(is_os('win'), 'stdin behavior differs on Windows')
clear()
local server_session
finally(function()
if server_session then
server_session:close()
end
n.check_close()
end)
local server_pipe = new_pipename()
local screen = tt.setup_child_nvim({
'-u',
'NONE',
'-i',
'NONE',
'--listen',
server_pipe,
'--cmd',
'set notermguicolors',
'-s',
'-',
'-',
'--',
'Xtest-file1',
'Xtest-file2',
})
screen:expect([[
^ |
~ |*3
{2:Xtest-file1 0,0-1 All}|
|
{5:-- TERMINAL --} |
]])
server_session = n.connect(server_pipe)
local expr = 'index(v:argv, "-") >= 0 || index(v:argv, "--") >= 0 ? v:true : v:false'
local has_s = 'index(v:argv, "-s") >= 0 ? v:true : v:false'
eq({ true, true }, { server_session:request('nvim_eval', expr) })
eq({ true, true }, { server_session:request('nvim_eval', has_s) })
tt.feed_data(":restart put='foo'\013")
screen:expect([[
|
^foo |
~ |*2
{2:[No Name] [+] 2,1 All}|
|
{5:-- TERMINAL --} |
]])
server_session:close()
server_session = n.connect(server_pipe)
eq({ true, false }, { server_session:request('nvim_eval', expr) })
eq({ true, false }, { server_session:request('nvim_eval', has_s) })
local argv = ({ server_session:request('nvim_eval', 'v:argv') })[2] --[[@type table]]
eq(13, #argv)
eq("-c put='foo'", table.concat(argv, ' ', #argv - 1, #argv))
end)
end)
describe('TUI :connect', function()
if t.skip(is_os('win'), "relies on :detach which currently doesn't work on windows") then
return
end
local screen_empty = [[
^ |
{100:~ }|*5
|
]]
it('leaves the current server running', function()
n.clear()
finally(function()
n.check_close()
end)
local server1 = new_pipename()
local screen1 = tt.setup_child_nvim({ '--listen', server1, '--clean' })
screen1:expect({ any = vim.pesc('[No Name]') })
tt.feed_data(':connect\013')
screen1:expect({ any = 'E471: Argument required' })
tt.feed_data('iThis is server 1.\027')
screen1:expect({ any = vim.pesc('This is server 1^.') })
-- Prevent screen2 from receiving the old terminal state.
command('enew')
screen1:expect(screen_empty)
screen1:detach()
local server2 = new_pipename()
local screen2 = tt.setup_child_nvim({ '--listen', server2, '--clean' })
screen2:expect({ any = vim.pesc('[No Name]') })
tt.feed_data('iThis is server 2.\027')
screen2:expect({ any = vim.pesc('This is server 2^.') })
tt.feed_data(':connect ' .. server1 .. '\013')
screen2:expect({ any = vim.pesc('This is server 1^.') })
local server1_session = n.connect(server1)
server1_session:request('nvim_command', 'qall!')
screen2:expect({ any = vim.pesc('[Process exited 0]') })
screen2:detach()
local server2_session = n.connect(server2)
local screen3 = tt.setup_child_nvim({ '--remote-ui', '--server', server2 })
screen3:expect({ any = vim.pesc('This is server 2^.') })
screen3:detach()
server2_session:request('nvim_command', 'qall!')
end)
it('! stops the current server', function()
n.clear()
finally(function()
n.check_close()
end)
local server1 = new_pipename()
local screen1 = tt.setup_child_nvim({ '--listen', server1, '--clean' })
screen1:expect({ any = vim.pesc('[No Name]') })
tt.feed_data('iThis is server 1.\027')
screen1:expect({ any = vim.pesc('This is server 1^.') })
-- Prevent screen2 from receiving the old terminal state.
command('enew')
screen1:expect(screen_empty)
screen1:detach()
local server2 = new_pipename()
local screen2 = tt.setup_child_nvim({ '--listen', server2, '--clean' })
screen2:expect({ any = vim.pesc('[No Name]') })
tt.feed_data(':connect! ' .. server1 .. '\013')
screen2:expect({ any = vim.pesc('This is server 1^.') })
retry(nil, nil, function()
eq(nil, vim.uv.fs_stat(server2))
end)
local server1_session = n.connect(server1)
server1_session:request('nvim_command', 'qall!')
screen2:expect({ any = vim.pesc('[Process exited 0]') })
screen2:detach()
end)
end)
describe('TUI', function()
local screen --[[@type test.functional.ui.screen]]
local child_session --[[@type test.Session]]
local child_exec_lua --[[@type fun(code: string, ...):any]]
before_each(function()
clear()
local child_server = new_pipename()
screen = tt.setup_child_nvim({
'--listen',
child_server,
'--clean',
'--cmd',
nvim_set .. ' laststatus=2 background=dark',
'--cmd',
'colorscheme vim',
}, { env = env_notermguicolors })
screen:expect([[
^ |
{100:~ }|*3
{3:[No Name] }|
|
{5:-- TERMINAL --} |
]])
child_session = n.connect(child_server)
child_exec_lua = tt.make_lua_executor(child_session)
end)
-- Wait for mode in the child Nvim (avoid "typeahead race" #10826).
local function wait_for_mode(mode)
retry(nil, nil, function()
local _, m = child_session:request('nvim_get_mode')
eq(mode, m.mode)
end)
end
-- Assert buffer contents in the child Nvim.
local function expect_child_buf_lines(expected)
assert(type({}) == type(expected))
retry(nil, nil, function()
local _, buflines = child_session:request('nvim_buf_get_lines', 0, 0, -1, false)
eq(expected, buflines)
end)
end
-- Ensure both child client and child server have processed pending events.
local function poke_both_eventloop()
child_exec_lua([[
_G.termresponse = nil
vim.api.nvim_create_autocmd('TermResponse', {
once = true,
callback = function(ev) _G.termresponse = ev.data.sequence end,
})
]])
feed_data('\027P0$r\027\\')
retry(nil, nil, function()
eq('\027P0$r', child_exec_lua('return _G.termresponse'))
end)
end
it('rapid resize #7572 #7628', function()
-- Need buffer rows to provoke the behavior.
feed_data(':edit test/functional/fixtures/bigfile.txt\n')
screen:expect([[
^0000;<control>;Cc;0;BN;;;;;N;NULL;;;; |
0001;<control>;Cc;0;BN;;;;;N;START OF HEADING;;;; |
0002;<control>;Cc;0;BN;;;;;N;START OF TEXT;;;; |
0003;<control>;Cc;0;BN;;;;;N;END OF TEXT;;;; |
{3:test/functional/fixtures/bigfile.txt }|
:edit test/functional/fixtures/bigfile.txt |
{5:-- TERMINAL --} |
]])
command('call jobresize(b:terminal_job_id, 58, 9)')
command('call jobresize(b:terminal_job_id, 62, 13)')
command('call jobresize(b:terminal_job_id, 100, 42)')
command('call jobresize(b:terminal_job_id, 37, 1000)')
-- Resize to <5 columns.
screen:try_resize(4, 44)
command('call jobresize(b:terminal_job_id, 4, 1000)')
-- Resize to 1 row, then to 1 column, then increase rows to 4.
screen:try_resize(44, 1)
command('call jobresize(b:terminal_job_id, 44, 1)')
screen:try_resize(1, 1)
command('call jobresize(b:terminal_job_id, 1, 1)')
screen:try_resize(1, 4)
command('call jobresize(b:terminal_job_id, 1, 4)')
screen:try_resize(57, 17)
command('call jobresize(b:terminal_job_id, 57, 17)')
retry(nil, nil, function()
eq({ true, 57 }, { child_session:request('nvim_win_get_width', 0) })
end)
end)
it('accepts resize while pager is active', function()
t.skip(is_os('win'), 'FIXME: some spaces have wrong attrs on Windows')
child_session:request(
'nvim_exec2',
[[
set more
func! ManyErr()
for i in range(20)
echoerr "FAIL ".i
endfor
endfunc
]],
{}
)
feed_data(':call ManyErr()\r')
screen:expect([[
{101:Error in function ManyErr:} |
{103:line 2:} |
{101:FAIL 0} |
{101:FAIL 1} |
{101:FAIL 2} |
{102:-- More --}^ |
{5:-- TERMINAL --} |
]])
screen:try_resize(50, 10)
screen:expect([[
:call ManyErr() |
{101:Error in function ManyErr:} |
{103:line 2:} |
{101:FAIL 0} |
{101:FAIL 1} |
{101:FAIL 2} |
|*2
{102:-- More --}^ |
{5:-- TERMINAL --} |
]])
feed_data('j')
screen:expect([[
{101:Error in function ManyErr:} |
{103:line 2:} |
{101:FAIL 0} |
{101:FAIL 1} |
{101:FAIL 2} |
{101:FAIL 3} |
{101:FAIL 4} |
{101:FAIL 5} |
{102:-- More --}^ |
{5:-- TERMINAL --} |
]])
screen:try_resize(50, 7)
screen:expect([[
{101:FAIL 1} |
{101:FAIL 2} |
{101:FAIL 3} |
{101:FAIL 4} |
{101:FAIL 5} |
{102:-- More --}^ |
{5:-- TERMINAL --} |
]])
screen:try_resize(50, 5)
screen:expect([[
{101:FAIL 3} |
{101:FAIL 4} |
{101:FAIL 5} |
{102:-- More --}^ |
{5:-- TERMINAL --} |
]])
feed_data('g')
screen:expect([[
:call ManyErr() |
{101:Error in function ManyErr:} |
{103:line 2:} |
{102:-- More --}^ |
{5:-- TERMINAL --} |
]])
screen:try_resize(50, 10)
screen:expect([[
:call ManyErr() |
{101:Error in function ManyErr:} |
{103:line 2:} |
{101:FAIL 0} |
{101:FAIL 1} |
{101:FAIL 2} |
{101:FAIL 3} |
{101:FAIL 4} |
{102:-- More --}^ |
{5:-- TERMINAL --} |
]])
feed_data('\003')
screen:expect([[
^ |
{100:~ }|*6
{3:[No Name] }|
|
{5:-- TERMINAL --} |
]])
end)
it('accepts basic utf-8 input', function()
feed_data('iabc\ntest1\ntest2')
screen:expect([[
abc |
test1 |
test2^ |
{100:~ }|
{3:[No Name] [+] }|
{5:-- INSERT --} |
{5:-- TERMINAL --} |
]])
feed_data('\027')
screen:expect([[
abc |
test1 |
test^2 |
{100:~ }|
{3:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
end)
it('interprets leading <Esc> byte as ALT modifier in normal-mode', function()
local keys = 'dfghjkl'
for c in keys:gmatch('.') do
feed_data(':nnoremap <a-' .. c .. '> ialt-' .. c .. '<cr><esc>\r')
feed_data('\027' .. c)
end
screen:expect([[
alt-j |
alt-k |
alt-l |
^ |
{3:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
feed_data('gg')
screen:expect([[
^alt-d |
alt-f |
alt-g |
alt-h |
{3:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
end)
it('interprets ESC+key as ALT chord in i_CTRL-V', function()
-- Vim represents ALT/META by setting the "high bit" of the modified key:
-- ALT+j inserts "ê". Nvim does not (#3982).
feed_data('i\022\027j')
screen:expect([[
<M-j>^ |
{100:~ }|*3
{3:[No Name] [+] }|
{5:-- INSERT --} |
{5:-- TERMINAL --} |
]])
end)
it('interprets <Esc> encoded with kitty keyboard protocol', function()
child_session:request(
'nvim_exec2',
[[
nnoremap <M-;> <Nop>
nnoremap <Esc> AESC<Esc>
nnoremap <C-Esc> ACtrlEsc<Esc>
nnoremap <D-Esc> ASuperEsc<Esc>
nnoremap ; Asemicolon<Esc>
]],
{}
)
-- Works with no modifier
feed_data('\027[27u;')
expect_child_buf_lines({ 'ESCsemicolon' })
-- Works with Ctrl modifier
feed_data('\027[27;5u')
expect_child_buf_lines({ 'ESCsemicolonCtrlEsc' })
-- Works with Super modifier
feed_data('\027[27;9u')
expect_child_buf_lines({ 'ESCsemicolonCtrlEscSuperEsc' })
-- Works with NumLock modifier (which should be the same as no modifier) #33799
feed_data('\027[27;129u')
expect_child_buf_lines({ 'ESCsemicolonCtrlEscSuperEscESC' })
screen:expect([[
ESCsemicolonCtrlEscSuperEscES^C |
{100:~ }|*3
{3:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
-- <Esc>; should be recognized as <M-;> when <M-;> is mapped
feed_data('\027;')
screen:expect_unchanged()
expect_child_buf_lines({ 'ESCsemicolonCtrlEscSuperEscESC' })
end)
it('interprets <Esc><Nul> as <M-C-Space> #17198', function()
t.skip(is_os('win'), 'FIXME: does not work on Windows')
feed_data('i\022\027\000')
screen:expect([[
<M-C-Space>^ |
{100:~ }|*3
{3:[No Name] [+] }|
{5:-- INSERT --} |
{5:-- TERMINAL --} |
]])
end)
it("split sequences work within 'ttimeoutlen' time", function()
poke_both_eventloop() -- Make sure startup requests have finished.
child_session:request('nvim_set_option_value', 'ttimeoutlen', 250, {})
feed_data('i')
screen:expect([[
^ |
{100:~ }|*3
{3:[No Name] }|
{5:-- INSERT --} |
{5:-- TERMINAL --} |
]])
-- Split UTF-8 '⌂' character
feed_data('\226')
screen:expect_unchanged(false, 25)
feed_data('\140')
screen:expect_unchanged(false, 25)
feed_data('\130')
screen:expect([[
⌂^ |
{100:~ }|*3
{3:[No Name] [+] }|
{5:-- INSERT --} |
{5:-- TERMINAL --} |
]])
-- Split CSI u escape sequence for Ctrl-X
feed_data('\027')
screen:expect_unchanged(false, 25)
feed_data('[')
screen:expect_unchanged(false, 25)
feed_data('120;')
screen:expect_unchanged(false, 25)
feed_data('5u')
screen:expect([[
⌂^ |
{100:~ }|*3
{3:[No Name] [+] }|
{5:-- ^X mode (^]^D^E^F^I^K^L^N^O^P^Rs^U^V^Y)} |
{5:-- TERMINAL --} |
]])
-- <Esc> is sent after 'ttimeoutlen' exceeds.
feed_data('\027')
screen:expect_unchanged(false, 25)
vim.uv.sleep(225)
screen:expect([[
^⌂ |
{100:~ }|*3
{3:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
end)
it('accepts ASCII control sequences', function()
feed_data('i')
feed_data('\022\007') -- ctrl+g
feed_data('\022\022') -- ctrl+v
feed_data('\022\013') -- ctrl+m
screen:expect([[
{104:^G^V^M}^ |
{100:~ }|*3
{3:[No Name] [+] }|
{5:-- INSERT --} |
{5:-- TERMINAL --} |
]])
child_session:request('nvim_set_keymap', 'i', '\031', '!!!', {})
feed_data('\031')
screen:expect([[
{104:^G^V^M}!!!^ |
{100:~ }|*3
{3:[No Name] [+] }|
{5:-- INSERT --} |
{5:-- TERMINAL --} |
]])
child_session:request('nvim_buf_delete', 0, { force = true })
child_session:request('nvim_set_option_value', 'laststatus', 0, {})
child_session:request(
'nvim_call_function',
'jobstart',
{ { testprg('shell-test'), 'INTERACT' }, { term = true } }
)
screen:expect([[
interact $ ^ |
|*4
{5:-- TERMINAL --} |*2
]])
-- mappings for C0 control codes should work in Terminal mode #33750
child_session:request('nvim_set_keymap', 't', '\031', '<Cmd>new<CR>', {})
feed_data('\031')
screen:expect([[
^ |
{100:~ }|
{3:[No Name] }|
interact $ |
|*2
{5:-- TERMINAL --} |
]])
end)
local function test_mouse_wheel(esc)
t.skip(is_os('win'), 'FIXME: some spaces have wrong attrs on Windows')
child_session:request(
'nvim_exec2',
[[
set number nostartofline nowrap mousescroll=hor:1,ver:1
call setline(1, repeat([join(range(10), '----')], 10))
vsplit
]],
{}
)
screen:expect([[
{103: 1 }^0----1----2----3----4│{103: 1 }0----1----2----3----|
{103: 2 }0----1----2----3----4│{103: 2 }0----1----2----3----|
{103: 3 }0----1----2----3----4│{103: 3 }0----1----2----3----|
{103: 4 }0----1----2----3----4│{103: 4 }0----1----2----3----|
{3:[No Name] [+] }{2:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
-- <ScrollWheelDown> in active window
if esc then
feed_data('\027[<65;8;1M')
else
api.nvim_input_mouse('wheel', 'down', '', 0, 0, 7)
end
screen:expect([[
{103: 2 }^0----1----2----3----4│{103: 1 }0----1----2----3----|
{103: 3 }0----1----2----3----4│{103: 2 }0----1----2----3----|
{103: 4 }0----1----2----3----4│{103: 3 }0----1----2----3----|
{103: 5 }0----1----2----3----4│{103: 4 }0----1----2----3----|
{3:[No Name] [+] }{2:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
-- <ScrollWheelDown> in inactive window
if esc then
feed_data('\027[<65;48;1M')
else
api.nvim_input_mouse('wheel', 'down', '', 0, 0, 47)
end
screen:expect([[
{103: 2 }^0----1----2----3----4│{103: 2 }0----1----2----3----|
{103: 3 }0----1----2----3----4│{103: 3 }0----1----2----3----|
{103: 4 }0----1----2----3----4│{103: 4 }0----1----2----3----|
{103: 5 }0----1----2----3----4│{103: 5 }0----1----2----3----|
{3:[No Name] [+] }{2:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
-- <ScrollWheelRight> in active window
if esc then
feed_data('\027[<67;8;1M')
else
api.nvim_input_mouse('wheel', 'right', '', 0, 0, 7)
end
screen:expect([[
{103: 2 }^----1----2----3----4-│{103: 2 }0----1----2----3----|
{103: 3 }----1----2----3----4-│{103: 3 }0----1----2----3----|
{103: 4 }----1----2----3----4-│{103: 4 }0----1----2----3----|
{103: 5 }----1----2----3----4-│{103: 5 }0----1----2----3----|
{3:[No Name] [+] }{2:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
-- <ScrollWheelRight> in inactive window
if esc then
feed_data('\027[<67;48;1M')
else
api.nvim_input_mouse('wheel', 'right', '', 0, 0, 47)
end
screen:expect([[
{103: 2 }^----1----2----3----4-│{103: 2 }----1----2----3----4|
{103: 3 }----1----2----3----4-│{103: 3 }----1----2----3----4|
{103: 4 }----1----2----3----4-│{103: 4 }----1----2----3----4|
{103: 5 }----1----2----3----4-│{103: 5 }----1----2----3----4|
{3:[No Name] [+] }{2:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
-- <S-ScrollWheelDown> in active window
if esc then
feed_data('\027[<69;8;1M')
else
api.nvim_input_mouse('wheel', 'down', 'S', 0, 0, 7)
end
screen:expect([[
{103: 5 }^----1----2----3----4-│{103: 2 }----1----2----3----4|
{103: 6 }----1----2----3----4-│{103: 3 }----1----2----3----4|
{103: 7 }----1----2----3----4-│{103: 4 }----1----2----3----4|
{103: 8 }----1----2----3----4-│{103: 5 }----1----2----3----4|
{3:[No Name] [+] }{2:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
-- <S-ScrollWheelDown> in inactive window
if esc then
feed_data('\027[<69;48;1M')
else
api.nvim_input_mouse('wheel', 'down', 'S', 0, 0, 47)
end
screen:expect([[
{103: 5 }^----1----2----3----4-│{103: 5 }----1----2----3----4|
{103: 6 }----1----2----3----4-│{103: 6 }----1----2----3----4|
{103: 7 }----1----2----3----4-│{103: 7 }----1----2----3----4|
{103: 8 }----1----2----3----4-│{103: 8 }----1----2----3----4|
{3:[No Name] [+] }{2:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
-- <S-ScrollWheelRight> in active window
if esc then
feed_data('\027[<71;8;1M')
else
api.nvim_input_mouse('wheel', 'right', 'S', 0, 0, 7)
end
screen:expect([[
{103: 5 }^----6----7----8----9 │{103: 5 }----1----2----3----4|
{103: 6 }----6----7----8----9 │{103: 6 }----1----2----3----4|
{103: 7 }----6----7----8----9 │{103: 7 }----1----2----3----4|
{103: 8 }----6----7----8----9 │{103: 8 }----1----2----3----4|
{3:[No Name] [+] }{2:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
-- <S-ScrollWheelRight> in inactive window
if esc then
feed_data('\027[<71;48;1M')
else
api.nvim_input_mouse('wheel', 'right', 'S', 0, 0, 47)
end
screen:expect([[
{103: 5 }^----6----7----8----9 │{103: 5 }5----6----7----8----|
{103: 6 }----6----7----8----9 │{103: 6 }5----6----7----8----|
{103: 7 }----6----7----8----9 │{103: 7 }5----6----7----8----|
{103: 8 }----6----7----8----9 │{103: 8 }5----6----7----8----|
{3:[No Name] [+] }{2:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
-- <ScrollWheelUp> in active window
if esc then
feed_data('\027[<64;8;1M')
else
api.nvim_input_mouse('wheel', 'up', '', 0, 0, 7)
end
screen:expect([[
{103: 4 }----6----7----8----9 │{103: 5 }5----6----7----8----|
{103: 5 }^----6----7----8----9 │{103: 6 }5----6----7----8----|
{103: 6 }----6----7----8----9 │{103: 7 }5----6----7----8----|
{103: 7 }----6----7----8----9 │{103: 8 }5----6----7----8----|
{3:[No Name] [+] }{2:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
-- <ScrollWheelUp> in inactive window
if esc then
feed_data('\027[<64;48;1M')
else
api.nvim_input_mouse('wheel', 'up', '', 0, 0, 47)
end
screen:expect([[
{103: 4 }----6----7----8----9 │{103: 4 }5----6----7----8----|
{103: 5 }^----6----7----8----9 │{103: 5 }5----6----7----8----|
{103: 6 }----6----7----8----9 │{103: 6 }5----6----7----8----|
{103: 7 }----6----7----8----9 │{103: 7 }5----6----7----8----|
{3:[No Name] [+] }{2:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
-- <ScrollWheelLeft> in active window
if esc then
feed_data('\027[<66;8;1M')
else
api.nvim_input_mouse('wheel', 'left', '', 0, 0, 7)
end
screen:expect([[
{103: 4 }5----6----7----8----9│{103: 4 }5----6----7----8----|
{103: 5 }5^----6----7----8----9│{103: 5 }5----6----7----8----|
{103: 6 }5----6----7----8----9│{103: 6 }5----6----7----8----|
{103: 7 }5----6----7----8----9│{103: 7 }5----6----7----8----|
{3:[No Name] [+] }{2:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
-- <ScrollWheelLeft> in inactive window
if esc then
feed_data('\027[<66;48;1M')
else
api.nvim_input_mouse('wheel', 'left', '', 0, 0, 47)
end
screen:expect([[
{103: 4 }5----6----7----8----9│{103: 4 }-5----6----7----8---|
{103: 5 }5^----6----7----8----9│{103: 5 }-5----6----7----8---|
{103: 6 }5----6----7----8----9│{103: 6 }-5----6----7----8---|
{103: 7 }5----6----7----8----9│{103: 7 }-5----6----7----8---|
{3:[No Name] [+] }{2:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
-- <S-ScrollWheelUp> in active window
if esc then
feed_data('\027[<68;8;1M')
else
api.nvim_input_mouse('wheel', 'up', 'S', 0, 0, 7)
end
screen:expect([[
{103: 1 }5----6----7----8----9│{103: 4 }-5----6----7----8---|
{103: 2 }5----6----7----8----9│{103: 5 }-5----6----7----8---|
{103: 3 }5----6----7----8----9│{103: 6 }-5----6----7----8---|
{103: 4 }5^----6----7----8----9│{103: 7 }-5----6----7----8---|
{3:[No Name] [+] }{2:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
-- <S-ScrollWheelUp> in inactive window
if esc then
feed_data('\027[<68;48;1M')
else
api.nvim_input_mouse('wheel', 'up', 'S', 0, 0, 47)
end
screen:expect([[
{103: 1 }5----6----7----8----9│{103: 1 }-5----6----7----8---|
{103: 2 }5----6----7----8----9│{103: 2 }-5----6----7----8---|
{103: 3 }5----6----7----8----9│{103: 3 }-5----6----7----8---|
{103: 4 }5^----6----7----8----9│{103: 4 }-5----6----7----8---|
{3:[No Name] [+] }{2:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
-- <S-ScrollWheelLeft> in active window
if esc then
feed_data('\027[<70;8;1M')
else
api.nvim_input_mouse('wheel', 'left', 'S', 0, 0, 7)
end
screen:expect([[
{103: 1 }0----1----2----3----4│{103: 1 }-5----6----7----8---|
{103: 2 }0----1----2----3----4│{103: 2 }-5----6----7----8---|
{103: 3 }0----1----2----3----4│{103: 3 }-5----6----7----8---|
{103: 4 }0----1----2----3----^4│{103: 4 }-5----6----7----8---|
{3:[No Name] [+] }{2:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
-- <S-ScrollWheelLeft> in inactive window
if esc then
feed_data('\027[<70;48;1M')
else
api.nvim_input_mouse('wheel', 'left', 'S', 0, 0, 47)
end
screen:expect([[
{103: 1 }0----1----2----3----4│{103: 1 }0----1----2----3----|
{103: 2 }0----1----2----3----4│{103: 2 }0----1----2----3----|
{103: 3 }0----1----2----3----4│{103: 3 }0----1----2----3----|
{103: 4 }0----1----2----3----^4│{103: 4 }0----1----2----3----|
{3:[No Name] [+] }{2:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
end
describe('accepts mouse wheel events', function()
it('(mouse events sent to host)', function()
test_mouse_wheel(false)
end)
it('(escape sequences sent to child)', function()
test_mouse_wheel(true)
end)
end)
local function test_mouse_popup(esc)
child_session:request(
'nvim_exec2',
[[
call setline(1, 'popup menu test')
set mouse=a mousemodel=popup
aunmenu PopUp
" Delete the default MenuPopup event handler.
autocmd! nvim.popupmenu
menu PopUp.foo :let g:menustr = 'foo'<CR>
menu PopUp.bar :let g:menustr = 'bar'<CR>
menu PopUp.baz :let g:menustr = 'baz'<CR>
highlight Pmenu ctermbg=NONE ctermfg=NONE cterm=underline,reverse
highlight PmenuSel ctermbg=NONE ctermfg=NONE cterm=underline,reverse,bold
]],
{}
)
if esc then
feed_data('\027[<2;5;1M')
else
api.nvim_input_mouse('right', 'press', '', 0, 0, 4)
end
screen:expect([[
^popup menu test |
{100:~ }{105: foo }{100: }|
{100:~ }{105: bar }{100: }|
{100:~ }{105: baz }{100: }|
{3:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
if esc then
feed_data('\027[<2;5;1m')
else
api.nvim_input_mouse('right', 'release', '', 0, 0, 4)
end
screen:expect_unchanged()
if esc then
feed_data('\027[<64;5;1M')
else
api.nvim_input_mouse('wheel', 'up', '', 0, 0, 4)
end
screen:expect([[
^popup menu test |
{100:~ }{106: foo }{100: }|
{100:~ }{105: bar }{100: }|
{100:~ }{105: baz }{100: }|
{3:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
if esc then
feed_data('\027[<35;7;4M')
else
api.nvim_input_mouse('move', '', '', 0, 3, 6)
end
screen:expect([[
^popup menu test |
{100:~ }{105: foo }{100: }|
{100:~ }{105: bar }{100: }|
{100:~ }{106: baz }{100: }|
{3:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
if esc then
feed_data('\027[<65;7;4M')
else
api.nvim_input_mouse('wheel', 'down', '', 0, 3, 6)
end
screen:expect([[
^popup menu test |
{100:~ }{105: foo }{100: }|
{100:~ }{106: bar }{100: }|
{100:~ }{105: baz }{100: }|
{3:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
if esc then
feed_data('\027[<0;7;3M')
else
api.nvim_input_mouse('left', 'press', '', 0, 2, 6)
end
screen:expect([[
^popup menu test |
{100:~ }|*3
{3:[No Name] [+] }|
:let g:menustr = 'bar' |
{5:-- TERMINAL --} |
]])
if esc then
feed_data('\027[<0;7;3m')
else
api.nvim_input_mouse('left', 'release', '', 0, 2, 6)
end
screen:expect_unchanged()
if esc then
feed_data('\027[<2;45;3M')
else
api.nvim_input_mouse('right', 'press', '', 0, 2, 44)
end
screen:expect([[
^popup menu test |
{100:~ }|*2
{100:~ }{105: foo }{100: }|
{3:[No Name] [+] }{105: bar }{3: }|
:let g:menustr = 'bar' {105: baz } |
{5:-- TERMINAL --} |
]])
if esc then
feed_data('\027[<34;48;6M')
else
api.nvim_input_mouse('right', 'drag', '', 0, 5, 47)
end
screen:expect([[
^popup menu test |
{100:~ }|*2
{100:~ }{105: foo }{100: }|
{3:[No Name] [+] }{105: bar }{3: }|
:let g:menustr = 'bar' {106: baz } |
{5:-- TERMINAL --} |
]])
if esc then
feed_data('\027[<2;48;6m')
else
api.nvim_input_mouse('right', 'release', '', 0, 5, 47)
end
screen:expect([[
^popup menu test |
{100:~ }|*3
{3:[No Name] [+] }|
:let g:menustr = 'baz' |
{5:-- TERMINAL --} |
]])
end
describe('mouse events work with right-click menu', function()
it('(mouse events sent to host)', function()
test_mouse_popup(false)
end)
it('(escape sequences sent to child)', function()
test_mouse_popup(true)
end)
end)
it('accepts keypad keys from kitty keyboard protocol #19180', function()
feed_data('i')
feed_data(fn.nr2char(57399)) -- KP_0
feed_data(fn.nr2char(57400)) -- KP_1
feed_data(fn.nr2char(57401)) -- KP_2
feed_data(fn.nr2char(57402)) -- KP_3
feed_data(fn.nr2char(57403)) -- KP_4
feed_data(fn.nr2char(57404)) -- KP_5
feed_data(fn.nr2char(57405)) -- KP_6
feed_data(fn.nr2char(57406)) -- KP_7
feed_data(fn.nr2char(57407)) -- KP_8
feed_data(fn.nr2char(57408)) -- KP_9
feed_data(fn.nr2char(57409)) -- KP_DECIMAL
feed_data(fn.nr2char(57410)) -- KP_DIVIDE
feed_data(fn.nr2char(57411)) -- KP_MULTIPLY
feed_data(fn.nr2char(57412)) -- KP_SUBTRACT
feed_data(fn.nr2char(57413)) -- KP_ADD
feed_data(fn.nr2char(57414)) -- KP_ENTER
feed_data(fn.nr2char(57415)) -- KP_EQUAL
screen:expect([[
0123456789./*-+ |
=^ |
{100:~ }|*2
{3:[No Name] [+] }|
{5:-- INSERT --} |
{5:-- TERMINAL --} |
]])
feed_data(fn.nr2char(57417)) -- KP_LEFT
screen:expect([[
0123456789./*-+ |
^= |
{100:~ }|*2
{3:[No Name] [+] }|
{5:-- INSERT --} |
{5:-- TERMINAL --} |
]])
feed_data(fn.nr2char(57418)) -- KP_RIGHT
screen:expect([[
0123456789./*-+ |
=^ |
{100:~ }|*2
{3:[No Name] [+] }|
{5:-- INSERT --} |
{5:-- TERMINAL --} |
]])
feed_data(fn.nr2char(57419)) -- KP_UP
screen:expect([[
0^123456789./*-+ |
= |
{100:~ }|*2
{3:[No Name] [+] }|
{5:-- INSERT --} |
{5:-- TERMINAL --} |
]])
feed_data(fn.nr2char(57420)) -- KP_DOWN
screen:expect([[
0123456789./*-+ |
=^ |
{100:~ }|*2
{3:[No Name] [+] }|
{5:-- INSERT --} |
{5:-- TERMINAL --} |
]])
feed_data(fn.nr2char(57425)) -- KP_INSERT
screen:expect([[
0123456789./*-+ |
=^ |
{100:~ }|*2
{3:[No Name] [+] }|
{5:-- REPLACE --} |
{5:-- TERMINAL --} |
]])
feed_data('\027[27u') -- ESC
screen:expect([[
0123456789./*-+ |
^= |
{100:~ }|*2
{3:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
feed_data('\027[57417;5u') -- CTRL + KP_LEFT
screen:expect([[
^0123456789./*-+ |
= |
{100:~ }|*2
{3:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
feed_data('\027[57418;2u') -- SHIFT + KP_RIGHT
screen:expect([[
0123456789^./*-+ |
= |
{100:~ }|*2
{3:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
feed_data(fn.nr2char(57426)) -- KP_DELETE
screen:expect([[
0123456789^/*-+ |
= |
{100:~ }|*2
{3:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
feed_data(fn.nr2char(57423)) -- KP_HOME
screen:expect([[
^0123456789/*-+ |
= |
{100:~ }|*2
{3:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
feed_data(fn.nr2char(57424)) -- KP_END
screen:expect([[
0123456789/*-^+ |
= |
{100:~ }|*2
{3:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
child_session:request(
'nvim_exec2',
[[
tab split
tabnew
highlight Tabline ctermbg=NONE ctermfg=NONE cterm=underline
]],
{}
)
screen:expect([[
{107: + [No Name] + [No Name] }{5: [No Name] }{2: }{107:X}|
^ |
{100:~ }|*2
{3:[No Name] }|
|
{5:-- TERMINAL --} |
]])
feed_data('\027[57421;5u') -- CTRL + KP_PAGE_UP
screen:expect([[
{107: + [No Name] }{5: + [No Name] }{107: [No Name] }{2: }{107:X}|
0123456789/*-^+ |
= |
{100:~ }|
{3:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
feed_data('\027[57422;5u') -- CTRL + KP_PAGE_DOWN
screen:expect([[
{107: + [No Name] + [No Name] }{5: [No Name] }{2: }{107:X}|
^ |
{100:~ }|*2
{3:[No Name] }|
|
{5:-- TERMINAL --} |
]])
end)
it('supports Super and Meta modifiers', function()
feed_data('i')
feed_data('\022\027[106;9u') -- Super + j
feed_data('\022\027[107;33u') -- Meta + k
feed_data('\022\027[13;41u') -- Super + Meta + Enter
feed_data('\022\027[127;48u') -- Shift + Alt + Ctrl + Super + Meta + Backspace
feed_data('\n')
feed_data('\022\027[57376;9u') -- Super + F13
feed_data('\022\027[57377;33u') -- Meta + F14
feed_data('\022\027[57378;41u') -- Super + Meta + F15
feed_data('\022\027[57379;48u') -- Shift + Alt + Ctrl + Super + Meta + F16
screen:expect([[
<D-j><T-k><T-D-CR><M-T-C-S-D-BS> |
<D-F13><T-F14><T-D-F15><M-T-C-S-D-F16>^ |
{100:~ }|*2
{3:[No Name] [+] }|
{5:-- INSERT --} |
{5:-- TERMINAL --} |
]])
end)
it('paste: Insert mode', function()
-- "bracketed paste"
feed_data('i""\027i\027[200~')
screen:expect([[
"^" |
{100:~ }|*3
{3:[No Name] [+] }|
{5:-- INSERT --} |
{5:-- TERMINAL --} |
]])
feed_data('pasted from terminal')
expect_child_buf_lines({ '"pasted from terminal"' })
screen:expect([[
"pasted from terminal^" |
{100:~ }|*3
{3:[No Name] [+] }|
{5:-- INSERT --} |
{5:-- TERMINAL --} |
]])
feed_data('\027[201~') -- End paste.
poke_both_eventloop()
screen:expect_unchanged()
feed_data('\027[27u') -- ESC: go to Normal mode.
wait_for_mode('n')
screen:expect([[
"pasted from termina^l" |
{100:~ }|*3
{3:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
-- Dot-repeat/redo.
feed_data('2.')
expect_child_buf_lines({ '"pasted from terminapasted from terminalpasted from terminall"' })
screen:expect([[
"pasted from terminapasted from terminalpasted fro|
m termina^ll" |
{100:~ }|*2
{3:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
-- Undo.
feed_data('u')
expect_child_buf_lines({ '"pasted from terminal"' })
feed_data('u')
expect_child_buf_lines({ '""' })
feed_data('u')
expect_child_buf_lines({ '' })
end)
it('paste: select-mode', function()
feed_data('ithis is line 1\nthis is line 2\nline 3 is here\n\027')
wait_for_mode('n')
screen:expect([[
this is line 1 |
this is line 2 |
line 3 is here |
^ |
{3:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
-- Select-mode. Use <C-n> to move down.
feed_data('gg04lgh\14\14')
screen:expect([[
this{108: is line 1} |
{108:this is line 2} |
{108:line}^ 3 is here |
|
{3:[No Name] [+] }|
{5:-- SELECT --} |
{5:-- TERMINAL --} |
]])
feed_data('\027[200~')
feed_data('just paste it™')
feed_data('\027[201~')
screen:expect([[
thisjust paste it^™3 is here |
|
{100:~ }|*2
{3:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
-- Undo.
feed_data('u')
expect_child_buf_lines {
'this is line 1',
'this is line 2',
'line 3 is here',
'',
}
-- Redo.
feed_data('\18') -- <C-r>
expect_child_buf_lines {
'thisjust paste it™3 is here',
'',
}
end)
it('paste: terminal mode', function()
if is_ci('github') then
pending('tty-test complains about not owning the terminal -- actions/runner#241')
end
child_exec_lua('vim.o.statusline="^^^^^^^"')
child_exec_lua('vim.cmd.terminal(...)', testprg('tty-test'))
feed_data('i')
screen:expect([[
tty ready |
^ |
|*2
{109:^^^^^^^ }|
{5:-- TERMINAL --} |*2
]])
feed_data('\027[200~')
feed_data('hallo')
feed_data('\027[201~')
screen:expect([[
tty ready |
hallo^ |
|*2
{109:^^^^^^^ }|
{5:-- TERMINAL --} |*2
]])
end)
it('paste: normal-mode (+CRLF #10872)', function()
t.skip(is_os('win'), 'FIXME: some spaces have wrong attrs on Windows')
feed_data(':set ruler | echo')
wait_for_mode('c')
feed_data('\n')
wait_for_mode('n')
local expected_lf = { 'line 1', 'ESC:\027 / CR: \rx' }
local expected_crlf = { 'line 1', 'ESC:\027 / CR: ', 'x' }
local expected_grid1 = [[
line 1 |
ESC:{104:^[} / CR: |
^x |
{100:~ }|
{3:[No Name] [+] 3,1 All}|
|
{5:-- TERMINAL --} |
]]
-- "bracketed paste"
feed_data('\027[200~' .. table.concat(expected_lf, '\n') .. '\027[201~')
screen:expect(expected_grid1)
-- Dot-repeat/redo.
feed_data('.')
local expected_grid2 = [[
ESC:{104:^[} / CR: |
xline 1 |
ESC:{104:^[} / CR: |
^x |
{3:[No Name] [+] 5,1 Bot}|
|
{5:-- TERMINAL --} |
]]
screen:expect(expected_grid2)
-- Undo.
feed_data('u')
expect_child_buf_lines(expected_crlf)
feed_data('u')
expect_child_buf_lines({ '' })
feed_data(':echo')
wait_for_mode('c')
feed_data('\n')
wait_for_mode('n')
-- CRLF input
feed_data('\027[200~' .. table.concat(expected_lf, '\r\n') .. '\027[201~')
screen:expect(expected_grid1)
expect_child_buf_lines(expected_crlf)
-- Dot-repeat/redo.
feed_data('.')
screen:expect(expected_grid2)
-- Undo.
feed_data('u')
expect_child_buf_lines(expected_crlf)
feed_data('u')
expect_child_buf_lines({ '' })
end)
it('paste: cmdline-mode inserts 1 line', function()
feed_data('ifoo\n') -- Insert some text (for dot-repeat later).
feed_data('\027:""') -- Enter Cmdline-mode.
feed_data('\027[D') -- <Left> to place cursor between quotes.
wait_for_mode('c')
screen:expect([[
foo |
|
{100:~ }|*2
{3:[No Name] [+] }|
:"^" |
{5:-- TERMINAL --} |
]])
-- "bracketed paste"
feed_data('\027[200~line 1\nline 2\n')
wait_for_mode('c')
feed_data('line 3\nline 4\n\027[201~')
poke_both_eventloop()
wait_for_mode('c')
screen:expect([[
foo |
|
{100:~ }|*2
{3:[No Name] [+] }|
:"line 1^" |
{5:-- TERMINAL --} |
]])
-- Dot-repeat/redo.
feed_data('\027[27u')
wait_for_mode('n')
feed_data('.')
screen:expect([[
foo |*2
^ |
{100:~ }|
{3:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
end)
it('paste: cmdline-mode collects chunks of unfinished line', function()
local function expect_cmdline(expected)
retry(nil, nil, function()
local _, cmdline = child_session:request('nvim_call_function', 'getcmdline', {})
eq(expected, cmdline)
local _, pos = child_session:request('nvim_call_function', 'getcmdpos', {})
eq(#expected, pos) -- Cursor is just before the last char.
end)
end
feed_data('\027:""') -- Enter Cmdline-mode.
feed_data('\027[D') -- <Left> to place cursor between quotes.
expect_cmdline('""')
feed_data('\027[200~stuff 1 ')
expect_cmdline('"stuff 1 "')
-- Discards everything after the first line.
feed_data('more\nstuff 2\nstuff 3\n')
expect_cmdline('"stuff 1 more"')
feed_data('stuff 3')
expect_cmdline('"stuff 1 more"')
-- End the paste sequence.
feed_data('\027[201~')
poke_both_eventloop()
expect_cmdline('"stuff 1 more"')
feed_data(' typed')
expect_cmdline('"stuff 1 more typed"')
end)
it('paste: recovers from vim.paste() failure', function()
child_exec_lua([[
_G.save_paste_fn = vim.paste
-- Stack traces for this test are non-deterministic, so disable them
_G.debug.traceback = function(msg) return msg end
vim.paste = function(lines, phase) error("fake fail") end
]])
-- Prepare something for dot-repeat/redo.
feed_data('ifoo\n\027[27u')
wait_for_mode('n')
screen:expect([[
foo |
^ |
{100:~ }|*2
{3:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
-- Start pasting...
feed_data('\027[200~line 1\nline 2\n')
screen:expect([[
foo |
^ |
{100:~ }|*2
{3:[No Name] [+] }|
{101:paste: Lua: [string "<nvim>"]:4: fake fail} |
{5:-- TERMINAL --} |
]])
-- Remaining chunks are discarded after vim.paste() failure.
feed_data('line 3\nline 4\n')
feed_data('line 5\nline 6\n')
feed_data('line 7\nline 8\n')
-- Stop paste.
feed_data('\027[201~')
screen:expect_unchanged()
feed_data('\n') -- <CR> to dismiss hit-enter prompt
expect_child_buf_lines({ 'foo', '' })
-- Dot-repeat/redo is not modified by failed paste.
feed_data('.')
screen:expect([[
foo |*2
^ |
{100:~ }|
{3:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
-- Editor should still work after failed/drained paste.
feed_data('ityped input...\027[27u')
screen:expect([[
foo |*2
typed input..^. |
{100:~ }|
{3:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
-- Paste works if vim.paste() succeeds.
child_exec_lua([[vim.paste = _G.save_paste_fn]])
feed_data('\027[200~line A\nline B\n\027[201~')
screen:expect([[
foo |
typed input...line A |
line B |
^ |
{3:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
end)
it('paste: vim.paste() cancel (retval=false) #10865', function()
-- This test only exercises the "cancel" case. Use-case would be "dangling
-- paste", but that is not implemented yet. #10865
child_exec_lua([[
vim.paste = function(lines, phase) return false end
]])
feed_data('\027[200~line A\nline B\n\027[201~')
expect_child_buf_lines({ '' })
feed_data('ifoo\n\027[27u')
expect_child_buf_lines({ 'foo', '' })
end)
it('paste: vim.paste() cancel (retval=false) with streaming #30462', function()
child_exec_lua([[
vim.paste = (function(overridden)
return function(lines, phase)
for i, line in ipairs(lines) do
if line:find('!') then
return false
end
end
return overridden(lines, phase)
end
end)(vim.paste)
]])
feed_data('A')
wait_for_mode('i')
feed_data('\027[200~aaa')
expect_child_buf_lines({ 'aaa' })
feed_data('bbb')
expect_child_buf_lines({ 'aaabbb' })
feed_data('ccc!') -- This chunk is cancelled.
expect_child_buf_lines({ 'aaabbb' })
feed_data('ddd\027[201~') -- This chunk is ignored.
poke_both_eventloop()
expect_child_buf_lines({ 'aaabbb' })
feed_data('\027[27u')
wait_for_mode('n')
feed_data('.') -- Dot-repeat only includes chunks actually pasted.
expect_child_buf_lines({ 'aaabbbaaabbb' })
feed_data('$\027[200~eee\027[201~') -- A following paste works normally.
expect_child_buf_lines({ 'aaabbbaaabbbeee' })
end)
it("paste: 'nomodifiable' buffer", function()
t.skip(is_os('win'), 'FIXME: some spaces have wrong attrs on Windows')
child_exec_lua([[
vim.bo.modifiable = false
-- Truncate the error message to hide the line number
_G.debug.traceback = function(msg) return msg:sub(-49) end
]])
feed_data('\027[200~fail 1\nfail 2\n\027[201~')
screen:expect([[
|
{100:~ }|
{3: }|
{101:paste: Lua: Vim:E21: Cannot make changes, 'modifia}|
{101:ble' is off} |
{102:Press ENTER or type command to continue}^ |
{5:-- TERMINAL --} |
]])
feed_data('\n') -- <Enter> to dismiss hit-enter prompt
child_exec_lua('vim.bo.modifiable = true')
feed_data('\027[200~success 1\nsuccess 2\n\027[201~')
screen:expect([[
success 1 |
success 2 |
^ |
{100:~ }|
{3:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
end)
it('paste: exactly 64 bytes #10311', function()
local expected = string.rep('z', 64)
feed_data('i')
wait_for_mode('i')
-- "bracketed paste"
feed_data('\027[200~' .. expected .. '\027[201~')
expect_child_buf_lines({ expected })
feed_data(' end')
expected = expected .. ' end'
screen:expect([[
zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz|
zzzzzzzzzzzzzz end^ |
{100:~ }|*2
{3:[No Name] [+] }|
{5:-- INSERT --} |
{5:-- TERMINAL --} |
]])
expect_child_buf_lines({ expected })
end)
it('paste: less-than sign in cmdline #11088', function()
local expected = '<'
feed_data(':')
wait_for_mode('c')
-- "bracketed paste"
feed_data('\027[200~' .. expected .. '\027[201~')
screen:expect([[
|
{100:~ }|*3
{3:[No Name] }|
:<^ |
{5:-- TERMINAL --} |
]])
end)
it('paste: big burst of input', function()
feed_data(':set ruler\n')
local q = {}
for i = 1, 3000 do
q[i] = 'item ' .. tostring(i)
end
feed_data('i')
wait_for_mode('i')
-- "bracketed paste"
feed_data('\027[200~' .. table.concat(q, '\n') .. '\027[201~')
expect_child_buf_lines(q)
feed_data(' end')
screen:expect([[
item 2997 |
item 2998 |
item 2999 |
item 3000 end^ |
{3:[No Name] [+] 3000,14 Bot}|
{5:-- INSERT --} |
{5:-- TERMINAL --} |
]])
feed_data('\027[27u') -- ESC: go to Normal mode.
wait_for_mode('n')
-- Dot-repeat/redo.
feed_data('.')
screen:expect([[
item 2997 |
item 2998 |
item 2999 |
item 3000 en^dd |
{3:[No Name] [+] 5999,13 Bot}|
|
{5:-- TERMINAL --} |
]])
end)
it('paste: forwards spurious "start paste" code', function()
-- If multiple "start paste" sequences are sent without a corresponding
-- "stop paste" sequence, only the first occurrence should be consumed.
feed_data('i')
wait_for_mode('i')
-- Send the "start paste" sequence.
feed_data('\027[200~')
feed_data('\npasted from terminal (1)\n')
-- Send spurious "start paste" sequence.
feed_data('\027[200~')
feed_data('\n')
-- Send the "stop paste" sequence.
feed_data('\027[201~')
screen:expect([[
|
pasted from terminal (1) |
{104:^[}[200~ |
^ |
{3:[No Name] [+] }|
{5:-- INSERT --} |
{5:-- TERMINAL --} |
]])
end)
it('paste: ignores spurious "stop paste" code', function()
-- If "stop paste" sequence is received without a preceding "start paste"
-- sequence, it should be ignored.
feed_data('i')
wait_for_mode('i')
-- Send "stop paste" sequence.
feed_data('\027[201~')
screen:expect([[
^ |
{100:~ }|*3
{3:[No Name] }|
{5:-- INSERT --} |
{5:-- TERMINAL --} |
]])
end)
it('paste: split "start paste" code', function()
t.skip(is_os('win'), 'FIXME: wrong behavior on Windows')
feed_data('i')
wait_for_mode('i')
-- Send split "start paste" sequence.
feed_data('\027[2')
feed_data('00~pasted from terminal\027[201~')
screen:expect([[
pasted from terminal^ |
{100:~ }|*3
{3:[No Name] [+] }|
{5:-- INSERT --} |
{5:-- TERMINAL --} |
]])
end)
it('paste: split "stop paste" code', function()
t.skip(is_os('win'), 'FIXME: wrong behavior on Windows')
feed_data('i')
wait_for_mode('i')
-- Send split "stop paste" sequence.
feed_data('\027[200~pasted from terminal\027[20')
feed_data('1~')
screen:expect([[
pasted from terminal^ |
{100:~ }|*3
{3:[No Name] [+] }|
{5:-- INSERT --} |
{5:-- TERMINAL --} |
]])
end)
it('paste: streamed paste with isolated "stop paste" code', function()
child_exec_lua([[
_G.paste_phases = {}
vim.paste = (function(overridden)
return function(lines, phase)
table.insert(_G.paste_phases, phase)
overridden(lines, phase)
end
end)(vim.paste)
]])
feed_data('i')
wait_for_mode('i')
feed_data('\027[200~pasted') -- phase 1
screen:expect([[
pasted^ |
{100:~ }|*3
{3:[No Name] [+] }|
{5:-- INSERT --} |
{5:-- TERMINAL --} |
]])
feed_data(' from terminal') -- phase 2
screen:expect([[
pasted from terminal^ |
{100:~ }|*3
{3:[No Name] [+] }|
{5:-- INSERT --} |
{5:-- TERMINAL --} |
]])
-- Send isolated "stop paste" sequence.
feed_data('\027[201~') -- phase 3
poke_both_eventloop()
screen:expect_unchanged()
local rv = child_exec_lua('return _G.paste_phases')
-- In rare cases there may be multiple chunks of phase 2 because of timing.
eq({ 1, 2, 3 }, { rv[1], rv[2], rv[#rv] })
end)
it('allows termguicolors to be set at runtime', function()
t.skip(is_os('win'), 'FIXME: some spaces have wrong attrs on Windows')
screen:set_option('rgb', true)
feed_data(':hi SpecialKey ctermfg=3 guifg=SeaGreen\n')
feed_data('i')
feed_data('\022\007') -- ctrl+g
feed_data('\028\014') -- crtl+\ ctrl+N
feed_data(':set termguicolors?\n')
screen:expect([[
{110:^^G} |
{111:~ }|*3
{3:[No Name] [+] }|
notermguicolors |
{5:-- TERMINAL --} |
]])
feed_data(':set termguicolors\n')
screen:expect([[
{113:^^G} |
{1:~}{18: }|*3
{3:[No Name] [+] }|
:set termguicolors |
{5:-- TERMINAL --} |
]])
feed_data(':set notermguicolors\n')
screen:expect([[
{110:^^G} |
{111:~ }|*3
{3:[No Name] [+] }|
:set notermguicolors |
{5:-- TERMINAL --} |
]])
end)
it('forwards :term palette colors with termguicolors', function()
if is_ci('github') then
pending('tty-test complains about not owning the terminal -- actions/runner#241')
end
screen:set_rgb_cterm(true)
screen:set_default_attr_ids({
[1] = { { reverse = true }, { reverse = true } },
[2] = {
{ bold = true, background = Screen.colors.LightGreen, foreground = Screen.colors.Black },
{ bold = true },
},
[3] = { { bold = true }, { bold = true } },
[4] = { { fg_indexed = true, foreground = tonumber('0xe0e000') }, { foreground = 3 } },
[5] = { { foreground = tonumber('0xff8000') }, {} },
[6] = {
{
fg_indexed = true,
bg_indexed = true,
bold = true,
background = tonumber('0x66ff99'),
foreground = Screen.colors.Black,
},
{ bold = true, background = 121, foreground = 0 },
},
[7] = {
{
fg_indexed = true,
bg_indexed = true,
background = tonumber('0x66ff99'),
foreground = Screen.colors.Black,
},
{ background = 121, foreground = 0 },
},
})
child_exec_lua('vim.o.statusline="^^^^^^^"')
child_exec_lua('vim.o.termguicolors=true')
child_exec_lua('vim.cmd.terminal(...)', testprg('tty-test'))
screen:expect([[
^tty ready |
|*3
{2:^^^^^^^ }|
|
{3:-- TERMINAL --} |
]])
feed_data(
':call chansend(&channel, "\\033[38;5;3mtext\\033[38:2:255:128:0mcolor\\033[0;10mtext")\n'
)
screen:expect([[
^tty ready |
{4:text}{5:color}text |
|*2
{2:^^^^^^^ }|
|
{3:-- TERMINAL --} |
]])
feed_data(':set notermguicolors\n')
screen:expect([[
^tty ready |
{4:text}colortext |
|*2
{6:^^^^^^^}{7: }|
:set notermguicolors |
{3:-- TERMINAL --} |
]])
end)
-- Note: libvterm doesn't support colored underline or undercurl.
it('supports undercurl and underdouble when run in :terminal', function()
child_session:request('nvim_set_hl', 0, 'Visual', { undercurl = true })
feed_data('ifoobar\027V')
screen:expect([[
{114:fooba}^r |
{100:~ }|*3
{3:[No Name] [+] }|
{5:-- VISUAL LINE --} |
{5:-- TERMINAL --} |
]])
child_session:request('nvim_set_hl', 0, 'Visual', { underdouble = true })
screen:expect([[
{115:fooba}^r |
{100:~ }|*3
{3:[No Name] [+] }|
{5:-- VISUAL LINE --} |
{5:-- TERMINAL --} |
]])
end)
it('in nvim_list_uis(), sets nvim_set_client_info()', function()
-- $TERM in :terminal.
local exp_term = (is_os('bsd') or is_os('win')) and 'xterm' or 'xterm-256color'
local ui_chan = 1
local expected = {
{
chan = ui_chan,
ext_cmdline = false,
ext_hlstate = false,
ext_linegrid = true,
ext_messages = false,
ext_multigrid = false,
ext_popupmenu = false,
ext_tabline = false,
ext_termcolors = true,
ext_wildmenu = false,
height = 6,
override = false,
rgb = false,
stdin_tty = true,
stdout_tty = true,
term_background = '',
term_colors = 256,
term_name = exp_term,
width = 50,
},
}
local _, rv = child_session:request('nvim_list_uis')
eq(expected, rv)
---@type table
local expected_version = child_exec_lua('return vim.version()')
-- vim.version() returns `prerelease` string. Coerce it to boolean.
expected_version.prerelease = not not expected_version.prerelease
local expected_chan_info = {
client = {
attributes = {
license = 'Apache 2',
-- pid = 5371,
website = 'https://neovim.io',
},
methods = {},
name = 'nvim-tui',
type = 'ui',
version = expected_version,
},
id = ui_chan,
mode = 'rpc',
stream = 'stdio',
}
local status, chan_info = child_session:request('nvim_get_chan_info', ui_chan)
ok(status)
local info = chan_info.client
ok(info.attributes.pid and info.attributes.pid > 0, 'PID', info.attributes.pid or 'nil')
ok(info.version.major >= 0)
ok(info.version.minor >= 0)
ok(info.version.patch >= 0)
-- Delete variable fields so we can deep-compare.
info.attributes.pid = nil
eq(expected_chan_info, chan_info)
end)
it('allows grid to assume wider ambiwidth chars than host terminal', function()
t.skip(is_os('win'), 'FIXME: some spaces have wrong attrs on Windows')
child_session:request(
'nvim_buf_set_lines',
0,
0,
-1,
true,
{ (''):rep(60), (''):rep(60) }
)
child_session:request('nvim_set_option_value', 'cursorline', true, {})
child_session:request('nvim_set_option_value', 'list', true, {})
child_session:request('nvim_set_option_value', 'listchars', 'eol:$', { win = 0 })
feed_data('gg')
local singlewidth_screen = [[
{107:^℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃}|
{107:℃℃℃℃℃℃℃℃℃℃}{116:$}{107: }|
℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃℃|
℃℃℃℃℃℃℃℃℃℃{100:$} |
{3:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]]
-- When grid assumes "℃" to be double-width but host terminal assumes it to be single-width,
-- the second cell of "℃" is a space and the attributes of the "℃" are applied to it.
local doublewidth_screen = [[
{107:^℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ }|
{107:℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ }|
{107:℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ }{116:$}{107: }|
℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ ℃ {100:@@@@}|
{3:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]]
screen:expect(singlewidth_screen)
child_session:request('nvim_set_option_value', 'ambiwidth', 'double', {})
screen:expect(doublewidth_screen)
child_session:request('nvim_set_option_value', 'ambiwidth', 'single', {})
screen:expect(singlewidth_screen)
child_session:request('nvim_call_function', 'setcellwidths', { { { 0x2103, 0x2103, 2 } } })
screen:expect(doublewidth_screen)
child_session:request('nvim_call_function', 'setcellwidths', { { { 0x2103, 0x2103, 1 } } })
screen:expect(singlewidth_screen)
end)
it('allows grid to assume wider non-ambiwidth chars than host terminal', function()
t.skip(is_os('win'), 'FIXME: some spaces have wrong attrs on Windows')
child_session:request(
'nvim_buf_set_lines',
0,
0,
-1,
true,
{ (''):rep(60), (''):rep(60) }
)
child_session:request('nvim_set_option_value', 'cursorline', true, {})
child_session:request('nvim_set_option_value', 'list', true, {})
child_session:request('nvim_set_option_value', 'listchars', 'eol:$', { win = 0 })
feed_data('gg')
local singlewidth_screen = [[
{107:^✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓}|
{107:✓✓✓✓✓✓✓✓✓✓}{116:$}{107: }|
✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓|
✓✓✓✓✓✓✓✓✓✓{100:$} |
{3:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]]
-- When grid assumes "✓" to be double-width but host terminal assumes it to be single-width,
-- the second cell of "✓" is a space and the attributes of the "✓" are applied to it.
local doublewidth_screen = [[
{107:^✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ }|
{107:✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ }|
{107:✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ }{116:$}{107: }|
✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ {100:@@@@}|
{3:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]]
screen:expect(singlewidth_screen)
child_session:request('nvim_set_option_value', 'ambiwidth', 'double', {})
screen:expect_unchanged()
child_session:request('nvim_call_function', 'setcellwidths', { { { 0x2713, 0x2713, 2 } } })
screen:expect(doublewidth_screen)
child_session:request('nvim_set_option_value', 'ambiwidth', 'single', {})
screen:expect_unchanged()
child_session:request('nvim_call_function', 'setcellwidths', { { { 0x2713, 0x2713, 1 } } })
screen:expect(singlewidth_screen)
end)
it('draws correctly when cursor_address overflows #21643', function()
screen:try_resize(70, 333)
retry(nil, nil, function()
eq({ true, 330 }, { child_session:request('nvim_win_get_height', 0) })
end)
child_session:request('nvim_set_option_value', 'cursorline', true, {})
-- Use full screen message so that redrawing afterwards is more deterministic.
child_session:notify('nvim_command', 'intro')
screen:expect({ any = 'Nvim is open source and freely distributable' })
-- Going to top-left corner needs 3 bytes.
-- Setting underline attribute needs 9 bytes.
-- A Ꝩ character takes 3 bytes.
-- The whole line needs 3 + 9 + 3 * 21838 + 3 = 65529 bytes.
-- The cursor_address that comes after will overflow the 65535-byte buffer.
local line = (''):rep(21838) .. ''
child_session:notify('nvim_buf_set_lines', 0, 0, -1, true, { line, 'b' })
-- Close the :intro message and redraw the lines.
feed_data('\n')
screen:expect([[
{107:^ꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨ}|
{107:ꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨ}|*310
{107:ꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨ℃ }|
b |
{100:~ }|*17
{3:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
end)
it('draws correctly when setting title overflows #30793', function()
screen:try_resize(67, 327)
retry(nil, nil, function()
eq({ true, 324 }, { child_session:request('nvim_win_get_height', 0) })
end)
child_exec_lua([[
vim.o.cmdheight = 0
vim.o.laststatus = 0
vim.o.ruler = false
vim.o.showcmd = false
vim.o.termsync = false
vim.o.title = true
]])
retry(nil, nil, function()
eq('[No Name] - Nvim', api.nvim_buf_get_var(0, 'term_title'))
eq({ true, 326 }, { child_session:request('nvim_win_get_height', 0) })
end)
-- Use full screen message so that redrawing afterwards is more deterministic.
child_session:notify('nvim_command', 'intro')
screen:expect({ any = 'Nvim is open source and freely distributable' })
-- Going to top-left corner needs 3 bytes.
-- A Ꝩ character takes 3 bytes.
-- The whole line needs 3 + 3 * 21842 = 65529 bytes.
-- The title will be updated because the buffer is now modified.
-- The start of the OSC 0 sequence to set title can fit in the 65535-byte buffer,
-- but the title string cannot.
local line = (''):rep(21842)
child_session:notify('nvim_buf_set_lines', 0, 0, -1, true, { line })
-- Close the :intro message and redraw the lines.
feed_data('\n')
screen:expect([[
^ꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨ|
ꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨꝨ|*325
{5:-- TERMINAL --} |
]])
retry(nil, nil, function()
eq('[No Name] + - Nvim', api.nvim_buf_get_var(0, 'term_title'))
end)
end)
it('visual bell (padding) does not crash #21610', function()
feed_data ':set visualbell\n'
screen:expect([[
^ |
{100:~ }|*3
{3:[No Name] }|
:set visualbell |
{5:-- TERMINAL --} |
]])
-- move left is enough to invoke the bell
feed_data 'h'
-- visual change to show we process events after this
feed_data 'i'
screen:expect([[
^ |
{100:~ }|*3
{3:[No Name] }|
{5:-- INSERT --} |
{5:-- TERMINAL --} |
]])
end)
it('no assert failure on deadly signal #21896', function()
exec_lua([[vim.uv.kill(vim.fn.jobpid(vim.bo.channel), 'sigterm')]])
screen:expect(is_os('win') and { any = '%[Process exited 1%]' } or [[
Nvim: Caught deadly signal 'SIGTERM' |
|
[Process exited 1]^ |
|*3
{5:-- TERMINAL --} |
]])
end)
it('exit status 1 and error message with deadly signal sent to server', function()
local _, server_pid = child_session:request('nvim_call_function', 'getpid', {})
exec_lua([[vim.uv.kill(..., 'sigterm')]], server_pid)
if not is_os('win') then
screen:expect({ any = vim.pesc([[Nvim: Caught deadly signal 'SIGTERM']]) })
end
screen:expect({ any = vim.pesc('[Process exited 1]') })
end)
it('exits immediately when stdin is closed #35744', function()
local chan = api.nvim_get_option_value('channel', { buf = 0 })
local pid = fn.jobpid(chan)
fn.chanclose(chan)
retry(nil, 50, function()
eq(vim.NIL, api.nvim_get_proc(pid))
end)
screen:expect({ any = vim.pesc('[Process exited 1]') })
end)
it('exits properly when :quit non-last window in event handler #14379', function()
local code = [[
vim.defer_fn(function()
vim.cmd('vsplit | quit')
end, 0)
vim.cmd('quit')
]]
child_session:notify('nvim_exec_lua', code, {})
screen:expect([[
|
[Process exited 0]^ |
|*4
{5:-- TERMINAL --} |
]])
end)
it('no stack-use-after-scope with cursor color #22432', function()
screen:set_option('rgb', true)
command('set termguicolors')
child_session:request(
'nvim_exec2',
[[
set tgc
hi Cursor guifg=Red guibg=Green
set guicursor=n:block-Cursor/lCursor
]],
{}
)
screen:expect([[
^ |
{1:~}{18: }|*3
{3:[No Name] }|
|
{5:-- TERMINAL --} |
]])
feed_data('i')
screen:expect([[
^ |
{1:~}{18: }|*3
{3:[No Name] }|
{5:-- INSERT --} |
{5:-- TERMINAL --} |
]])
end)
it('redraws on SIGWINCH even if terminal size is unchanged #23411', function()
-- On Windows, SIGWINCH cannot be sent as a signal with uv_kill(), while
-- SIGWINCH handlers are only called on terminal resize.
t.skip(is_os('win'), 'N/A for Windows')
child_session:request('nvim_echo', { { 'foo' } }, false, {})
screen:expect([[
^ |
{100:~ }|*3
{3:[No Name] }|
foo |
{5:-- TERMINAL --} |
]])
exec_lua([[vim.uv.kill(vim.fn.jobpid(vim.bo.channel), 'sigwinch')]])
screen:expect([[
^ |
{100:~ }|*3
{3:[No Name] }|
|
{5:-- TERMINAL --} |
]])
end)
it('supports hiding cursor', function()
child_session:request(
'nvim_command',
"let g:id = jobstart([v:progpath, '--clean', '--headless'])"
)
feed_data(':call jobwait([g:id])\n')
screen:expect([[
|
{100:~ }|*3
{3:[No Name] }|
:call jobwait([g:id]) |
{5:-- TERMINAL --} |
]])
feed_data('\003')
screen:expect([[
^ |
{100:~ }|*3
{3:[No Name] }|
Type :qa and press <Enter> to exit Nvim |
{5:-- TERMINAL --} |
]])
end)
it('cursor is not hidden on incsearch with no match', function()
feed_data('ifoo\027')
feed_data('/foo')
screen:expect([[
{2:foo} |
{100:~ }|*3
{3:[No Name] [+] }|
/foo^ |
{5:-- TERMINAL --} |
]])
screen:sleep(10)
feed_data('b')
screen:expect([[
foo |
{100:~ }|*3
{3:[No Name] [+] }|
/foob^ |
{5:-- TERMINAL --} |
]])
screen:sleep(10)
feed_data('a')
screen:expect([[
foo |
{100:~ }|*3
{3:[No Name] [+] }|
/fooba^ |
{5:-- TERMINAL --} |
]])
end)
it('emits hyperlinks with OSC 8', function()
t.skip(is_os('win'), 'FIXME: does not work on Windows')
exec_lua([[
local buf = vim.api.nvim_get_current_buf()
_G.urls = {}
vim.api.nvim_create_autocmd('TermRequest', {
buffer = buf,
callback = function(args)
local req = args.data.sequence
if not req then
return
end
local id, url = req:match('\027]8;id=(%d+);(.*)$')
if id ~= nil and url ~= nil then
table.insert(_G.urls, { id = tonumber(id), url = url })
end
end,
})
]])
child_exec_lua([[
vim.api.nvim_buf_set_lines(0, 0, 0, true, {'Hello'})
_G.NS = vim.api.nvim_create_namespace('test')
vim.api.nvim_buf_set_extmark(0, _G.NS, 0, 1, {
end_col = 3,
url = 'https://example.com',
})
]])
retry(nil, 1000, function()
eq({ { id = 0xE1EA0000, url = 'https://example.com' } }, exec_lua([[return _G.urls]]))
end)
-- No crash with very long URL #30794
child_exec_lua([[
vim.api.nvim_buf_set_extmark(0, _G.NS, 0, 3, {
end_col = 5,
url = 'https://example.com/' .. ('a'):rep(65536),
})
]])
retry(nil, nil, function()
eq({
{ id = 0xE1EA0000, url = 'https://example.com' },
{ id = 0xE1EA0001, url = 'https://example.com/' .. ('a'):rep(65536) },
}, exec_lua([[return _G.urls]]))
end)
end)
it('TermResponse works with vim.wait() from another autocommand #32706', function()
child_exec_lua([[
_G.termresponse = nil
vim.api.nvim_create_autocmd('TermResponse', {
callback = function(ev)
_G.sequence = ev.data.sequence
_G.v_termresponse = vim.v.termresponse
end,
})
vim.api.nvim_create_autocmd('InsertEnter', {
buffer = 0,
callback = function()
_G.result = vim.wait(3000, function()
local expected = '\027P1+r5463'
return _G.sequence == expected and _G.v_termresponse == expected
end)
end,
})
]])
feed_data('i')
feed_data('\027P1+r5463\027\\')
retry(nil, 4000, function()
eq(true, child_exec_lua('return _G.result'))
end)
end)
it('TermResponse from unblock_autocmds() sets "data"', function()
if not child_exec_lua('return pcall(require, "ffi")') then
pending('N/A: missing LuaJIT FFI')
end
child_exec_lua([[
local ffi = require('ffi')
ffi.cdef[=[
void block_autocmds(void);
void unblock_autocmds(void);
]=]
ffi.C.block_autocmds()
vim.api.nvim_create_autocmd('TermResponse', {
once = true,
callback = function(ev)
_G.data = ev.data
end,
})
]])
feed_data('\027P0$r\027\\')
retry(nil, 4000, function()
eq('\027P0$r', child_exec_lua('return vim.v.termresponse'))
end)
eq(vim.NIL, child_exec_lua('return _G.data'))
child_exec_lua('require("ffi").C.unblock_autocmds()')
eq({ sequence = '\027P0$r' }, child_exec_lua('return _G.data'))
-- If TermResponse during TermResponse changes v:termresponse, data.sequence contains the actual
-- response that triggered the autocommand.
-- The second autocommand below forces a use-after-free when v:termresponse's value changes
-- during TermResponse if data.sequence didn't allocate its own copy.
child_exec_lua([[
require('ffi').C.block_autocmds()
vim.api.nvim_create_autocmd('TermResponse', {
once = true,
callback = function(ev)
_G.au1_termresponse1 = vim.v.termresponse
_G.au1_sequence1 = ev.data.sequence
local chan = vim.fn.sockconnect('pipe', vim.v.servername, { rpc = true })
vim.rpcrequest(chan, 'nvim_ui_term_event', 'termresponse', 'baz')
_G.au1_termresponse2 = vim.v.termresponse
_G.au1_sequence2 = ev.data.sequence
end,
})
_G.au2_sequences = {}
vim.api.nvim_create_autocmd('TermResponse', {
callback = function(ev)
table.insert(_G.au2_sequences, ev.data.sequence)
end,
})
]])
child_session:request('nvim_ui_term_event', 'termresponse', 'foobar')
eq('foobar', child_exec_lua('return vim.v.termresponse'))
-- For good measure, check deferred TermResponse doesn't try to fire if autocmds are still
-- blocked after unblock_autocmds.
child_exec_lua('require("ffi").C.block_autocmds() require("ffi").C.unblock_autocmds()')
eq(vim.NIL, child_exec_lua('return _G.au1_termresponse1'))
child_exec_lua('require("ffi").C.unblock_autocmds()')
eq('foobar', child_exec_lua('return _G.au1_termresponse1'))
eq('foobar', child_exec_lua('return _G.au1_sequence1'))
eq('baz', child_exec_lua('return _G.au1_termresponse2'))
eq('foobar', child_exec_lua('return _G.au1_sequence2')) -- unchanged
-- Second autocmd triggers due to "baz" (via the nested TermResponse), then from "foobar".
eq({ 'baz', 'foobar' }, child_exec_lua('return _G.au2_sequences'))
end)
it('nvim_ui_send works', function()
child_session:request('nvim_ui_send', '\027]2;TEST_TITLE\027\\')
retry(nil, nil, function()
eq('TEST_TITLE', api.nvim_buf_get_var(0, 'term_title'))
end)
end)
end)
describe('TUI', function()
before_each(clear)
it('resize at startup #17285 #15044 #11330', function()
local screen = Screen.new(50, 10)
screen:add_extra_attr_ids({
[100] = { foreground = tonumber('0x4040ff'), fg_indexed = true },
[101] = { foreground = Screen.colors.Gray100, background = Screen.colors.DarkGreen },
})
fn.jobstart({
nvim_prog,
'--clean',
'--cmd',
'colorscheme vim',
'--cmd',
'set notermguicolors',
'--cmd',
nvim_set,
'--cmd',
'let start = reltime() | while v:true | if reltimefloat(reltime(start)) > 2 | break | endif | endwhile',
}, {
term = true,
env = {
VIMRUNTIME = os.getenv('VIMRUNTIME'),
},
})
exec([[
sleep 500m
vs new
]])
screen:expect(([[
^ │ |
{1:~ }│{100:~ }|*6
{1:~ }│ |
{3:new }{101:{MATCH:<.*%s} [-] }|
|
]]):format(is_os('win') and '[/\\]nvim%.exe' or '/nvim'))
end)
-- #28667, #28668
for _, guicolors in ipairs({ 'notermguicolors', 'termguicolors' }) do
it('has no black flicker when clearing regions during startup with ' .. guicolors, function()
local screen = Screen.new(50, 10)
-- Colorscheme is automatically detected as light in _core/defaults.lua, so fg
-- should be dark except on Windows, where it doesn't respond to the OSC11 query,
-- so bg is dark.
local fg = is_os('win') and Screen.colors.NvimLightGrey2 or Screen.colors.NvimDarkGrey2
local bg = is_os('win') and Screen.colors.NvimDarkGrey2 or Screen.colors.NvimLightGrey2
screen:add_extra_attr_ids({
[100] = {
foreground = fg,
background = bg,
},
})
fn.jobstart({
nvim_prog,
'--clean',
'--cmd',
'set ' .. guicolors,
'--cmd',
'echo "foo"',
'--cmd',
'sleep 10',
}, {
term = true,
env = { VIMRUNTIME = os.getenv('VIMRUNTIME') },
})
if guicolors == 'termguicolors' then
screen:expect([[
{100:^ }|
{100: }|*7
{100:foo }|
|
]])
else
screen:expect([[
^ |
|*7
foo |
|
]])
end
end)
end
it('argv[0] can be overridden #23953', function()
t.skip(is_os('win'), 'N/A for Windows')
local screen = Screen.new(50, 7, { rgb = false })
fn.jobstart(
{ testprg('shell-test'), 'EXECVP', nvim_prog, 'Xargv0nvim', '--clean' },
{ term = true, env = { VIMRUNTIME = os.getenv('VIMRUNTIME') } }
)
command('startinsert')
screen:expect([[
^ |
~ |*3
[No Name] 0,0-1 All|
|
{5:-- TERMINAL --} |
]])
feed_data(':put =v:argv + [v:progname]\n')
screen:expect([[
Xargv0nvim |
--embed |
--clean |
^Xargv0nvim |
[No Name] [+] 5,1 Bot|
4 more lines |
{5:-- TERMINAL --} |
]])
end)
it("float is still highlighted with 'winblend' over uninitialized cells #34360", function()
write_file(
'Xblend.lua',
[[
local win = vim.api.nvim_open_win(0, false, { relative = 'editor', width = 3, height = 1, row = 1000, col = 0, zindex = 400 })
vim.api.nvim_set_option_value('winblend', 30, { win = win })
vim.fn.setline(1, "foo")
vim.api.nvim_buf_set_extmark(0, vim.api.nvim_create_namespace(''), 0, 0, { end_col = 3, hl_group = 'Title' })
]]
)
finally(function()
os.remove('Xblend.lua')
end)
local screen = tt.setup_child_nvim({ '--clean', '-u', 'Xblend.lua' })
screen:expect([[
{5:^foo} |
~ |*3
[No Name] [+] 1,1 All|
{5:foo} |
{5:-- TERMINAL --} |
]])
end)
it('with non-tty (pipe) stdout/stderr', function()
t.skip(is_os('win'), 'N/A for Windows')
finally(function()
os.remove('testF')
os.remove(testlog)
end)
local screen = tt.setup_screen(
0,
('"%s" --clean --cmd "set noswapfile noshowcmd noruler" --cmd "normal iabc" > /dev/null 2>&1 && cat testF && rm testF'):format(
nvim_prog
),
nil,
{ VIMRUNTIME = os.getenv('VIMRUNTIME'), NVIM_LOG_FILE = testlog }
)
feed_data(':w testF\n:q\n')
screen:expect([[
:w testF |
:q |
abc |
|
[Process exited 0]^ |
|
{5:-- TERMINAL --} |
]])
assert_log('TUI: timed out waiting for DA1 response', testlog)
end)
it('<C-h> #10134', function()
local screen = tt.setup_child_nvim({
'--clean',
'--cmd',
'colorscheme vim',
'--cmd',
'set noruler',
'--cmd',
':nnoremap <C-h> :echomsg "\\<C-h\\>"<CR>',
}, { env = env_notermguicolors })
screen:expect([[
^ |
{100:~ }|*3
{3:[No Name] }|
|
{5:-- TERMINAL --} |
]])
command([[call chansend(b:terminal_job_id, "\<C-h>")]])
screen:expect([[
^ |
{100:~ }|*3
{3:[No Name] }|
<C-h> |
{5:-- TERMINAL --} |
]])
end)
it('draws line with many trailing spaces correctly #24955', function()
local screen = tt.setup_child_nvim({
'--clean',
'--cmd',
'colorscheme vim',
'--cmd',
'call setline(1, ["1st line" .. repeat(" ", 153), "2nd line"])',
}, { cols = 80, env = env_notermguicolors })
screen:expect([[
^1st line |
|*2
2nd line |
{3:[No Name] [+] 1,1 All}|
|
{5:-- TERMINAL --} |
]])
feed_data('$')
screen:expect([[
1st line |
|
^ |
2nd line |
{3:[No Name] [+] 1,161 All}|
|
{5:-- TERMINAL --} |
]])
end)
it('draws screen lines with leading spaces correctly #29711', function()
local screen = tt.setup_child_nvim({
'--clean',
'--cmd',
'set foldcolumn=6 | call setline(1, ["", repeat("aabb", 1000)]) | echo 42',
}, { extra_rows = 10, cols = 66 })
screen:expect([[
^ |
aabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabb|*12
aabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabba@@@|
[No Name] [+] 1,0-1 Top|
42 |
{5:-- TERMINAL --} |
]])
feed_data('\12') -- Ctrl-L
-- The first line counts as 3 cells.
-- For the second line, 6 repeated spaces at the start counts as 2 cells,
-- so each screen line of the second line counts as 62 cells.
-- After drawing the first line and 8 screen lines of the second line,
-- 3 + 8 * 62 = 499 cells have been counted.
-- The 6 repeated spaces at the start of the next screen line exceeds the
-- 500-cell limit, so the buffer is flushed after these spaces.
screen:expect([[
^ |
aabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabb|*12
aabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabba@@@|
[No Name] [+] 1,0-1 Top|
|
{5:-- TERMINAL --} |
]])
end)
it('no heap-buffer-overflow when changing &columns', function()
t.skip(is_os('win'), 'FIXME: does not work on Windows')
-- Set a different bg colour and change $TERM to something dumber so the `print_spaces()`
-- codepath in `clear_region()` is hit.
local screen = tt.setup_child_nvim({
'--clean',
'--cmd',
'set notermguicolors | highlight Normal ctermbg=red',
'--cmd',
'call setline(1, ["a"->repeat(&columns)])',
}, { env = { TERM = 'ansi' } })
screen:expect([[
{117:^aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}|
{117:~ }|*3
{118:[No Name] [+] 1,1 All}|
{117: }|
{5:-- TERMINAL --} |
]])
feed_data(':set columns=12\n')
screen:expect([[
{117:^aaaaaaaaaaaa }|
{117:aaaaaaaaaaaa }|*3
{118:< All}{117: }|
{117: }|
{5:-- TERMINAL --} |
]])
-- Wider than TUI, so screen state will look weird.
-- Wait for the statusline to redraw to confirm that the TUI lives and ASAN is happy.
feed_data(':set columns=99|set stl=redrawn%m\n')
screen:expect({ any = 'redrawn%[%+%]' })
end)
end)
describe('TUI UIEnter/UILeave', function()
it('fires exactly once, after VimEnter', function()
clear()
local screen = tt.setup_child_nvim({
'--clean',
'--cmd',
'colorscheme vim',
'--cmd',
'set noswapfile noshowcmd noruler',
'--cmd',
'let g:evs = []',
'--cmd',
'autocmd UIEnter * :call add(g:evs, "UIEnter")',
'--cmd',
'autocmd UILeave * :call add(g:evs, "UILeave")',
'--cmd',
'autocmd VimEnter * :call add(g:evs, "VimEnter")',
}, { env = env_notermguicolors })
screen:expect([[
^ |
{100:~ }|*3
{3:[No Name] }|
|
{5:-- TERMINAL --} |
]])
feed_data(':echo g:evs\n')
screen:expect([[
^ |
{100:~ }|*3
{3:[No Name] }|
['VimEnter', 'UIEnter'] |
{5:-- TERMINAL --} |
]])
end)
end)
describe('TUI FocusGained/FocusLost', function()
local screen --[[@type test.functional.ui.screen]]
local child_session --[[@type test.Session]]
before_each(function()
clear()
local child_server = new_pipename()
screen = tt.setup_child_nvim({
'--listen',
child_server,
'-u',
'NONE',
'-i',
'NONE',
'--cmd',
'colorscheme vim',
'--cmd',
'set noswapfile noshowcmd noruler background=dark',
}, { env = env_notermguicolors })
screen:expect([[
^ |
{100:~ }|*3
{3:[No Name] }|
|
{5:-- TERMINAL --} |
]])
child_session = n.connect(child_server)
child_session:request(
'nvim_exec2',
[[
autocmd FocusGained * echo 'gained'
autocmd FocusLost * echo 'lost'
]],
{}
)
feed_data('\034\016') -- CTRL-\ CTRL-N
end)
it('in normal-mode', function()
retry(2, 3 * screen.timeout, function()
feed_data('\027[I')
screen:expect([[
^ |
{100:~ }|*3
{3:[No Name] }|
gained |
{5:-- TERMINAL --} |
]])
feed_data('\027[O')
screen:expect([[
^ |
{100:~ }|*3
{3:[No Name] }|
lost |
{5:-- TERMINAL --} |
]])
end)
end)
it('in insert-mode', function()
feed_data(':set noshowmode\r')
feed_data('i')
screen:expect([[
^ |
{100:~ }|*3
{3:[No Name] }|
:set noshowmode |
{5:-- TERMINAL --} |
]])
retry(2, 3 * screen.timeout, function()
feed_data('\027[I')
screen:expect([[
^ |
{100:~ }|*3
{3:[No Name] }|
gained |
{5:-- TERMINAL --} |
]])
feed_data('\027[O')
screen:expect([[
^ |
{100:~ }|*3
{3:[No Name] }|
lost |
{5:-- TERMINAL --} |
]])
end)
end)
-- During cmdline-mode we ignore :echo invoked by timers/events.
-- See commit: 5cc87d4dabd02167117be7a978b5c8faaa975419.
it('in cmdline-mode does NOT :echo', function()
feed_data(':')
feed_data('\027[I')
screen:expect([[
|
{100:~ }|*3
{3:[No Name] }|
:^ |
{5:-- TERMINAL --} |
]])
feed_data('\027[O')
screen:expect_unchanged()
end)
it('in cmdline-mode', function()
-- Set up autocmds that modify the buffer, instead of just calling :echo.
-- This is how we can test handling of focus gained/lost during cmdline-mode.
-- See commit: 5cc87d4dabd02167117be7a978b5c8faaa975419.
child_session:request(
'nvim_exec2',
[[
autocmd!
autocmd FocusLost * call append(line('$'), 'lost')
autocmd FocusGained * call append(line('$'), 'gained')
]],
{}
)
retry(2, 3 * screen.timeout, function()
-- Enter cmdline-mode.
feed_data(':')
screen:sleep(1)
-- Send focus lost/gained termcodes.
feed_data('\027[O')
feed_data('\027[I')
screen:sleep(1)
-- Exit cmdline-mode. Redraws from timers/events are blocked during
-- cmdline-mode, so the buffer won't be updated until we exit cmdline-mode.
feed_data('\n')
screen:expect { any = 'lost' .. (' '):rep(46) .. '|\ngained' }
end)
end)
it('in terminal-mode', function()
feed_data(':set shell=' .. testprg('shell-test') .. ' shellcmdflag=EXE\n')
feed_data(':set shellxquote=\n') -- win: avoid extra quotes
feed_data(':set noshowmode laststatus=0\n')
feed_data(':terminal zia\n')
-- Wait for terminal to be ready.
screen:expect([[
^ready $ zia |
|
[Process exited 0] |
|*2
:terminal zia |
{5:-- TERMINAL --} |
]])
feed_data('\027[I')
screen:expect {
grid = [[
^ready $ zia |
|
[Process exited 0] |
|*2
gained |
{5:-- TERMINAL --} |
]],
timeout = (4 * screen.timeout),
}
feed_data('\027[O')
screen:expect([[
^ready $ zia |
|
[Process exited 0] |
|*2
lost |
{5:-- TERMINAL --} |
]])
end)
it('in press-enter prompt', function()
t.skip(is_os('win'), 'FIXME: some spaces have wrong attrs on Windows')
feed_data(":echom 'msg1'|echom 'msg2'|echom 'msg3'|echom 'msg4'|echom 'msg5'\n")
-- Execute :messages to provoke the press-enter prompt.
feed_data(':messages\n')
screen:expect([[
msg1 |
msg2 |
msg3 |
msg4 |
msg5 |
{102:Press ENTER or type command to continue}^ |
{5:-- TERMINAL --} |
]])
feed_data('\027[I')
feed_data('\027[I')
screen:expect_unchanged()
end)
end)
-- These tests require `tt` because --headless/--embed does not initialize the TUI.
describe("TUI 't_Co' (terminal colors)", function()
local screen --[[@type test.functional.ui.screen]]
local function assert_term_colors(term, colorterm, maxcolors)
clear({ env = { TERM = term }, args = {} })
screen = tt.setup_child_nvim({
'--clean',
'--cmd',
'colorscheme vim',
'--cmd',
nvim_set .. ' notermguicolors',
}, {
env = {
LANG = 'C',
TERM = term or '',
COLORTERM = colorterm or '',
},
})
local tline --[[@type string]]
if maxcolors == 8 then
tline = '{112:~ }'
elseif maxcolors == 16 then
tline = '~ '
else
tline = '{100:~ }'
end
screen:expect(string.format(
[[
^ |
%s|*4
|
{5:-- TERMINAL --} |
]],
tline
))
feed_data(':echo &t_Co\n')
screen:expect(string.format(
[[
^ |
%s|*4
%-3s |
{5:-- TERMINAL --} |
]],
tline,
tostring(maxcolors and maxcolors or '')
))
end
-- ansi and no terminal type at all:
if is_os('win') then
it('guessed vtpcon with no TERM uses 256 colors', function()
assert_term_colors(nil, nil, 256)
end)
else
it('no TERM uses 8 colors', function()
assert_term_colors(nil, nil, 8)
end)
end
it('TERM=ansi no COLORTERM uses 8 colors', function()
assert_term_colors('ansi', nil, 8)
end)
it('TERM=ansi with COLORTERM=anything-no-number uses 16 colors', function()
assert_term_colors('ansi', 'yet-another-term', 16)
end)
it('unknown TERM COLORTERM with 256 in name uses 256 colors', function()
assert_term_colors('ansi', 'yet-another-term-256color', 256)
end)
it('TERM=ansi-256color sets 256 colours', function()
assert_term_colors('ansi-256color', nil, 256)
end)
-- Unknown terminal types:
it('unknown TERM no COLORTERM sets 8 colours', function()
assert_term_colors('yet-another-term', nil, 8)
end)
it('unknown TERM with COLORTERM=anything-no-number uses 16 colors', function()
assert_term_colors('yet-another-term', 'yet-another-term', 16)
end)
it('unknown TERM with 256 in name sets 256 colours', function()
assert_term_colors('yet-another-term-256color', nil, 256)
end)
it('unknown TERM COLORTERM with 256 in name uses 256 colors', function()
assert_term_colors('yet-another-term', 'yet-another-term-256color', 256)
end)
-- Linux kernel terminal emulator:
it('TERM=linux uses 256 colors', function()
assert_term_colors('linux', nil, 256)
end)
it('TERM=linux-16color uses 256 colors', function()
assert_term_colors('linux-16color', nil, 256)
end)
it('TERM=linux-256color uses 256 colors', function()
assert_term_colors('linux-256color', nil, 256)
end)
-- screen:
--
-- FreeBSD and Windows fall back to the built-in screen-256colour entry.
-- Linux and MacOS have a screen entry in external terminfo with 8 colours,
-- which is raised to 16 by COLORTERM.
it('TERM=screen no COLORTERM uses 8/256 colors', function()
if is_os('freebsd') or is_os('win') then
assert_term_colors('screen', nil, 256)
else
assert_term_colors('screen', nil, 8)
end
end)
it('TERM=screen COLORTERM=screen uses 16/256 colors', function()
if is_os('freebsd') or is_os('win') then
assert_term_colors('screen', 'screen', 256)
else
assert_term_colors('screen', 'screen', 16)
end
end)
it('TERM=screen COLORTERM=screen-256color uses 256 colors', function()
assert_term_colors('screen', 'screen-256color', 256)
end)
it('TERM=screen-256color no COLORTERM uses 256 colors', function()
assert_term_colors('screen-256color', nil, 256)
end)
-- tmux:
--
-- FreeBSD and MacOS fall back to the built-in tmux-256colour entry.
-- Linux has a tmux entry in external terminfo with 8 colours,
-- which is raised to 256.
it('TERM=tmux no COLORTERM uses 256 colors', function()
assert_term_colors('tmux', nil, 256)
end)
it('TERM=tmux COLORTERM=tmux uses 256 colors', function()
assert_term_colors('tmux', 'tmux', 256)
end)
it('TERM=tmux COLORTERM=tmux-256color uses 256 colors', function()
assert_term_colors('tmux', 'tmux-256color', 256)
end)
it('TERM=tmux-256color no COLORTERM uses 256 colors', function()
assert_term_colors('tmux-256color', nil, 256)
end)
-- xterm and imitators:
it('TERM=xterm uses 256 colors', function()
assert_term_colors('xterm', nil, 256)
end)
it('TERM=xterm COLORTERM=gnome-terminal uses 256 colors', function()
assert_term_colors('xterm', 'gnome-terminal', 256)
end)
it('TERM=xterm COLORTERM=mate-terminal uses 256 colors', function()
assert_term_colors('xterm', 'mate-terminal', 256)
end)
it('TERM=xterm-256color uses 256 colors', function()
assert_term_colors('xterm-256color', nil, 256)
end)
-- rxvt and stterm:
--
-- FreeBSD and MacOS fall back to the built-in rxvt-256color and
-- st-256colour entries.
-- Linux has an rxvt, an st, and an st-16color entry in external terminfo
-- with 8, 8, and 16 colours respectively, which are raised to 256.
it('TERM=rxvt no COLORTERM uses 256 colors', function()
assert_term_colors('rxvt', nil, 256)
end)
it('TERM=rxvt COLORTERM=rxvt uses 256 colors', function()
assert_term_colors('rxvt', 'rxvt', 256)
end)
it('TERM=rxvt-256color uses 256 colors', function()
assert_term_colors('rxvt-256color', nil, 256)
end)
it('TERM=st no COLORTERM uses 256 colors', function()
assert_term_colors('st', nil, 256)
end)
it('TERM=st COLORTERM=st uses 256 colors', function()
assert_term_colors('st', 'st', 256)
end)
it('TERM=st COLORTERM=st-256color uses 256 colors', function()
assert_term_colors('st', 'st-256color', 256)
end)
it('TERM=st-16color no COLORTERM uses 8/256 colors', function()
assert_term_colors('st', nil, 256)
end)
it('TERM=st-16color COLORTERM=st uses 16/256 colors', function()
assert_term_colors('st', 'st', 256)
end)
it('TERM=st-16color COLORTERM=st-256color uses 256 colors', function()
assert_term_colors('st', 'st-256color', 256)
end)
it('TERM=st-256color uses 256 colors', function()
assert_term_colors('st-256color', nil, 256)
end)
-- gnome and vte:
--
-- FreeBSD and MacOS fall back to the built-in vte-256color entry.
-- Linux has a gnome, a vte, a gnome-256color, and a vte-256color entry in
-- external terminfo with 8, 8, 256, and 256 colours respectively, which are
-- raised to 256.
it('TERM=gnome no COLORTERM uses 256 colors', function()
assert_term_colors('gnome', nil, 256)
end)
it('TERM=gnome COLORTERM=gnome uses 256 colors', function()
assert_term_colors('gnome', 'gnome', 256)
end)
it('TERM=gnome COLORTERM=gnome-256color uses 256 colors', function()
assert_term_colors('gnome', 'gnome-256color', 256)
end)
it('TERM=gnome-256color uses 256 colors', function()
assert_term_colors('gnome-256color', nil, 256)
end)
it('TERM=vte no COLORTERM uses 256 colors', function()
assert_term_colors('vte', nil, 256)
end)
it('TERM=vte COLORTERM=vte uses 256 colors', function()
assert_term_colors('vte', 'vte', 256)
end)
it('TERM=vte COLORTERM=vte-256color uses 256 colors', function()
assert_term_colors('vte', 'vte-256color', 256)
end)
it('TERM=vte-256color uses 256 colors', function()
assert_term_colors('vte-256color', nil, 256)
end)
-- others:
-- TODO(blueyed): this is made pending, since it causes failure + later hang
-- when using non-compatible libvterm (#9494/#10179).
pending('TERM=interix uses 8 colors', function()
assert_term_colors('interix', nil, 8)
end)
it('TERM=iTerm.app uses 256 colors', function()
assert_term_colors('iTerm.app', nil, 256)
end)
it('TERM=iterm uses 256 colors', function()
assert_term_colors('iterm', nil, 256)
end)
end)
-- These tests require `tt` because --headless/--embed does not initialize the TUI.
describe("TUI 'term' option", function()
local screen --[[@type test.functional.ui.screen]]
local function assert_term(term_envvar, term_expected)
clear()
screen = tt.setup_child_nvim({
'--clean',
'--cmd',
nvim_set .. ' notermguicolors',
}, {
env = {
LANG = 'C',
TERM = term_envvar or '',
},
})
local full_timeout = screen.timeout
retry(nil, 2 * full_timeout, function() -- Wait for TUI thread to set 'term'.
feed_data(":echo 'term='.(&term)\n")
screen:expect { any = 'term=' .. term_expected, timeout = 250 }
end)
end
it('gets builtin term if $TERM is invalid', function()
assert_term('foo', 'ansi')
end)
it('gets system-provided term if $TERM is valid', function()
if is_os('openbsd') then
assert_term('xterm', 'xterm')
elseif is_os('bsd') then -- BSD lacks terminfo, builtin is always used.
assert_term('xterm', 'xterm')
elseif is_os('mac') then
local status, _ = pcall(assert_term, 'xterm', 'xterm')
if not status then
pending('macOS: unibilium could not find terminfo')
end
else
assert_term('xterm', 'xterm')
end
end)
it('builtin terms', function()
-- These non-standard terminfos are always builtin.
assert_term('win32con', 'win32con')
assert_term('conemu', 'conemu')
assert_term('vtpcon', 'vtpcon')
end)
end)
-- These tests require `tt` because --headless/--embed does not initialize the TUI.
describe('TUI', function()
local screen --[[@type test.functional.ui.screen]]
-- Runs (child) `nvim` in a TTY (:terminal), to start the builtin TUI.
local function nvim_tui(extra_args)
clear()
screen = tt.setup_child_nvim({
'--clean',
'--cmd',
'colorscheme vim',
'--cmd',
nvim_set .. ' notermguicolors',
extra_args,
}, {
env = {
LANG = 'C',
},
})
end
it('-V3log logs terminfo values', function()
local logfile = 'Xtest_tui_verbose_log'
nvim_tui('-V3' .. logfile)
finally(function()
os.remove(logfile)
end)
-- Wait for TUI to start.
feed_data('Gitext')
screen:expect([[
text^ |
{100:~ }|*4
{5:-- INSERT --} |
{5:-- TERMINAL --} |
]])
retry(nil, 3000, function() -- Wait for log file to be flushed.
local log = read_file(logfile) or ''
eq('--- Terminal info --- {{{\n', string.match(log, '%-%-%- Terminal.-\n')) -- }}}
ok(#log > 50)
end)
end)
it('does not crash on large inputs #26099', function()
nvim_tui()
screen:expect([[
^ |
{100:~ }|*4
|
{5:-- TERMINAL --} |
]])
feed_data(string.format('\027]52;c;%s\027\\', string.rep('A', 8192)))
screen:expect_unchanged()
end)
it('queries the terminal for truecolor support', function()
t.skip(is_os('win'), 'FIXME: does not work on Windows')
clear()
exec_lua([[
vim.api.nvim_create_autocmd('TermRequest', {
callback = function(args)
local req = args.data.sequence
local sequence = req:match('^\027P%+q([%x;]+)$')
if sequence then
local t = {}
for cap in vim.gsplit(sequence, ';') do
local resp = string.format('\027P1+r%s\027\\', sequence)
vim.api.nvim_chan_send(vim.bo[args.buf].channel, resp)
t[vim.text.hexdecode(cap)] = true
end
vim.g.xtgettcap = t
return true
end
end,
})
]])
local child_server = new_pipename()
screen = tt.setup_child_nvim({
'--clean',
'--listen',
child_server,
}, {
env = {
VIMRUNTIME = os.getenv('VIMRUNTIME'),
-- Force COLORTERM to be unset and use a TERM that does not contain Tc or RGB in terminfo.
-- This will force the nested nvim instance to query with XTGETTCAP
COLORTERM = '',
TERM = 'xterm-256colors',
},
})
screen:expect({ any = '%[No Name%]' })
local child_session = n.connect(child_server)
retry(nil, 1000, function()
eq({
Tc = true,
RGB = true,
setrgbf = true,
setrgbb = true,
}, eval("get(g:, 'xtgettcap', '')"))
eq({ true, 1 }, { child_session:request('nvim_eval', '&termguicolors') })
end)
end)
it('does not query the terminal for truecolor support if $COLORTERM is set', function()
clear()
exec_lua([[
vim.api.nvim_create_autocmd('TermRequest', {
callback = function(args)
local req = args.data.sequence
vim.g.termrequest = req
local xtgettcap = req:match('^\027P%+q([%x;]+)$')
if xtgettcap then
local t = {}
for cap in vim.gsplit(xtgettcap, ';') do
local resp = string.format('\027P1+r%s\027\\', xtgettcap)
vim.api.nvim_chan_send(vim.bo[args.buf].channel, resp)
t[vim.text.hexdecode(cap)] = true
end
vim.g.xtgettcap = t
return true
elseif req:match('^\027P$qm\027\\$') then
vim.g.decrqss = true
end
end,
})
]])
local child_server = new_pipename()
screen = tt.setup_child_nvim({
'--clean',
'--listen',
child_server,
}, {
env = {
VIMRUNTIME = os.getenv('VIMRUNTIME'),
-- With COLORTERM=256, Nvim should not query the terminal and should not set 'tgc'
COLORTERM = '256',
TERM = 'xterm-256colors',
},
})
screen:expect({ any = '%[No Name%]' })
local child_session = n.connect(child_server)
retry(nil, 1000, function()
local xtgettcap = eval("get(g:, 'xtgettcap', {})")
eq(nil, xtgettcap['Tc'])
eq(nil, xtgettcap['RGB'])
eq(nil, xtgettcap['setrgbf'])
eq(nil, xtgettcap['setrgbb'])
eq(0, eval([[get(g:, 'decrqss')]]))
eq({ true, 0 }, { child_session:request('nvim_eval', '&termguicolors') })
end)
end)
it('queries the terminal for OSC 52 support with XTGETTCAP', function()
t.skip(is_os('win'), 'FIXME: does not work on Windows')
clear()
if not exec_lua('return pcall(require, "ffi")') then
pending('N/A: missing LuaJIT FFI')
end
-- Change vterm's DA1 response so that it doesn't include 52
exec_lua(function()
local ffi = require('ffi')
ffi.cdef [[
extern char vterm_primary_device_attr[]
]]
ffi.copy(ffi.C.vterm_primary_device_attr, '61;22')
end)
exec_lua([[
_G.query = false
vim.api.nvim_create_autocmd('TermRequest', {
callback = function(args)
local req = args.data.sequence
local sequence = req:match('^\027P%+q([%x;]+)$')
if sequence and vim.text.hexdecode(sequence) == 'Ms' then
local resp = string.format('\027P1+r%s=%s\027\\', sequence, vim.text.hexencode('\027]52;;\027\\'))
vim.api.nvim_chan_send(vim.bo[args.buf].channel, resp)
_G.query = true
return true
end
end,
})
]])
local child_server = new_pipename()
screen = tt.setup_child_nvim({
'--listen',
child_server,
'--clean',
}, {
env = {
VIMRUNTIME = os.getenv('VIMRUNTIME'),
},
})
screen:expect({ any = '%[No Name%]' })
local child_session = n.connect(child_server)
retry(nil, 1000, function()
eq({ true, { osc52 = true } }, { child_session:request('nvim_eval', 'g:termfeatures') })
end)
eq(true, exec_lua([[return _G.query]]))
-- Attach another (non-TUI) UI to the child instance
local alt = Screen.new(nil, nil, nil, child_session)
finally(function()
alt:detach()
-- Avoid a dangling process after :detach.
child_session:request('nvim_command', 'qall!')
end)
-- Detach the first (primary) client so only the second UI is attached
feed_data(':detach\n')
alt:expect({ any = '%[No Name%]' })
-- osc52 should be cleared from termfeatures
eq({ true, {} }, { child_session:request('nvim_eval', 'g:termfeatures') })
end)
it('determines OSC 52 support from DA1 response', function()
t.skip(is_os('win'), 'FIXME: does not work on Windows')
clear()
exec_lua([[
-- Check that we do not emit an XTGETTCAP request when DA1 indicates support
_G.query = false
vim.api.nvim_create_autocmd('TermRequest', {
callback = function(args)
local req = args.data.sequence
local sequence = req:match('^\027P%+q([%x;]+)$')
if sequence and vim.text.hexdecode(sequence) == 'Ms' then
_G.query = true
return true
end
end,
})
]])
local child_server = new_pipename()
screen = tt.setup_child_nvim({
'--listen',
child_server,
'--clean',
}, {
env = {
VIMRUNTIME = os.getenv('VIMRUNTIME'),
},
})
screen:expect({ any = '%[No Name%]' })
local child_session = n.connect(child_server)
retry(nil, 1000, function()
eq({ true, { osc52 = true } }, { child_session:request('nvim_eval', 'g:termfeatures') })
end)
eq(false, exec_lua([[return _G.query]]))
end)
it('does not query the terminal for OSC 52 support when disabled', function()
clear()
exec_lua([[
_G.query = false
vim.api.nvim_create_autocmd('TermRequest', {
callback = function(args)
local req = args.data.sequence
local sequence = req:match('^\027P%+q([%x;]+)$')
if sequence and vim.text.hexdecode(sequence) == 'Ms' then
_G.query = true
return true
end
end,
})
]])
local child_server = new_pipename()
screen = tt.setup_child_nvim({
'--listen',
child_server,
'--clean',
'--cmd',
'let g:termfeatures = #{osc52: v:false}',
}, {
env = {
VIMRUNTIME = os.getenv('VIMRUNTIME'),
},
})
screen:expect({ any = '%[No Name%]' })
local child_session = n.connect(child_server)
eq({ true, { osc52 = false } }, { child_session:request('nvim_eval', 'g:termfeatures') })
eq(false, exec_lua([[return _G.query]]))
end)
end)
describe('TUI bg color', function()
if t.skip(is_os('win')) then
return
end
before_each(clear)
it('is properly set in a nested Nvim instance when background=dark', function()
command('highlight clear Normal')
command('set background=dark') -- set outer Nvim background
local child_server = new_pipename()
local screen = tt.setup_child_nvim({
'--clean',
'--listen',
child_server,
'--cmd',
'colorscheme vim',
'--cmd',
'set noswapfile',
})
screen:expect({ any = '%[No Name%]' })
local child_session = n.connect(child_server)
retry(nil, nil, function()
eq({ true, 'dark' }, { child_session:request('nvim_eval', '&background') })
end)
end)
it('is properly set in a nested Nvim instance when background=light', function()
command('highlight clear Normal')
command('set background=light') -- set outer Nvim background
local child_server = new_pipename()
local screen = tt.setup_child_nvim({
'--clean',
'--listen',
child_server,
'--cmd',
'colorscheme vim',
'--cmd',
'set noswapfile',
})
screen:expect({ any = '%[No Name%]' })
local child_session = n.connect(child_server)
retry(nil, nil, function()
eq({ true, 'light' }, { child_session:request('nvim_eval', '&background') })
end)
end)
it('queries the terminal for background color', function()
exec_lua([[
vim.api.nvim_create_autocmd('TermRequest', {
callback = function(args)
local req = args.data.sequence
if req == '\027]11;?' then
vim.g.oscrequest = true
return true
end
end,
})
]])
tt.setup_child_nvim({
'--clean',
'--cmd',
'colorscheme vim',
'--cmd',
'set noswapfile',
})
retry(nil, 1000, function()
eq(true, eval("get(g:, 'oscrequest', v:false)"))
end)
end)
it('triggers OptionSet from automatic background processing', function()
local screen = tt.setup_child_nvim({
'--clean',
'--cmd',
'colorscheme vim',
'--cmd',
'set noswapfile',
'-c',
'autocmd OptionSet background echo "did OptionSet, yay!"',
})
screen:expect([[
^ |
{5:~} |*3
{3:[No Name] 0,0-1 All}|
did OptionSet, yay! |
{5:-- TERMINAL --} |
]])
end)
it('sends theme update notifications when background changes #31652', function()
command('set background=dark') -- set outer Nvim background
local child_server = new_pipename()
local screen = tt.setup_child_nvim({
'--clean',
'--listen',
child_server,
'--cmd',
'colorscheme vim',
'--cmd',
'set noswapfile',
})
screen:expect({ any = '%[No Name%]' })
local child_session = n.connect(child_server)
retry(nil, nil, function()
eq({ true, 'dark' }, { child_session:request('nvim_eval', '&background') })
end)
command('set background=light') -- set outer Nvim background
retry(nil, nil, function()
eq({ true, 'light' }, { child_session:request('nvim_eval', '&background') })
end)
end)
end)
describe('TUI client', function()
local function start_tui_and_remote_client()
local server_super = n.clear()
local client_super = n.new_session(true)
finally(function()
client_super:close()
server_super:close()
end)
local server_pipe = new_pipename()
local screen_server = tt.setup_child_nvim({
'--clean',
'--listen',
server_pipe,
'--cmd',
'colorscheme vim',
'--cmd',
nvim_set .. ' laststatus=2 background=dark',
}, { env = env_notermguicolors })
screen_server:expect([[
^ |
{100:~ }|*3
{3:[No Name] }|
|
{5:-- TERMINAL --} |
]])
feed_data('iHello, World')
screen_server:expect([[
Hello, World^ |
{100:~ }|*3
{3:[No Name] [+] }|
{5:-- INSERT --} |
{5:-- TERMINAL --} |
]])
feed_data('\027')
local s0 = [[
Hello, Worl^d |
{100:~ }|*3
{3:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]]
screen_server:expect(s0)
feed_data(':echo "GUI Running: " .. has("gui_running")\013')
screen_server:expect({ any = 'GUI Running: 0' })
set_session(client_super)
local screen_client = tt.setup_child_nvim({
'--remote-ui',
'--server',
server_pipe,
}, { env = env_notermguicolors })
screen_client:expect(s0)
return server_super, screen_server, screen_client
end
it('connects to remote instance (with its own TUI)', function()
local _, screen_server, screen_client = start_tui_and_remote_client()
feed_data(':echo "GUI Running: " .. has("gui_running")\013')
screen_client:expect({ any = 'GUI Running: 0' })
-- grid smaller than containing terminal window is cleared properly
feed_data(":call setline(1,['a'->repeat(&columns)]->repeat(&lines))\n")
feed_data('0:set lines=3\n')
local s1 = [[
^aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa|
{3:[No Name] [+] }|
|*4
{5:-- TERMINAL --} |
]]
screen_client:expect(s1)
screen_server:expect(s1)
end)
it(':restart works when connecting to remote instance (with its own TUI)', function()
local _, screen_server, screen_client = start_tui_and_remote_client()
-- Run :restart on the remote client.
-- The remote client should start a new server while the original one should exit.
feed_data(':restart +qall!\n')
screen_client:expect([[
^ |
{100:~ }|*3
{3:[No Name] }|
|
{5:-- TERMINAL --} |
]])
screen_server:expect({ any = vim.pesc('[Process exited 0]') })
feed_data(':echo "GUI Running: " .. has("gui_running")\013')
screen_client:expect({ any = 'GUI Running: 0' })
feed_data(':q!\r')
screen_client:expect({ any = vim.pesc('[Process exited 0]') })
end)
local function start_headless_server_and_client(use_testlog)
local server = n.new_session(false, {
args_rm = { '--cmd' },
args = {
'--cmd',
'colorscheme vim',
'--cmd',
nvim_set .. ' notermguicolors background=dark',
},
})
local client_super =
n.new_session(true, use_testlog and { env = { NVIM_LOG_FILE = testlog } } or {})
finally(function()
client_super:close()
server:close()
os.remove(testlog)
end)
set_session(server)
--- @type string
local server_pipe = api.nvim_get_vvar('servername')
server:request('nvim_input', 'iHalloj!<Esc>')
set_session(client_super)
local screen_client = tt.setup_child_nvim({
'--remote-ui',
'--server',
server_pipe,
}, { env = env_notermguicolors })
screen_client:expect([[
Halloj^! |
{100:~ }|*4
|
{5:-- TERMINAL --} |
]])
return server, server_pipe, screen_client
end
it('connects to remote instance (--headless)', function()
local server, server_pipe, screen_client = start_headless_server_and_client(false)
-- No heap-use-after-free when receiving UI events after deadly signal #22184
server:request('nvim_input', ('a'):rep(1000))
exec_lua([[vim.uv.kill(vim.fn.jobpid(vim.bo.channel), 'sigterm')]])
screen_client:expect(is_os('win') and { any = '%[Process exited 1%]' } or [[
Nvim: Caught deadly signal 'SIGTERM' |
|
[Process exited 1]^ |
|*3
{5:-- TERMINAL --} |
]])
eq(0, api.nvim_get_vvar('shell_error'))
-- exits on input eof #22244
-- Use system() without input so that stdin is closed.
fn.system({ nvim_prog, '--remote-ui', '--server', server_pipe })
eq(1, api.nvim_get_vvar('shell_error'))
command('bwipe!')
-- Start another remote client to attach to the same server.
fn.jobstart({ nvim_prog, '--remote-ui', '--server', server_pipe }, { term = true })
command('startinsert')
screen_client:expect([[
{100:<<<}aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa|
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa|*3
aaaaaa^ |
{5:-- INSERT --} |
{5:-- TERMINAL --} |
]])
feed_data('\027')
feed_data(':echo "GUI Running: " .. has("gui_running")\013')
screen_client:expect({ any = 'GUI Running: 0' })
end)
it(':restart works when connecting to remote instance (--headless)', function()
local _, server_pipe, screen_client = start_headless_server_and_client(false)
-- Run :restart on the client.
-- The client should start a new server while the original server should exit.
feed_data(':restart +qall!\n')
screen_client:expect([[
^ |
{100:~ }|*4
|
{5:-- TERMINAL --} |
]])
retry(nil, nil, function()
eq(nil, vim.uv.fs_stat(server_pipe))
end)
feed_data(':echo "GUI Running: " .. has("gui_running")\013')
screen_client:expect({ any = 'GUI Running: 0' })
feed_data(':q!\r')
screen_client:expect({ any = vim.pesc('[Process exited 0]') })
end)
local ffi_str_defs = [[
local ffi = require('ffi')
local cstr = ffi.typeof('char[?]')
ffi.cdef('typedef struct { char *data; size_t size; } String;')
local function to_api_string(str)
return ffi.new('String', { data = cstr(#str + 1, str), size = #str })
end
]]
it('does not crash or hang with a very long title', function()
local server, _, screen_client = start_headless_server_and_client(true)
local server_exec_lua = tt.make_lua_executor(server)
if not server_exec_lua('return pcall(require, "ffi")') then
pending('N/A: missing LuaJIT FFI')
end
local bufname = api.nvim_buf_get_name(0)
local old_title = api.nvim_buf_get_var(0, 'term_title')
if not is_os('win') then
eq(bufname, old_title)
end
-- Normally a title cannot be longer than the 65535-byte buffer as maketitle()
-- limits it length. Use FFI to send a very long title directly.
server_exec_lua(ffi_str_defs .. [[
ffi.cdef('void ui_call_set_title(String title);')
ffi.C.ui_call_set_title(to_api_string(('a'):rep(65536)))
]])
screen_client:expect_unchanged()
assert_log('set_title: title string too long!', testlog)
eq(old_title, api.nvim_buf_get_var(0, 'term_title'))
-- Following escape sequences are not affected.
server:request('nvim_set_option_value', 'title', true, {})
retry(nil, nil, function()
eq('[No Name] + - Nvim', api.nvim_buf_get_var(0, 'term_title'))
end)
end)
it('logs chdir failure properly', function()
t.skip(is_os('win'), 'N/A for Windows')
local server, _, screen_client = start_headless_server_and_client(true)
local server_exec_lua = tt.make_lua_executor(server)
if not server_exec_lua('return pcall(require, "ffi")') then
pending('N/A: missing LuaJIT FFI')
end
-- Use FFI to send a chdir event to a non-directory path.
server_exec_lua(ffi_str_defs .. [[
ffi.cdef('void ui_call_chdir(String path);')
ffi.C.ui_call_chdir(to_api_string('README.md'))
]])
screen_client:expect_unchanged()
assert_log('Failed to chdir to README.md: not a directory', testlog)
end)
it('nvim_ui_send works with remote client #36317', function()
local server, _, _ = start_headless_server_and_client(false)
server:request('nvim_ui_send', '\027]2;TEST_TITLE\027\\')
retry(nil, nil, function()
eq('TEST_TITLE', api.nvim_buf_get_var(0, 'term_title'))
end)
end)
it('throws error when no server exists', function()
clear()
local screen = tt.setup_child_nvim({
'--remote-ui',
'--server',
'127.0.0.1:2436546',
}, { cols = 60 })
screen:expect([[
Remote ui failed to start: {MATCH:.*}|
|
[Process exited 1]^ |
|*3
{5:-- TERMINAL --} |
]])
end)
local function test_remote_tui_quit(status)
local server_super, screen_server, screen_client = start_tui_and_remote_client()
-- quitting the server
set_session(server_super)
feed_data(status and ':' .. status .. 'cquit!\n' or ':quit!\n')
status = status and status or 0
screen_server:expect({ any = 'Process exited ' .. status })
screen_client:expect({ any = 'Process exited ' .. status })
end
describe('exits when server quits', function()
it('with :quit', function()
test_remote_tui_quit()
end)
it('with :cquit', function()
test_remote_tui_quit(42)
end)
end)
it('suspend/resume works with multiple clients', function()
t.skip(is_os('win'), 'N/A for Windows')
local server_super, screen_server, screen_client = start_tui_and_remote_client()
local screen_normal = [[
Hello, Worl^d |
{100:~ }|*3
{3:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]]
local screen_suspended = [[
|*5
^[Process suspended] |
{5:-- TERMINAL --} |
]]
screen_client:expect({ grid = screen_normal, unchanged = true })
screen_server:expect({ grid = screen_normal, unchanged = true })
-- Suspend both clients.
feed_data(':suspend\r')
screen_client:expect({ grid = screen_suspended })
screen_server:expect({ grid = screen_suspended })
-- Resume the remote client.
n.feed('<Space>')
screen_client:expect({ grid = screen_normal })
screen_server:expect({ grid = screen_suspended, unchanged = true })
-- Resume the embedding client.
server_super:request('nvim_input', '<Space>')
screen_server:expect({ grid = screen_normal })
screen_client:expect({ grid = screen_normal, unchanged = true })
-- Suspend both clients again.
feed_data(':suspend\r')
screen_client:expect({ grid = screen_suspended })
screen_server:expect({ grid = screen_suspended })
-- Resume the remote client.
n.feed('<Space>')
screen_client:expect({ grid = screen_normal })
screen_server:expect({ grid = screen_suspended, unchanged = true })
-- Suspend the remote client again.
feed_data(':suspend\r')
screen_client:expect({ grid = screen_suspended })
screen_server:expect({ grid = screen_suspended, unchanged = true })
-- Resume the embedding client.
server_super:request('nvim_input', '<Space>')
screen_server:expect({ grid = screen_normal })
screen_client:expect({ grid = screen_suspended, unchanged = true })
-- Resume the remote client.
n.feed('<Space>')
screen_client:expect({ grid = screen_normal })
screen_server:expect({ grid = screen_normal, unchanged = true })
feed_data(':quit!\r')
screen_server:expect({ any = vim.pesc('[Process exited 0]') })
screen_client:expect({ any = vim.pesc('[Process exited 0]') })
end)
end)