neovim/test/functional/ex_cmds/cmd_map_spec.lua
bfredl e61228a214 fix(tests): needing two calls to setup a screen is cringe
Before calling "attach" a screen object is just a dummy container for
(row, col) values whose purpose is to be sent as part of the "attach"
function call anyway.

Just create the screen in an attached state directly. Keep the complete
(row, col, options) config together. It is still completely valid to
later detach and re-attach as needed, including to another session.
2024-11-14 12:40:57 +01:00

745 lines
28 KiB
Lua

local t = require('test.testutil')
local n = require('test.functional.testnvim')()
local Screen = require('test.functional.ui.screen')
local clear = n.clear
local feed = n.feed
local eq = t.eq
local expect = n.expect
local eval = n.eval
local fn = n.fn
local insert = n.insert
local write_file = t.write_file
local exc_exec = n.exc_exec
local command = n.command
describe('mappings with <Cmd>', function()
local screen
local tmpfile = 'X_ex_cmds_cmd_map'
local function cmdmap(lhs, rhs)
command('noremap ' .. lhs .. ' <Cmd>' .. rhs .. '<cr>')
command('noremap! ' .. lhs .. ' <Cmd>' .. rhs .. '<cr>')
end
before_each(function()
clear()
screen = Screen.new(65, 8)
screen:set_default_attr_ids({
[1] = { bold = true, foreground = Screen.colors.Blue1 },
[2] = { foreground = Screen.colors.Grey100, background = Screen.colors.Red },
[3] = { bold = true, foreground = Screen.colors.SeaGreen4 },
[4] = { bold = true },
[5] = { foreground = Screen.colors.Black, background = Screen.colors.LightGrey },
[6] = { foreground = Screen.colors.Blue1 },
[7] = { bold = true, reverse = true },
[8] = { background = Screen.colors.WebGray },
[9] = { background = Screen.colors.LightMagenta },
[10] = { foreground = Screen.colors.Red },
})
cmdmap('<F3>', 'let m = mode(1)')
cmdmap('<F4>', 'normal! ww')
cmdmap('<F5>', 'normal! "ay')
cmdmap('<F6>', 'throw "very error"')
command([[
function! TextObj()
if mode() !=# "v"
normal! v
end
call cursor(1,3)
normal! o
call cursor(2,4)
endfunction]])
cmdmap('<F7>', 'call TextObj()')
insert([[
some short lines
of test text]])
feed('gg')
cmdmap('<F8>', 'startinsert')
cmdmap('<F9>', 'stopinsert')
command('abbr foo <Cmd>let g:y = 17<cr>bar')
end)
after_each(function()
os.remove(tmpfile)
end)
it('can be displayed', function()
command('map <F3>')
screen:expect([[
^some short lines |
of test text |
{1:~ }|*5
{6:<F3>} {6:*} {6:<Cmd>}let m = mode(1){6:<CR>} |
]])
end)
it('handles invalid mappings', function()
command('let x = 0')
command('noremap <F3> <Cmd><Cmd>let x = 1<cr>')
feed('<F3>')
screen:expect([[
^some short lines |
of test text |
{1:~ }|*5
{2:E1136: <Cmd> mapping must end with <CR> before second <Cmd>} |
]])
command('noremap <F3> <Cmd>let x = 3')
feed('<F3>')
screen:expect([[
^some short lines |
of test text |
{1:~ }|*5
{2:E1255: <Cmd> mapping must end with <CR>} |
]])
eq(0, eval('x'))
end)
it('allows special keys and modifiers', function()
command('noremap <F3> <Cmd>normal! <Down><CR>')
feed('<F3>')
screen:expect([[
some short lines |
^of test text |
{1:~ }|*5
|
]])
command('noremap <F3> <Cmd>normal! <C-Right><CR>')
feed('<F3>')
screen:expect([[
some short lines |
of ^test text |
{1:~ }|*5
|
]])
end)
it('handles string containing K_SPECIAL (0x80) bytes correctly', function()
command([[noremap <F3> <Cmd>let g:str = 'foo…bar'<CR>]])
feed('<F3>')
eq('foo…bar', eval('g:str'))
local str = eval([["foo\<D-…>bar"]])
command([[noremap <F3> <Cmd>let g:str = ']] .. str .. [['<CR>]])
feed('<F3>')
eq(str, eval('g:str'))
command([[noremap <F3> <Cmd>let g:str = 'foo<D-…>bar'<CR>]])
feed('<F3>')
eq(str, eval('g:str'))
end)
it('works in various modes and sees correct `mode()` value', function()
-- normal mode
feed('<F3>')
eq('n', eval('m'))
-- visual mode
feed('v<F3>')
eq('v', eval('m'))
-- didn't leave visual mode
eq('v', eval('mode(1)'))
feed('<esc>')
eq('n', eval('mode(1)'))
-- visual mapping in select mode
feed('gh<F3>')
eq('v', eval('m'))
-- didn't leave select mode
eq('s', eval('mode(1)'))
feed('<esc>')
eq('n', eval('mode(1)'))
-- select mode mapping
command('snoremap <F3> <Cmd>let m = mode(1)<cr>')
feed('gh<F3>')
eq('s', eval('m'))
-- didn't leave select mode
eq('s', eval('mode(1)'))
feed('<esc>')
eq('n', eval('mode(1)'))
-- operator-pending mode
feed('d<F3>')
eq('no', eval('m'))
-- did leave operator-pending mode
eq('n', eval('mode(1)'))
--insert mode
feed('i<F3>')
eq('i', eval('m'))
eq('i', eval('mode(1)'))
-- replace mode
feed('<Ins><F3>')
eq('R', eval('m'))
eq('R', eval('mode(1)'))
feed('<esc>')
eq('n', eval('mode(1)'))
-- virtual replace mode
feed('gR<F3>')
eq('Rv', eval('m'))
eq('Rv', eval('mode(1)'))
feed('<esc>')
eq('n', eval('mode(1)'))
-- langmap works, but is not distinguished in mode(1)
feed(':set iminsert=1<cr>i<F3>')
eq('i', eval('m'))
eq('i', eval('mode(1)'))
feed('<esc>')
eq('n', eval('mode(1)'))
feed(':<F3>')
eq('c', eval('m'))
eq('c', eval('mode(1)'))
feed('<esc>')
eq('n', eval('mode(1)'))
-- terminal mode
command('tnoremap <F3> <Cmd>let m = mode(1)<cr>')
command('split | terminal')
feed('i')
eq('t', eval('mode(1)'))
feed('<F3>')
eq('t', eval('m'))
eq('t', eval('mode(1)'))
end)
it('works in normal mode', function()
cmdmap('<F2>', 'let s = [mode(1), v:count, v:register]')
-- check v:count and v:register works
feed('<F2>')
eq({ 'n', 0, '"' }, eval('s'))
feed('7<F2>')
eq({ 'n', 7, '"' }, eval('s'))
feed('"e<F2>')
eq({ 'n', 0, 'e' }, eval('s'))
feed('5"k<F2>')
eq({ 'n', 5, 'k' }, eval('s'))
feed('"+2<F2>')
eq({ 'n', 2, '+' }, eval('s'))
-- text object enters visual mode
feed('<F7>')
screen:expect([[
so{5:me short lines} |
{5:of }^test text |
{1:~ }|*5
{4:-- VISUAL --} |
]])
feed('<esc>')
-- startinsert
feed('<F8>')
eq('i', eval('mode(1)'))
feed('<esc>')
eq('n', eval('mode(1)'))
cmdmap(',a', 'call feedkeys("aalpha") \\| let g:a = getline(2)')
cmdmap(',b', 'call feedkeys("abeta", "x") \\| let g:b = getline(2)')
feed(',a<F3>')
screen:expect([[
some short lines |
of alpha^test text |
{1:~ }|*5
{4:-- INSERT --} |
]])
-- feedkeys were not executed immediately
eq({ 'n', 'of test text' }, eval('[m,a]'))
eq('i', eval('mode(1)'))
feed('<esc>')
feed(',b<F3>')
screen:expect([[
some short lines |
of alphabet^atest text |
{1:~ }|*5
|
]])
-- feedkeys(..., 'x') was executed immediately, but insert mode gets aborted
eq({ 'n', 'of alphabetatest text' }, eval('[m,b]'))
eq('n', eval('mode(1)'))
end)
it('works in :normal command', function()
command('noremap ,x <Cmd>call append(1, "xx")\\| call append(1, "aa")<cr>')
command('noremap ,f <Cmd>nosuchcommand<cr>')
command('noremap ,e <Cmd>throw "very error"\\| call append(1, "yy")<cr>')
command('noremap ,m <Cmd>echoerr "The message."\\| call append(1, "zz")<cr>')
command(
'noremap ,w <Cmd>for i in range(5)\\|if i==1\\|echoerr "Err"\\|endif\\|call append(1, i)\\|endfor<cr>'
)
feed(':normal ,x<cr>')
screen:expect([[
^some short lines |
aa |
xx |
of test text |
{1:~ }|*3
:normal ,x |
]])
eq('Vim:E492: Not an editor command: nosuchcommand', exc_exec('normal ,f'))
eq('very error', exc_exec('normal ,e'))
eq('Vim(echoerr):The message.', exc_exec('normal ,m'))
feed('w')
screen:expect([[
some ^short lines |
aa |
xx |
of test text |
{1:~ }|*3
:normal ,x |
]])
command(':%d')
eq('Vim(echoerr):Err', exc_exec('normal ,w'))
screen:expect([[
^ |
0 |
{1:~ }|*5
--No lines in buffer-- |
]])
command(':%d')
feed(':normal ,w<cr>')
screen:expect([[
^ |
4 |
3 |
2 |
1 |
0 |
{1:~ }|
{2:Err} |
]])
end)
it('works in visual mode', function()
-- can extend visual mode
feed('v<F4>')
screen:expect([[
{5:some short }^lines |
of test text |
{1:~ }|*5
{4:-- VISUAL --} |
]])
eq('v', fn.mode(1))
-- can invoke operator, ending visual mode
feed('<F5>')
eq('n', fn.mode(1))
eq({ 'some short l' }, fn.getreg('a', 1, 1))
-- error doesn't interrupt visual mode
feed('ggvw<F6>')
screen:expect([[
{5:some }short lines |
of test text |
{1:~ }|*2
{7: }|
{2:Error detected while processing :} |
{2:E605: Exception not caught: very error} |
{3:Press ENTER or type command to continue}^ |
]])
feed('<cr>')
eq('E605: Exception not caught: very error', eval('v:errmsg'))
-- still in visual mode, <cr> was consumed by the error prompt
screen:expect([[
{5:some }^short lines |
of test text |
{1:~ }|*5
{4:-- VISUAL --} |
]])
eq('v', fn.mode(1))
feed('<F7>')
screen:expect([[
so{5:me short lines} |
{5:of }^test text |
{1:~ }|*5
{4:-- VISUAL --} |
]])
eq('v', fn.mode(1))
-- startinsert gives "-- (insert) VISUAL --" mode
feed('<F8>')
screen:expect([[
so{5:me short lines} |
{5:of }^test text |
{1:~ }|*5
{4:-- (insert) VISUAL --} |
]])
eq('v', eval('mode(1)'))
feed('<esc>')
eq('i', eval('mode(1)'))
end)
it('works in select mode', function()
command('snoremap <F1> <cmd>throw "very error"<cr>')
command('snoremap <F2> <cmd>normal! <c-g>"by<cr>')
-- can extend select mode
feed('gh<F4>')
screen:expect([[
{5:some short }^lines |
of test text |
{1:~ }|*5
{4:-- SELECT --} |
]])
eq('s', fn.mode(1))
-- visual mapping in select mode restart select mode after operator
feed('<F5>')
eq('s', fn.mode(1))
eq({ 'some short l' }, fn.getreg('a', 1, 1))
-- select mode mapping works, and does not restart select mode
feed('<F2>')
eq('n', fn.mode(1))
eq({ 'some short l' }, fn.getreg('b', 1, 1))
-- error doesn't interrupt temporary visual mode
feed('<esc>ggvw<c-g><F6>')
screen:expect([[
{5:some }short lines |
of test text |
{1:~ }|*2
{7: }|
{2:Error detected while processing :} |
{2:E605: Exception not caught: very error} |
{3:Press ENTER or type command to continue}^ |
]])
feed('<cr>')
eq('E605: Exception not caught: very error', eval('v:errmsg'))
-- still in visual mode, <cr> was consumed by the error prompt
screen:expect([[
{5:some }^short lines |
of test text |
{1:~ }|*5
{4:-- VISUAL --} |
]])
-- quirk: restoration of select mode is not performed
eq('v', fn.mode(1))
-- error doesn't interrupt select mode
feed('<esc>ggvw<c-g><F1>')
screen:expect([[
{5:some }short lines |
of test text |
{1:~ }|*2
{7: }|
{2:Error detected while processing :} |
{2:E605: Exception not caught: very error} |
{3:Press ENTER or type command to continue}^ |
]])
feed('<cr>')
eq('E605: Exception not caught: very error', eval('v:errmsg'))
-- still in select mode, <cr> was consumed by the error prompt
screen:expect([[
{5:some }^short lines |
of test text |
{1:~ }|*5
{4:-- SELECT --} |
]])
-- quirk: restoration of select mode is not performed
eq('s', fn.mode(1))
feed('<F7>')
screen:expect([[
so{5:me short lines} |
{5:of }^test text |
{1:~ }|*5
{4:-- SELECT --} |
]])
eq('s', fn.mode(1))
-- startinsert gives "-- SELECT (insert) --" mode
feed('<F8>')
screen:expect([[
so{5:me short lines} |
{5:of }^test text |
{1:~ }|*5
{4:-- (insert) SELECT --} |
]])
eq('s', eval('mode(1)'))
feed('<esc>')
eq('i', eval('mode(1)'))
end)
it('works in operator-pending mode', function()
feed('d<F4>')
expect([[
lines
of test text]])
eq({ 'some short ' }, fn.getreg('"', 1, 1))
feed('.')
expect([[
test text]])
eq({ 'lines', 'of ' }, fn.getreg('"', 1, 1))
feed('uu')
expect([[
some short lines
of test text]])
-- error aborts operator-pending, operator not performed
feed('d<F6>')
screen:expect([[
some short lines |
of test text |
{1:~ }|*2
{7: }|
{2:Error detected while processing :} |
{2:E605: Exception not caught: very error} |
{3:Press ENTER or type command to continue}^ |
]])
feed('<cr>')
eq('E605: Exception not caught: very error', eval('v:errmsg'))
expect([[
some short lines
of test text]])
feed('"bd<F7>')
expect([[
soest text]])
eq({ 'me short lines', 'of t' }, fn.getreg('b', 1, 1))
-- startinsert aborts operator
feed('d<F8>')
eq('i', eval('mode(1)'))
expect([[
soest text]])
end)
it('works in insert mode', function()
-- works the same as <c-o>w<c-o>w
feed('iindeed <F4>little ')
screen:expect([[
indeed some short little ^lines |
of test text |
{1:~ }|*5
{4:-- INSERT --} |
]])
feed('<F6>')
screen:expect([[
indeed some short little lines |
of test text |
{1:~ }|*2
{7: }|
{2:Error detected while processing :} |
{2:E605: Exception not caught: very error} |
{3:Press ENTER or type command to continue}^ |
]])
feed('<cr>')
eq('E605: Exception not caught: very error', eval('v:errmsg'))
-- still in insert
screen:expect([[
indeed some short little ^lines |
of test text |
{1:~ }|*5
{4:-- INSERT --} |
]])
eq('i', eval('mode(1)'))
-- When entering visual mode from InsertEnter autocmd, an async event, or
-- a <cmd> mapping, vim ends up in undocumented "INSERT VISUAL" mode. If a
-- vim patch decides to disable this mode, this test is expected to fail.
feed('<F7>stuff ')
screen:expect([[
in{5:deed some short little lines} |
{5:of stuff }^test text |
{1:~ }|*5
{4:-- INSERT VISUAL --} |
]])
expect([[
indeed some short little lines
of stuff test text]])
feed('<F5>')
eq({ 'deed some short little lines', 'of stuff t' }, fn.getreg('a', 1, 1))
-- still in insert
screen:expect([[
in^deed some short little lines |
of stuff test text |
{1:~ }|*5
{4:-- INSERT --} |
]])
eq('i', eval('mode(1)'))
-- also works as part of abbreviation
feed('<space>foo ')
screen:expect([[
in bar ^deed some short little lines |
of stuff test text |
{1:~ }|*5
{4:-- INSERT --} |
]])
eq(17, eval('g:y'))
-- :startinsert does nothing
feed('<F8>')
eq('i', eval('mode(1)'))
-- :stopinsert works
feed('<F9>')
eq('n', eval('mode(1)'))
end)
it('works in insert completion (Ctrl-X) mode', function()
feed('os<c-x><c-n>')
screen:expect([[
some short lines |
some^ |
{8:some } |
{9:short }{1: }|
{1:~ }|*3
{4:-- Keyword Local completion (^N^P) }{3:match 1 of 2} |
]])
feed('<f3>')
eq('ic', eval('m'))
-- ensure a redraw, this would have moved the cursor
-- instead if CTRL-X mode was left.
feed('<up>')
screen:expect([[
some short lines |
some^ |
{9:some } |
{9:short }{1: }|
{1:~ }|*3
{4:-- Keyword Local completion (^N^P) }{10:Back at original} |
]])
end)
it('works in cmdline mode', function()
feed(':text<F3>')
eq('c', eval('m'))
-- didn't leave cmdline mode
eq('c', eval('mode(1)'))
feed('<cr>')
eq('n', eval('mode(1)'))
screen:expect([[
^some short lines |
of test text |
{1:~ }|*5
{2:E492: Not an editor command: text} |
]])
feed(':echo 2<F6>')
screen:expect([[
some short lines |
of test text |
{1:~ }|
{7: }|
:echo 2 |
{2:Error detected while processing :} |
{2:E605: Exception not caught: very error} |
:echo 2^ |
]])
eq('E605: Exception not caught: very error', eval('v:errmsg'))
-- didn't leave cmdline mode
eq('c', eval('mode(1)'))
feed('+2<cr>')
screen:expect([[
some short lines |
of test text |
{7: }|
:echo 2 |
{2:Error detected while processing :} |
{2:E605: Exception not caught: very error} |
4 |
{3:Press ENTER or type command to continue}^ |
]])
-- however, message scrolling may cause extra CR prompt
-- This is consistent with output from async events.
feed('<cr>')
screen:expect([[
^some short lines |
of test text |
{1:~ }|*5
|
]])
eq('n', eval('mode(1)'))
feed(':let g:x = 3<F4>')
screen:expect([[
some short lines |
of test text |
{1:~ }|*5
:let g:x = 3^ |
]])
feed('+2<cr>')
-- cursor was moved in the background
screen:expect([[
some short ^lines |
of test text |
{1:~ }|*5
:let g:x = 3+2 |
]])
eq(5, eval('g:x'))
feed(':let g:y = 7<F8>')
screen:expect([[
some short lines |
of test text |
{1:~ }|*5
:let g:y = 7^ |
]])
eq('c', eval('mode(1)'))
feed('+2<cr>')
-- startinsert takes effect after leaving cmdline mode
screen:expect([[
some short ^lines |
of test text |
{1:~ }|*5
{4:-- INSERT --} |
]])
eq('i', eval('mode(1)'))
eq(9, eval('g:y'))
end)
it("doesn't crash when invoking cmdline mode recursively #8859", function()
cmdmap('<F2>', 'norm! :foo')
feed(':bar')
screen:expect([[
some short lines |
of test text |
{1:~ }|*5
:bar^ |
]])
feed('<f2>x')
screen:expect([[
some short lines |
of test text |
{1:~ }|*5
:barx^ |
]])
end)
it('works with <SID> mappings', function()
command('new!')
write_file(
tmpfile,
[[
map <f2> <Cmd>call <SID>do_it()<Cr>
function! s:do_it()
let g:x = 10
endfunction
]]
)
command('source ' .. tmpfile)
feed('<f2>')
eq('', eval('v:errmsg'))
eq(10, eval('g:x'))
end)
end)