diff options
author | Yilin Yang <yiliny@umich.edu> | 2019-05-12 11:44:48 +0200 |
---|---|---|
committer | Justin M. Keyes <justinkz@gmail.com> | 2019-05-12 11:44:48 +0200 |
commit | fbf2c414ad3409e8359ff744765e7486043bb4f7 (patch) | |
tree | 980bbbe5ce39cd6d82ee28fbef9b9ce1fb5949ab /test/functional/api/keymap_spec.lua | |
parent | 24f9dd73d5f78bad48ac6dd4e2615ccdb95d9daa (diff) | |
download | rneovim-fbf2c414ad3409e8359ff744765e7486043bb4f7.tar.gz rneovim-fbf2c414ad3409e8359ff744765e7486043bb4f7.tar.bz2 rneovim-fbf2c414ad3409e8359ff744765e7486043bb4f7.zip |
API: nvim_set_keymap, nvim_del_keymap #9924
closes #9136
- Treat empty {rhs} like <Nop>
- getchar.c: Pull "repl. MapArg termcodes" into func
The "preprocessing code" surrounding the replace_termcodes calls needs
to invoke replace_termcodes, and also check if RHS is equal to "<Nop>".
To reduce code duplication, factor this out into a helper function.
Also add an rhs_is_noop flag to MapArguments; buf_do_map_explicit
expects an empty {rhs} string for "<Nop>", but also needs to distinguish
that from something like ":map lhs<cr>" where no {rhs} was provided.
- getchar.c: Use allocated buffer for rhs in MapArgs
Since the MAXMAPLEN limit does not apply to the RHS of a mapping (or
else an RHS that calls a really long autoload function from a plugin
would be incorrectly rejected as being too long), use an allocated
buffer for RHS rather than a static buffer of length MAXMAPLEN + 1.
- Mappings LHS and RHS can contain literal space characters, newlines, etc.
- getchar.c: replace_termcodes in str_to_mapargs
It makes sense to do this; str_to_mapargs is, intuitively, supposed to
take a "raw" command string and parse it into a totally "do_map-ready"
struct.
- api/vim.c: Update lhs, rhs len after replace_termcodes
Fixes a bug in which replace_termcodes changes the length of lhs or rhs,
but the later search through the mappings/abbreviations hashtables
still uses the old length value. This would cause the search to fail
erroneously and throw 'E31: No such mapping' errors or 'E24: No such
abbreviation' errors.
- getchar: Create new map_arguments struct
So that a string of map arguments can be parsed into a more useful, more
portable data structure.
- getchar.c: Add buf_do_map function
Exactly the same as the old do_map, but replace the hardcoded references
to the global `buf_T* curbuf` with a function parameter so that we can
invoke it from nvim_buf_set_keymap.
- Remove gettext calls in do_map error handling
Diffstat (limited to 'test/functional/api/keymap_spec.lua')
-rw-r--r-- | test/functional/api/keymap_spec.lua | 505 |
1 files changed, 504 insertions, 1 deletions
diff --git a/test/functional/api/keymap_spec.lua b/test/functional/api/keymap_spec.lua index f52372bee3..47478dce5d 100644 --- a/test/functional/api/keymap_spec.lua +++ b/test/functional/api/keymap_spec.lua @@ -1,15 +1,19 @@ local helpers = require('test.functional.helpers')(after_each) local global_helpers = require('test.helpers') +local bufmeths = helpers.bufmeths local clear = helpers.clear local command = helpers.command local curbufmeths = helpers.curbufmeths -local eq = helpers.eq +local eq, neq = helpers.eq, helpers.neq +local expect_err = helpers.expect_err +local feed = helpers.feed local funcs = helpers.funcs local meths = helpers.meths local source = helpers.source local shallowcopy = global_helpers.shallowcopy +local sleep = global_helpers.sleep describe('nvim_get_keymap', function() before_each(clear) @@ -308,3 +312,502 @@ describe('nvim_get_keymap', function() eq({space_table}, meths.get_keymap('n')) end) end) + +describe('nvim_[set/del]_keymap', function() + before_each(clear) + + -- generate_expected is truthy when we want to generate an expected output for + -- maparg(); mapargs() won't take '!' as an input, though it will return '!' + -- in its output if getting a mapping set with |:map!| + local function normalize_mapmode(mode, generate_expected) + if not generate_expected and mode == '!' then + -- can't retrieve mapmode-ic mappings with '!', but can with 'i' or 'c'. + mode = 'i' + elseif mode == '' or mode == ' ' or mode == 'm' then + mode = generate_expected and ' ' or 'm' + end + return mode + end + + -- Generate a mapargs dict, for comparison against the mapping that was + -- actually set + local function generate_mapargs(mode, lhs, rhs, opts) + if not opts then + opts = {} + end + + local to_return = {} + to_return.mode = normalize_mapmode(mode, true) + to_return.noremap = not opts.noremap and 0 or 1 + to_return.lhs = lhs + to_return.rhs = rhs + to_return.silent = not opts.silent and 0 or 1 + to_return.nowait = not opts.nowait and 0 or 1 + to_return.expr = not opts.expr and 0 or 1 + to_return.sid = not opts.sid and 0 or opts.sid + to_return.buffer = not opts.buffer and 0 or opts.buffer + + -- mode 't' doesn't print when calling maparg + if mode == 't' then + to_return.mode = '' + end + + return to_return + end + + -- Retrieve a mapargs dict from neovim, if one exists + local function get_mapargs(mode, lhs) + return funcs.maparg(lhs, normalize_mapmode(mode), false, true) + end + + -- Test error handling + it('throws errors when given empty lhs', function() + -- escape parentheses in lua string, else comparison fails erroneously + expect_err('Invalid %(empty%) LHS', + meths.set_keymap, '', '', 'rhs', {}) + expect_err('Invalid %(empty%) LHS', + meths.set_keymap, '', '', '', {}) + + expect_err('Invalid %(empty%) LHS', meths.del_keymap, '', '') + end) + + it('throws errors when given an lhs longer than MAXMAPLEN', function() + -- assume MAXMAPLEN of 50 chars, as declared in vim.h + local MAXMAPLEN = 50 + local lhs = '' + for i=1,MAXMAPLEN do + lhs = lhs..(i % 10) + end + + -- exactly 50 chars should be fine + meths.set_keymap('', lhs, 'rhs', {}) + + -- del_keymap should unmap successfully + meths.del_keymap('', lhs) + eq({}, get_mapargs('', lhs)) + + -- 51 chars should produce an error + lhs = lhs..'1' + expect_err('LHS exceeds maximum map length: '..lhs, + meths.set_keymap, '', lhs, 'rhs', {}) + expect_err('LHS exceeds maximum map length: '..lhs, + meths.del_keymap, '', lhs) + end) + + it('does not throw errors when rhs is longer than MAXMAPLEN', function() + local MAXMAPLEN = 50 + local rhs = '' + for i=1,MAXMAPLEN do + rhs = rhs..(i % 10) + end + rhs = rhs..'1' + meths.set_keymap('', 'lhs', rhs, {}) + eq(generate_mapargs('', 'lhs', rhs), + get_mapargs('', 'lhs')) + end) + + it('throws errors when given too-long mode shortnames', function() + expect_err('Shortname is too long: map', + meths.set_keymap, 'map', 'lhs', 'rhs', {}) + + expect_err('Shortname is too long: vmap', + meths.set_keymap, 'vmap', 'lhs', 'rhs', {}) + + expect_err('Shortname is too long: xnoremap', + meths.set_keymap, 'xnoremap', 'lhs', 'rhs', {}) + + expect_err('Shortname is too long: map', meths.del_keymap, 'map', 'lhs') + expect_err('Shortname is too long: vmap', meths.del_keymap, 'vmap', 'lhs') + expect_err('Shortname is too long: xnoremap', meths.del_keymap, 'xnoremap', 'lhs') + end) + + it('throws errors when given unrecognized mode shortnames', function() + expect_err('Invalid mode shortname: ?', + meths.set_keymap, '?', 'lhs', 'rhs', {}) + + expect_err('Invalid mode shortname: y', + meths.set_keymap, 'y', 'lhs', 'rhs', {}) + + expect_err('Invalid mode shortname: p', + meths.set_keymap, 'p', 'lhs', 'rhs', {}) + + expect_err('Invalid mode shortname: ?', meths.del_keymap, '?', 'lhs') + expect_err('Invalid mode shortname: y', meths.del_keymap, 'y', 'lhs') + expect_err('Invalid mode shortname: p', meths.del_keymap, 'p', 'lhs') + end) + + it('throws errors when optnames are almost right', function() + expect_err('Invalid key: silentt', + meths.set_keymap, 'n', 'lhs', 'rhs', {silentt = true}) + expect_err('Invalid key: sidd', + meths.set_keymap, 'n', 'lhs', 'rhs', {sidd = false}) + expect_err('Invalid key: nowaiT', + meths.set_keymap, 'n', 'lhs', 'rhs', {nowaiT = false}) + end) + + it('does not recognize <buffer> as an option', function() + expect_err('Invalid key: buffer', + meths.set_keymap, 'n', 'lhs', 'rhs', {buffer = true}) + end) + + local optnames = {'nowait', 'silent', 'script', 'expr', 'unique'} + for _, opt in ipairs(optnames) do + -- note: need '%' to escape hyphens, which have special meaning in lua + it('throws an error when given non-boolean value for '..opt, function() + local opts = {} + opts[opt] = 2 + expect_err('Gave non%-boolean value for an opt: '..opt, + meths.set_keymap, 'n', 'lhs', 'rhs', opts) + end) + end + + -- Perform tests of basic functionality + it('can set ordinary mappings', function() + meths.set_keymap('n', 'lhs', 'rhs', {}) + eq(generate_mapargs('n', 'lhs', 'rhs'), get_mapargs('n', 'lhs')) + + meths.set_keymap('v', 'lhs', 'rhs', {}) + eq(generate_mapargs('v', 'lhs', 'rhs'), get_mapargs('v', 'lhs')) + end) + + it('doesn\'t throw when lhs or rhs have leading/trailing WS', function() + meths.set_keymap('n', ' lhs', 'rhs', {}) + eq(generate_mapargs('n', '<Space><Space><Space>lhs', 'rhs'), + get_mapargs('n', ' lhs')) + + meths.set_keymap('n', 'lhs ', 'rhs', {}) + eq(generate_mapargs('n', 'lhs<Space><Space><Space><Space>', 'rhs'), + get_mapargs('n', 'lhs ')) + + meths.set_keymap('v', ' lhs ', '\trhs\t\f', {}) + eq(generate_mapargs('v', '<Space>lhs<Space><Space>', '\trhs\t\f'), + get_mapargs('v', ' lhs ')) + end) + + it('can set noremap mappings', function() + meths.set_keymap('x', 'lhs', 'rhs', {noremap = true}) + eq(generate_mapargs('x', 'lhs', 'rhs', {noremap = true}), + get_mapargs('x', 'lhs')) + + meths.set_keymap('t', 'lhs', 'rhs', {noremap = true}) + eq(generate_mapargs('t', 'lhs', 'rhs', {noremap = true}), + get_mapargs('t', 'lhs')) + end) + + it('can unmap mappings', function() + meths.set_keymap('v', 'lhs', 'rhs', {}) + meths.del_keymap('v', 'lhs') + eq({}, get_mapargs('v', 'lhs')) + + meths.set_keymap('t', 'lhs', 'rhs', {noremap = true}) + meths.del_keymap('t', 'lhs') + eq({}, get_mapargs('t', 'lhs')) + end) + + -- Test some edge cases + it('accepts "!" and " " and "" as synonyms for mapmode-nvo', function() + local nvo_shortnames = {'', ' ', '!'} + for _, name in ipairs(nvo_shortnames) do + meths.set_keymap(name, 'lhs', 'rhs', {}) + meths.del_keymap(name, 'lhs') + eq({}, get_mapargs(name, 'lhs')) + end + end) + + local special_chars = {'<C-U>', '<S-Left>', '<F12><F2><Tab>', '<Space><Tab>'} + for _, lhs in ipairs(special_chars) do + for _, rhs in ipairs(special_chars) do + local mapmode = '!' + it('can set mappings with special characters, lhs: '..lhs..', rhs: '..rhs, + function() + meths.set_keymap(mapmode, lhs, rhs, {}) + eq(generate_mapargs(mapmode, lhs, rhs), get_mapargs(mapmode, lhs)) + end) + end + end + + it('can set mappings containing literal keycodes', function() + meths.set_keymap('n', '\n\r\n', 'rhs', {}) + local expected = generate_mapargs('n', '<NL><CR><NL>', 'rhs') + eq(expected, get_mapargs('n', '<C-j><CR><C-j>')) + end) + + it('can set mappings whose RHS is a <Nop>', function() + meths.set_keymap('i', 'lhs', '<Nop>', {}) + command('normal ilhs') + eq({''}, curbufmeths.get_lines(0, -1, 0)) -- imap to <Nop> does nothing + eq(generate_mapargs('i', 'lhs', '<Nop>', {}), + get_mapargs('i', 'lhs')) + + -- also test for case insensitivity + meths.set_keymap('i', 'lhs', '<nOp>', {}) + command('normal ilhs') + eq({''}, curbufmeths.get_lines(0, -1, 0)) + -- note: RHS in returned mapargs() dict reflects the original RHS + -- provided by the user + eq(generate_mapargs('i', 'lhs', '<nOp>', {}), + get_mapargs('i', 'lhs')) + + meths.set_keymap('i', 'lhs', '<NOP>', {}) + command('normal ilhs') + eq({''}, curbufmeths.get_lines(0, -1, 0)) + eq(generate_mapargs('i', 'lhs', '<NOP>', {}), + get_mapargs('i', 'lhs')) + end) + + it('treats an empty RHS in a mapping like a <Nop>', function() + meths.set_keymap('i', 'lhs', '', {}) + command('normal ilhs') + eq({''}, curbufmeths.get_lines(0, -1, 0)) + eq(generate_mapargs('i', 'lhs', '', {}), + get_mapargs('i', 'lhs')) + end) + + it('can set and unset <M-">', function() + -- Taken from the legacy test: test_mapping.vim. Exposes a bug in which + -- replace_termcodes changes the length of the mapping's LHS, but + -- do_map continues to use the *old* length of LHS. + meths.set_keymap('i', '<M-">', 'foo', {}) + meths.del_keymap('i', '<M-">') + eq({}, get_mapargs('i', '<M-">')) + end) + + it('interprets control sequences in expr-quotes correctly when called ' + ..'inside vim', function() + command([[call nvim_set_keymap('i', "\<space>", "\<tab>", {})]]) + eq(generate_mapargs('i', '<Space>', '\t', {}), + get_mapargs('i', '<Space>')) + feed('i ') + eq({'\t'}, curbufmeths.get_lines(0, -1, 0)) + end) + + it('throws appropriate error messages when setting <unique> maps', function() + meths.set_keymap('l', 'lhs', 'rhs', {}) + expect_err('E227: mapping already exists for lhs', + meths.set_keymap, 'l', 'lhs', 'rhs', {unique = true}) + -- different mapmode, no error should be thrown + meths.set_keymap('t', 'lhs', 'rhs', {unique = true}) + end) + + it('can set <expr> mappings whose RHS change dynamically', function() + meths.command_output([[ + function! FlipFlop() abort + if !exists('g:flip') | let g:flip = 0 | endif + let g:flip = !g:flip + return g:flip + endfunction + ]]) + eq(1, meths.call_function('FlipFlop', {})) + eq(0, meths.call_function('FlipFlop', {})) + eq(1, meths.call_function('FlipFlop', {})) + eq(0, meths.call_function('FlipFlop', {})) + + meths.set_keymap('i', 'lhs', 'FlipFlop()', {expr = true}) + command('normal ilhs') + eq({'1'}, curbufmeths.get_lines(0, -1, 0)) + + command('normal! ggVGd') + + command('normal ilhs') + eq({'0'}, curbufmeths.get_lines(0, -1, 0)) + end) + + it('can set mappings that do trigger other mappings', function() + meths.set_keymap('i', 'mhs', 'rhs', {}) + meths.set_keymap('i', 'lhs', 'mhs', {}) + + command('normal imhs') + eq({'rhs'}, curbufmeths.get_lines(0, -1, 0)) + + command('normal! ggVGd') + + command('normal ilhs') + eq({'rhs'}, curbufmeths.get_lines(0, -1, 0)) + end) + + it("can set noremap mappings that don't trigger other mappings", function() + meths.set_keymap('i', 'mhs', 'rhs', {}) + meths.set_keymap('i', 'lhs', 'mhs', {noremap = true}) + + command('normal imhs') + eq({'rhs'}, curbufmeths.get_lines(0, -1, 0)) + + command('normal! ggVGd') + + command('normal ilhs') -- shouldn't trigger mhs-to-rhs mapping + eq({'mhs'}, curbufmeths.get_lines(0, -1, 0)) + end) + + it("can set nowait mappings that fire without waiting", function() + meths.set_keymap('i', '123456', 'longer', {}) + meths.set_keymap('i', '123', 'shorter', {nowait = true}) + + -- feed keys one at a time; if all keys arrive atomically, the longer + -- mapping will trigger + local keys = 'i123456' + for c in string.gmatch(keys, '.') do + feed(c) + sleep(5) + end + eq({'shorter456'}, curbufmeths.get_lines(0, -1, 0)) + end) + + -- Perform exhaustive tests of basic functionality + local mapmodes = {'n', 'v', 'x', 's', 'o', '!', 'i', 'l', 'c', 't', ' ', ''} + for _, mapmode in ipairs(mapmodes) do + it('can set/unset normal mappings in mapmode '..mapmode, function() + meths.set_keymap(mapmode, 'lhs', 'rhs', {}) + eq(generate_mapargs(mapmode, 'lhs', 'rhs'), + get_mapargs(mapmode, 'lhs')) + + -- some mapmodes (like 'o') will prevent other mapmodes (like '!') from + -- taking effect, so unmap after each mapping + meths.del_keymap(mapmode, 'lhs') + eq({}, get_mapargs(mapmode, 'lhs')) + end) + end + + for _, mapmode in ipairs(mapmodes) do + it('can set/unset noremap mappings using mapmode '..mapmode, function() + meths.set_keymap(mapmode, 'lhs', 'rhs', {noremap = true}) + eq(generate_mapargs(mapmode, 'lhs', 'rhs', {noremap = true}), + get_mapargs(mapmode, 'lhs')) + + meths.del_keymap(mapmode, 'lhs') + eq({}, get_mapargs(mapmode, 'lhs')) + end) + end + + -- Test map-arguments, using optnames from above + -- remove some map arguments that are harder to test, or were already tested + optnames = {'nowait', 'silent', 'expr', 'noremap'} + for _, mapmode in ipairs(mapmodes) do + local printable_mode = normalize_mapmode(mapmode) + + -- Test with single mappings + for _, maparg in ipairs(optnames) do + it('can set/unset '..printable_mode..'-mappings with maparg: '..maparg, + function() + meths.set_keymap(mapmode, 'lhs', 'rhs', {[maparg] = true}) + eq(generate_mapargs(mapmode, 'lhs', 'rhs', {[maparg] = true}), + get_mapargs(mapmode, 'lhs')) + meths.del_keymap(mapmode, 'lhs') + eq({}, get_mapargs(mapmode, 'lhs')) + end) + it ('can set/unset '..printable_mode..'-mode mappings with maparg '.. + maparg..', whose value is false', function() + meths.set_keymap(mapmode, 'lhs', 'rhs', {[maparg] = false}) + eq(generate_mapargs(mapmode, 'lhs', 'rhs'), + get_mapargs(mapmode, 'lhs')) + meths.del_keymap(mapmode, 'lhs') + eq({}, get_mapargs(mapmode, 'lhs')) + end) + end + + -- Test with triplets of mappings, one of which is false + for i = 1, (#optnames - 2) do + local opt1, opt2, opt3 = optnames[i], optnames[i + 1], optnames[i + 2] + it('can set/unset '..printable_mode..'-mode mappings with mapargs '.. + opt1..', '..opt2..', '..opt3, function() + local opts = {[opt1] = true, [opt2] = false, [opt3] = true} + meths.set_keymap(mapmode, 'lhs', 'rhs', opts) + eq(generate_mapargs(mapmode, 'lhs', 'rhs', opts), + get_mapargs(mapmode, 'lhs')) + meths.del_keymap(mapmode, 'lhs') + eq({}, get_mapargs(mapmode, 'lhs')) + end) + end + end +end) + +describe('nvim_buf_[set/del]_keymap', function() + before_each(clear) + + -- nvim_set_keymap is implemented as a wrapped call to nvim_buf_set_keymap, + -- so its tests also effectively test nvim_buf_set_keymap + + -- here, we mainly test for buffer specificity and other special cases + + -- switch to the given buffer, abandoning any changes in the current buffer + local function switch_to_buf(bufnr) + command(bufnr..'buffer!') + end + + -- `set hidden`, then create two buffers and return their bufnr's + -- If start_from_first is truthy, the first buffer will be open when + -- the function returns; if falsy, the second buffer will be open. + local function make_two_buffers(start_from_first) + command('set hidden') + + local first_buf = meths.call_function('bufnr', {'%'}) + command('new') + local second_buf = meths.call_function('bufnr', {'%'}) + neq(second_buf, first_buf) -- sanity check + + if start_from_first then + switch_to_buf(first_buf) + end + + return first_buf, second_buf + end + + it('rejects negative bufnr values', function() + expect_err('Wrong type for argument 1, expecting Buffer', + bufmeths.set_keymap, -1, '', 'lhs', 'rhs', {}) + end) + + it('can set mappings active in the current buffer but not others', function() + local first, second = make_two_buffers(true) + + bufmeths.set_keymap(0, '', 'lhs', 'irhs<Esc>', {}) + command('normal lhs') + eq({'rhs'}, bufmeths.get_lines(0, 0, 1, 1)) + + -- mapping should have no effect in new buffer + switch_to_buf(second) + command('normal lhs') + eq({''}, bufmeths.get_lines(0, 0, 1, 1)) + + -- mapping should remain active in old buffer + switch_to_buf(first) + command('normal ^lhs') + eq({'rhsrhs'}, bufmeths.get_lines(0, 0, 1, 1)) + end) + + it('can set local mappings in buffer other than current', function() + local first = make_two_buffers(false) + bufmeths.set_keymap(first, '', 'lhs', 'irhs<Esc>', {}) + + -- shouldn't do anything + command('normal lhs') + eq({''}, bufmeths.get_lines(0, 0, 1, 1)) + + -- should take effect + switch_to_buf(first) + command('normal lhs') + eq({'rhs'}, bufmeths.get_lines(0, 0, 1, 1)) + end) + + it('can disable mappings made in another buffer, inside that buffer', function() + local first = make_two_buffers(false) + bufmeths.set_keymap(first, '', 'lhs', 'irhs<Esc>', {}) + bufmeths.del_keymap(first, '', 'lhs') + switch_to_buf(first) + + -- shouldn't do anything + command('normal lhs') + eq({''}, bufmeths.get_lines(0, 0, 1, 1)) + end) + + it("can't disable mappings given wrong buffer handle", function() + local first, second = make_two_buffers(false) + bufmeths.set_keymap(first, '', 'lhs', 'irhs<Esc>', {}) + expect_err('E31: No such mapping', + bufmeths.del_keymap, second, '', 'lhs') + + -- should still work + switch_to_buf(first) + command('normal lhs') + eq({'rhs'}, bufmeths.get_lines(0, 0, 1, 1)) + end) +end) |