neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

keymap_spec.lua (45908B)


      1 local t = require('test.testutil')
      2 local n = require('test.functional.testnvim')()
      3 
      4 local clear = n.clear
      5 local command = n.command
      6 local eq, neq = t.eq, t.neq
      7 local exec_lua = n.exec_lua
      8 local exec = n.exec
      9 local feed = n.feed
     10 local fn = n.fn
     11 local api = n.api
     12 local matches = t.matches
     13 local source = n.source
     14 local pcall_err = t.pcall_err
     15 
     16 local shallowcopy = t.shallowcopy
     17 local sleep = vim.uv.sleep
     18 
     19 local sid_api_client = -9
     20 local sid_lua = -8
     21 
     22 local mode_bits_map = {
     23  ['n'] = 0x01,
     24  ['x'] = 0x02,
     25  ['o'] = 0x04,
     26  ['c'] = 0x08,
     27  ['i'] = 0x10,
     28  ['l'] = 0x20,
     29  ['s'] = 0x40,
     30  ['t'] = 0x80,
     31  [' '] = 0x47,
     32  ['v'] = 0x42,
     33  ['!'] = 0x18,
     34 }
     35 
     36 describe('nvim_get_keymap', function()
     37  before_each(clear)
     38 
     39  -- Basic mapping and table to be used to describe results
     40  local foo_bar_string = 'nnoremap foo bar'
     41  local foo_bar_map_table = {
     42    lhs = 'foo',
     43    lhsraw = 'foo',
     44    script = 0,
     45    silent = 0,
     46    rhs = 'bar',
     47    expr = 0,
     48    replace_keycodes = 0,
     49    sid = 0,
     50    scriptversion = 1,
     51    buffer = 0,
     52    nowait = 0,
     53    mode = 'n',
     54    mode_bits = 0x01,
     55    abbr = 0,
     56    noremap = 1,
     57    lnum = 0,
     58  }
     59 
     60  it('returns empty list when no map', function()
     61    eq({}, api.nvim_get_keymap('n'))
     62  end)
     63 
     64  it('returns list of all applicable mappings', function()
     65    command(foo_bar_string)
     66    -- Only one mapping available
     67    -- Should be the same as the dictionary we supplied earlier
     68    -- and the dictionary you would get from maparg
     69    -- since this is a global map, and not script local
     70    eq({ foo_bar_map_table }, api.nvim_get_keymap('n'))
     71    eq({ fn.maparg('foo', 'n', false, true) }, api.nvim_get_keymap('n'))
     72 
     73    -- Add another mapping
     74    command('nnoremap foo_longer bar_longer')
     75    local foolong_bar_map_table = shallowcopy(foo_bar_map_table)
     76    foolong_bar_map_table['lhs'] = 'foo_longer'
     77    foolong_bar_map_table['lhsraw'] = 'foo_longer'
     78    foolong_bar_map_table['rhs'] = 'bar_longer'
     79 
     80    eq({ foolong_bar_map_table, foo_bar_map_table }, api.nvim_get_keymap('n'))
     81 
     82    -- Remove a mapping
     83    command('unmap foo_longer')
     84    eq({ foo_bar_map_table }, api.nvim_get_keymap('n'))
     85  end)
     86 
     87  it('works for other modes', function()
     88    -- Add two mappings, one in insert and one normal
     89    -- We'll only check the insert mode one
     90    command('nnoremap not_going to_check')
     91 
     92    command('inoremap foo bar')
     93    -- The table will be the same except for the mode
     94    local insert_table = shallowcopy(foo_bar_map_table)
     95    insert_table['mode'] = 'i'
     96    insert_table['mode_bits'] = 0x10
     97 
     98    eq({ insert_table }, api.nvim_get_keymap('i'))
     99  end)
    100 
    101  it('considers scope', function()
    102    -- change the map slightly
    103    command('nnoremap foo_longer bar_longer')
    104    local foolong_bar_map_table = shallowcopy(foo_bar_map_table)
    105    foolong_bar_map_table['lhs'] = 'foo_longer'
    106    foolong_bar_map_table['lhsraw'] = 'foo_longer'
    107    foolong_bar_map_table['rhs'] = 'bar_longer'
    108 
    109    local buffer_table = shallowcopy(foo_bar_map_table)
    110    buffer_table['buffer'] = 1
    111 
    112    command('nnoremap <buffer> foo bar')
    113 
    114    -- The buffer mapping should not show up
    115    eq({ foolong_bar_map_table }, api.nvim_get_keymap('n'))
    116    eq({ buffer_table }, api.nvim_buf_get_keymap(0, 'n'))
    117  end)
    118 
    119  it('considers scope for overlapping maps', function()
    120    command('nnoremap foo bar')
    121 
    122    local buffer_table = shallowcopy(foo_bar_map_table)
    123    buffer_table['buffer'] = 1
    124 
    125    command('nnoremap <buffer> foo bar')
    126 
    127    eq({ foo_bar_map_table }, api.nvim_get_keymap('n'))
    128    eq({ buffer_table }, api.nvim_buf_get_keymap(0, 'n'))
    129  end)
    130 
    131  it('can retrieve mapping for different buffers', function()
    132    local original_buffer = api.nvim_buf_get_number(0)
    133    -- Place something in each of the buffers to make sure they stick around
    134    -- and set hidden so we can leave them
    135    command('set hidden')
    136    command('new')
    137    command('normal! ihello 2')
    138    command('new')
    139    command('normal! ihello 3')
    140 
    141    local final_buffer = api.nvim_buf_get_number(0)
    142 
    143    command('nnoremap <buffer> foo bar')
    144    -- Final buffer will have buffer mappings
    145    local buffer_table = shallowcopy(foo_bar_map_table)
    146    buffer_table['buffer'] = final_buffer
    147    eq({ buffer_table }, api.nvim_buf_get_keymap(final_buffer, 'n'))
    148    eq({ buffer_table }, api.nvim_buf_get_keymap(0, 'n'))
    149 
    150    command('buffer ' .. original_buffer)
    151    eq(original_buffer, api.nvim_buf_get_number(0))
    152    -- Original buffer won't have any mappings
    153    eq({}, api.nvim_get_keymap('n'))
    154    eq({}, api.nvim_buf_get_keymap(0, 'n'))
    155    eq({ buffer_table }, api.nvim_buf_get_keymap(final_buffer, 'n'))
    156  end)
    157 
    158  -- Test toggle switches for basic options
    159  -- @param  option  The key represented in the `maparg()` result dict
    160  local function global_and_buffer_test(
    161    map,
    162    option,
    163    option_token,
    164    global_on_result,
    165    buffer_on_result,
    166    global_off_result,
    167    buffer_off_result,
    168    new_windows
    169  )
    170    local function make_new_windows(number_of_windows)
    171      if new_windows == nil then
    172        return nil
    173      end
    174 
    175      for _ = 1, number_of_windows do
    176        command('new')
    177      end
    178    end
    179 
    180    local mode = string.sub(map, 1, 1)
    181    -- Don't run this for the <buffer> mapping, since it doesn't make sense
    182    if option_token ~= '<buffer>' then
    183      it(
    184        string.format(
    185          'returns %d for the key "%s" when %s is used globally with %s (%s)',
    186          global_on_result,
    187          option,
    188          option_token,
    189          map,
    190          mode
    191        ),
    192        function()
    193          make_new_windows(new_windows)
    194          command(map .. ' ' .. option_token .. ' foo bar')
    195          local result = api.nvim_get_keymap(mode)[1][option]
    196          eq(global_on_result, result)
    197        end
    198      )
    199    end
    200 
    201    it(
    202      string.format(
    203        'returns %d for the key "%s" when %s is used for buffers with %s (%s)',
    204        buffer_on_result,
    205        option,
    206        option_token,
    207        map,
    208        mode
    209      ),
    210      function()
    211        make_new_windows(new_windows)
    212        command(map .. ' <buffer> ' .. option_token .. ' foo bar')
    213        local result = api.nvim_buf_get_keymap(0, mode)[1][option]
    214        eq(buffer_on_result, result)
    215      end
    216    )
    217 
    218    -- Don't run these for the <buffer> mapping, since it doesn't make sense
    219    if option_token ~= '<buffer>' then
    220      it(
    221        string.format(
    222          'returns %d for the key "%s" when %s is not used globally with %s (%s)',
    223          global_off_result,
    224          option,
    225          option_token,
    226          map,
    227          mode
    228        ),
    229        function()
    230          make_new_windows(new_windows)
    231          command(map .. ' baz bat')
    232          local result = api.nvim_get_keymap(mode)[1][option]
    233          eq(global_off_result, result)
    234        end
    235      )
    236 
    237      it(
    238        string.format(
    239          'returns %d for the key "%s" when %s is not used for buffers with %s (%s)',
    240          buffer_off_result,
    241          option,
    242          option_token,
    243          map,
    244          mode
    245        ),
    246        function()
    247          make_new_windows(new_windows)
    248          command(map .. ' <buffer> foo bar')
    249 
    250          local result = api.nvim_buf_get_keymap(0, mode)[1][option]
    251          eq(buffer_off_result, result)
    252        end
    253      )
    254    end
    255  end
    256 
    257  -- Standard modes and returns the same values in the dictionary as maparg()
    258  local mode_list = { 'nnoremap', 'nmap', 'imap', 'inoremap', 'cnoremap' }
    259  for mode in pairs(mode_list) do
    260    global_and_buffer_test(mode_list[mode], 'silent', '<silent>', 1, 1, 0, 0)
    261    global_and_buffer_test(mode_list[mode], 'nowait', '<nowait>', 1, 1, 0, 0)
    262    global_and_buffer_test(mode_list[mode], 'expr', '<expr>', 1, 1, 0, 0)
    263  end
    264 
    265  -- noremap will now be 2 if script was used, which is not the same as maparg()
    266  global_and_buffer_test('nmap', 'noremap', '<script>', 2, 2, 0, 0)
    267  global_and_buffer_test('nnoremap', 'noremap', '<script>', 2, 2, 1, 1)
    268 
    269  -- buffer will return the buffer ID, which is not the same as maparg()
    270  -- Three of these tests won't run
    271  global_and_buffer_test('nnoremap', 'buffer', '<buffer>', nil, 3, nil, nil, 2)
    272 
    273  it('returns script numbers for global maps', function()
    274    source([[
    275      function! s:maparg_test_function() abort
    276        return 'testing'
    277      endfunction
    278 
    279      nnoremap fizz :call <SID>maparg_test_function()<CR>
    280    ]])
    281    local sid_result = api.nvim_get_keymap('n')[1]['sid']
    282    eq(1, sid_result)
    283    eq('testing', api.nvim_call_function('<SNR>' .. sid_result .. '_maparg_test_function', {}))
    284  end)
    285 
    286  it('returns script numbers for buffer maps', function()
    287    source([[
    288      function! s:maparg_test_function() abort
    289        return 'testing'
    290      endfunction
    291 
    292      nnoremap <buffer> fizz :call <SID>maparg_test_function()<CR>
    293    ]])
    294    local sid_result = api.nvim_buf_get_keymap(0, 'n')[1]['sid']
    295    eq(1, sid_result)
    296    eq('testing', api.nvim_call_function('<SNR>' .. sid_result .. '_maparg_test_function', {}))
    297  end)
    298 
    299  it('works with <F12> and others', function()
    300    command('nnoremap <F12> :let g:maparg_test_var = 1<CR>')
    301    eq('<F12>', api.nvim_get_keymap('n')[1]['lhs'])
    302    eq(':let g:maparg_test_var = 1<CR>', api.nvim_get_keymap('n')[1]['rhs'])
    303  end)
    304 
    305  it('works correctly despite various &cpo settings', function()
    306    local cpo_table = {
    307      script = 0,
    308      silent = 0,
    309      expr = 0,
    310      replace_keycodes = 0,
    311      sid = 0,
    312      scriptversion = 1,
    313      buffer = 0,
    314      nowait = 0,
    315      abbr = 0,
    316      noremap = 1,
    317      lnum = 0,
    318    }
    319    local function cpomap(lhs, rhs, mode)
    320      local ret = shallowcopy(cpo_table)
    321      local lhsraw = api.nvim_eval(('"%s"'):format(lhs:gsub('\\', '\\\\'):gsub('<', '\\<*')))
    322      local lhsrawalt = api.nvim_eval(('"%s"'):format(lhs:gsub('\\', '\\\\'):gsub('<', '\\<')))
    323      ret.lhs = lhs
    324      ret.lhsraw = lhsraw
    325      ret.lhsrawalt = lhsrawalt ~= lhsraw and lhsrawalt or nil
    326      ret.rhs = rhs
    327      ret.mode = mode
    328      ret.mode_bits = mode_bits_map[mode]
    329      return ret
    330    end
    331 
    332    command('set cpo+=B')
    333    command('nnoremap \\<C-a><C-a><LT>C-a>\\  \\<C-b><C-b><LT>C-b>\\')
    334    command('nnoremap <special> \\<C-c><C-c><LT>C-c>\\  \\<C-d><C-d><LT>C-d>\\')
    335 
    336    command('set cpo+=B')
    337    command('xnoremap \\<C-a><C-a><LT>C-a>\\  \\<C-b><C-b><LT>C-b>\\')
    338    command('xnoremap <special> \\<C-c><C-c><LT>C-c>\\  \\<C-d><C-d><LT>C-d>\\')
    339 
    340    command('set cpo-=B')
    341    command('snoremap \\<C-a><C-a><LT>C-a>\\  \\<C-b><C-b><LT>C-b>\\')
    342    command('snoremap <special> \\<C-c><C-c><LT>C-c>\\  \\<C-d><C-d><LT>C-d>\\')
    343 
    344    command('set cpo-=B')
    345    command('onoremap \\<C-a><C-a><LT>C-a>\\  \\<C-b><C-b><LT>C-b>\\')
    346    command('onoremap <special> \\<C-c><C-c><LT>C-c>\\  \\<C-d><C-d><LT>C-d>\\')
    347 
    348    for _, cmd in ipairs({
    349      'set cpo-=B',
    350      'set cpo+=B',
    351    }) do
    352      command(cmd)
    353      eq({
    354        cpomap('\\<C-C><C-C><lt>C-c>\\', '\\<C-D><C-D><lt>C-d>\\', 'n'),
    355        cpomap('\\<C-A><C-A><lt>C-a>\\', '\\<C-B><C-B><lt>C-b>\\', 'n'),
    356      }, api.nvim_get_keymap('n'))
    357      eq({
    358        cpomap('\\<C-C><C-C><lt>C-c>\\', '\\<C-D><C-D><lt>C-d>\\', 'x'),
    359        cpomap('\\<C-A><C-A><lt>C-a>\\', '\\<C-B><C-B><lt>C-b>\\', 'x'),
    360      }, api.nvim_get_keymap('x'))
    361      eq({
    362        cpomap('<lt>C-c><C-C><lt>C-c> ', '<lt>C-d><C-D><lt>C-d>', 's'),
    363        cpomap('<lt>C-a><C-A><lt>C-a> ', '<lt>C-b><C-B><lt>C-b>', 's'),
    364      }, api.nvim_get_keymap('s'))
    365      eq({
    366        cpomap('<lt>C-c><C-C><lt>C-c> ', '<lt>C-d><C-D><lt>C-d>', 'o'),
    367        cpomap('<lt>C-a><C-A><lt>C-a> ', '<lt>C-b><C-B><lt>C-b>', 'o'),
    368      }, api.nvim_get_keymap('o'))
    369    end
    370  end)
    371 
    372  it('always uses space for space and bar for bar', function()
    373    local space_table = {
    374      lhs = '|   |',
    375      lhsraw = '|   |',
    376      rhs = '|    |',
    377      mode = 'n',
    378      mode_bits = 0x01,
    379      abbr = 0,
    380      script = 0,
    381      silent = 0,
    382      expr = 0,
    383      replace_keycodes = 0,
    384      sid = 0,
    385      scriptversion = 1,
    386      buffer = 0,
    387      nowait = 0,
    388      noremap = 1,
    389      lnum = 0,
    390    }
    391    command('nnoremap \\|<Char-0x20><Char-32><Space><Bar> \\|<Char-0x20><Char-32><Space> <Bar>')
    392    eq({ space_table }, api.nvim_get_keymap('n'))
    393  end)
    394 
    395  it('can handle lua mappings', function()
    396    eq(
    397      0,
    398      exec_lua([[
    399      GlobalCount = 0
    400      vim.api.nvim_set_keymap('n', 'asdf', '', {
    401        callback = function() GlobalCount = GlobalCount + 1 end,
    402      })
    403      return GlobalCount
    404    ]])
    405    )
    406 
    407    feed('asdf\n')
    408    eq(1, exec_lua([[return GlobalCount]]))
    409 
    410    eq(
    411      2,
    412      exec_lua([[
    413      vim.api.nvim_get_keymap('n')[1].callback()
    414      return GlobalCount
    415    ]])
    416    )
    417 
    418    exec([[
    419      call nvim_get_keymap('n')[0].callback()
    420    ]])
    421    eq(3, exec_lua([[return GlobalCount]]))
    422 
    423    local mapargs = api.nvim_get_keymap('n')
    424    mapargs[1].callback = nil
    425    eq({
    426      lhs = 'asdf',
    427      lhsraw = 'asdf',
    428      script = 0,
    429      silent = 0,
    430      expr = 0,
    431      replace_keycodes = 0,
    432      sid = sid_lua,
    433      scriptversion = 1,
    434      buffer = 0,
    435      nowait = 0,
    436      mode = 'n',
    437      mode_bits = 0x01,
    438      abbr = 0,
    439      noremap = 0,
    440      lnum = 0,
    441    }, mapargs[1])
    442  end)
    443 
    444  it('can handle map descriptions', function()
    445    api.nvim_set_keymap('n', 'lhs', 'rhs', { desc = 'map description' })
    446    eq({
    447      lhs = 'lhs',
    448      lhsraw = 'lhs',
    449      rhs = 'rhs',
    450      script = 0,
    451      silent = 0,
    452      expr = 0,
    453      replace_keycodes = 0,
    454      sid = sid_api_client,
    455      scriptversion = 1,
    456      buffer = 0,
    457      nowait = 0,
    458      mode = 'n',
    459      mode_bits = 0x01,
    460      abbr = 0,
    461      noremap = 0,
    462      lnum = 0,
    463      desc = 'map description',
    464    }, api.nvim_get_keymap('n')[1])
    465  end)
    466 
    467  it('can get abbreviations', function()
    468    command('inoreabbr foo bar')
    469    command('cnoreabbr <buffer> foo baz')
    470 
    471    local mapargs_i = {
    472      abbr = 1,
    473      buffer = 0,
    474      expr = 0,
    475      replace_keycodes = 0,
    476      lhs = 'foo',
    477      lhsraw = 'foo',
    478      lnum = 0,
    479      mode = 'i',
    480      mode_bits = 0x10,
    481      noremap = 1,
    482      nowait = 0,
    483      rhs = 'bar',
    484      script = 0,
    485      scriptversion = 1,
    486      sid = 0,
    487      silent = 0,
    488    }
    489    local mapargs_c = {
    490      abbr = 1,
    491      buffer = 1,
    492      expr = 0,
    493      replace_keycodes = 0,
    494      lhs = 'foo',
    495      lhsraw = 'foo',
    496      lnum = 0,
    497      mode = 'c',
    498      mode_bits = 0x08,
    499      noremap = 1,
    500      nowait = 0,
    501      rhs = 'baz',
    502      script = 0,
    503      scriptversion = 1,
    504      sid = 0,
    505      silent = 0,
    506    }
    507 
    508    local curbuf = api.nvim_get_current_buf()
    509 
    510    eq({ mapargs_i }, api.nvim_get_keymap('ia'))
    511    eq({}, api.nvim_buf_get_keymap(curbuf, 'ia'))
    512 
    513    eq({}, api.nvim_get_keymap('ca'))
    514    eq({ mapargs_c }, api.nvim_buf_get_keymap(curbuf, 'ca'))
    515 
    516    eq({ mapargs_i }, api.nvim_get_keymap('!a'))
    517    eq({ mapargs_c }, api.nvim_buf_get_keymap(curbuf, '!a'))
    518  end)
    519 end)
    520 
    521 describe('nvim_set_keymap, nvim_del_keymap', function()
    522  before_each(clear)
    523 
    524  -- `generate_expected` is truthy: for generating an expected output for
    525  -- maparg(), which does not accept "!" (though it returns "!" in its output
    526  -- if getting a mapping set with |:map!|).
    527  local function normalize_mapmode(mode, generate_expected)
    528    if mode:sub(-1) == 'a' then
    529      mode = mode:sub(1, -2)
    530    end
    531    if not generate_expected and mode == '!' then
    532      -- Cannot retrieve mapmode-ic mappings with "!", but can with "i" or "c".
    533      mode = 'i'
    534    elseif mode == '' then
    535      mode = generate_expected and ' ' or mode
    536    end
    537    return mode
    538  end
    539 
    540  -- Generate a mapargs dict, for comparison against the mapping that was
    541  -- actually set
    542  local function generate_mapargs(mode, lhs, rhs, opts)
    543    if not opts then
    544      opts = {}
    545    end
    546 
    547    local to_return = {}
    548    local expected_mode = normalize_mapmode(mode, true)
    549    to_return.mode = expected_mode
    550    to_return.mode_bits = mode_bits_map[expected_mode]
    551    to_return.abbr = mode:sub(-1) == 'a' and 1 or 0
    552    to_return.noremap = not opts.noremap and 0 or 1
    553    to_return.lhs = lhs
    554    to_return.rhs = rhs
    555    to_return.script = 0
    556    to_return.silent = not opts.silent and 0 or 1
    557    to_return.nowait = not opts.nowait and 0 or 1
    558    to_return.expr = not opts.expr and 0 or 1
    559    to_return.replace_keycodes = not opts.replace_keycodes and 0 or 1
    560    to_return.sid = not opts.sid and sid_api_client or opts.sid
    561    to_return.scriptversion = 1
    562    to_return.buffer = not opts.buffer and 0 or opts.buffer
    563    to_return.lnum = not opts.lnum and 0 or opts.lnum
    564    to_return.desc = opts.desc
    565 
    566    return to_return
    567  end
    568 
    569  -- Gets a maparg() dict from Nvim, if one exists.
    570  local function get_mapargs(mode, lhs)
    571    local mapargs = fn.maparg(lhs, normalize_mapmode(mode), mode:sub(-1) == 'a', true)
    572    -- drop "lhsraw" and "lhsrawalt" which are hard to check
    573    mapargs.lhsraw = nil
    574    mapargs.lhsrawalt = nil
    575    return mapargs
    576  end
    577 
    578  it('error on empty LHS', function()
    579    -- escape parentheses in lua string, else comparison fails erroneously
    580    eq('Invalid (empty) LHS', pcall_err(api.nvim_set_keymap, '', '', 'rhs', {}))
    581    eq('Invalid (empty) LHS', pcall_err(api.nvim_set_keymap, '', '', '', {}))
    582    eq('Invalid (empty) LHS', pcall_err(api.nvim_del_keymap, '', ''))
    583  end)
    584 
    585  it('error if LHS longer than MAXMAPLEN', function()
    586    -- assume MAXMAPLEN of 50 chars, as declared in mapping_defs.h
    587    local MAXMAPLEN = 50
    588    local lhs = ''
    589    for i = 1, MAXMAPLEN do
    590      lhs = lhs .. (i % 10)
    591    end
    592 
    593    -- exactly 50 chars should be fine
    594    api.nvim_set_keymap('', lhs, 'rhs', {})
    595 
    596    -- del_keymap should unmap successfully
    597    api.nvim_del_keymap('', lhs)
    598    eq({}, get_mapargs('', lhs))
    599 
    600    -- 51 chars should produce an error
    601    lhs = lhs .. '1'
    602    eq(
    603      'LHS exceeds maximum map length: ' .. lhs,
    604      pcall_err(api.nvim_set_keymap, '', lhs, 'rhs', {})
    605    )
    606    eq('LHS exceeds maximum map length: ' .. lhs, pcall_err(api.nvim_del_keymap, '', lhs))
    607  end)
    608 
    609  it('does not throw errors when rhs is longer than MAXMAPLEN', function()
    610    local MAXMAPLEN = 50
    611    local rhs = ''
    612    for i = 1, MAXMAPLEN do
    613      rhs = rhs .. (i % 10)
    614    end
    615    rhs = rhs .. '1'
    616    api.nvim_set_keymap('', 'lhs', rhs, {})
    617    eq(generate_mapargs('', 'lhs', rhs), get_mapargs('', 'lhs'))
    618  end)
    619 
    620  it('error on invalid mode shortname', function()
    621    eq('Invalid mode shortname: " "', pcall_err(api.nvim_set_keymap, ' ', 'lhs', 'rhs', {}))
    622    eq('Invalid mode shortname: "m"', pcall_err(api.nvim_set_keymap, 'm', 'lhs', 'rhs', {}))
    623    eq('Invalid mode shortname: "?"', pcall_err(api.nvim_set_keymap, '?', 'lhs', 'rhs', {}))
    624    eq('Invalid mode shortname: "y"', pcall_err(api.nvim_set_keymap, 'y', 'lhs', 'rhs', {}))
    625    eq('Invalid mode shortname: "p"', pcall_err(api.nvim_set_keymap, 'p', 'lhs', 'rhs', {}))
    626    eq('Invalid mode shortname: "a"', pcall_err(api.nvim_set_keymap, 'a', 'lhs', 'rhs', {}))
    627    eq('Invalid mode shortname: "oa"', pcall_err(api.nvim_set_keymap, 'oa', 'lhs', 'rhs', {}))
    628    eq('Invalid mode shortname: "!o"', pcall_err(api.nvim_set_keymap, '!o', 'lhs', 'rhs', {}))
    629    eq('Invalid mode shortname: "!i"', pcall_err(api.nvim_set_keymap, '!i', 'lhs', 'rhs', {}))
    630    eq('Invalid mode shortname: "!!"', pcall_err(api.nvim_set_keymap, '!!', 'lhs', 'rhs', {}))
    631    eq('Invalid mode shortname: "map"', pcall_err(api.nvim_set_keymap, 'map', 'lhs', 'rhs', {}))
    632    eq('Invalid mode shortname: "vmap"', pcall_err(api.nvim_set_keymap, 'vmap', 'lhs', 'rhs', {}))
    633    eq(
    634      'Invalid mode shortname: "xnoremap"',
    635      pcall_err(api.nvim_set_keymap, 'xnoremap', 'lhs', 'rhs', {})
    636    )
    637    eq('Invalid mode shortname: " "', pcall_err(api.nvim_del_keymap, ' ', 'lhs'))
    638    eq('Invalid mode shortname: "m"', pcall_err(api.nvim_del_keymap, 'm', 'lhs'))
    639    eq('Invalid mode shortname: "?"', pcall_err(api.nvim_del_keymap, '?', 'lhs'))
    640    eq('Invalid mode shortname: "y"', pcall_err(api.nvim_del_keymap, 'y', 'lhs'))
    641    eq('Invalid mode shortname: "p"', pcall_err(api.nvim_del_keymap, 'p', 'lhs'))
    642    eq('Invalid mode shortname: "a"', pcall_err(api.nvim_del_keymap, 'a', 'lhs'))
    643    eq('Invalid mode shortname: "oa"', pcall_err(api.nvim_del_keymap, 'oa', 'lhs'))
    644    eq('Invalid mode shortname: "!o"', pcall_err(api.nvim_del_keymap, '!o', 'lhs'))
    645    eq('Invalid mode shortname: "!i"', pcall_err(api.nvim_del_keymap, '!i', 'lhs'))
    646    eq('Invalid mode shortname: "!!"', pcall_err(api.nvim_del_keymap, '!!', 'lhs'))
    647    eq('Invalid mode shortname: "map"', pcall_err(api.nvim_del_keymap, 'map', 'lhs'))
    648    eq('Invalid mode shortname: "vmap"', pcall_err(api.nvim_del_keymap, 'vmap', 'lhs'))
    649    eq('Invalid mode shortname: "xnoremap"', pcall_err(api.nvim_del_keymap, 'xnoremap', 'lhs'))
    650  end)
    651 
    652  it('error on invalid optnames', function()
    653    eq(
    654      "Invalid key: 'silentt'",
    655      pcall_err(api.nvim_set_keymap, 'n', 'lhs', 'rhs', { silentt = true })
    656    )
    657    eq("Invalid key: 'sidd'", pcall_err(api.nvim_set_keymap, 'n', 'lhs', 'rhs', { sidd = false }))
    658    eq(
    659      "Invalid key: 'nowaiT'",
    660      pcall_err(api.nvim_set_keymap, 'n', 'lhs', 'rhs', { nowaiT = false })
    661    )
    662  end)
    663 
    664  it('error on <buffer> option key', function()
    665    eq(
    666      "Invalid key: 'buffer'",
    667      pcall_err(api.nvim_set_keymap, 'n', 'lhs', 'rhs', { buffer = true })
    668    )
    669  end)
    670 
    671  it('error when "replace_keycodes" is used without "expr"', function()
    672    eq(
    673      '"replace_keycodes" requires "expr"',
    674      pcall_err(api.nvim_set_keymap, 'n', 'lhs', 'rhs', { replace_keycodes = true })
    675    )
    676  end)
    677 
    678  local optnames = { 'nowait', 'silent', 'script', 'expr', 'unique' }
    679  for _, opt in ipairs(optnames) do
    680    -- note: need '%' to escape hyphens, which have special meaning in lua
    681    it('throws an error when given non-boolean value for ' .. opt, function()
    682      local opts = {}
    683      opts[opt] = 'fooo'
    684      eq(opt .. ' is not a boolean', pcall_err(api.nvim_set_keymap, 'n', 'lhs', 'rhs', opts))
    685    end)
    686  end
    687 
    688  -- Perform tests of basic functionality
    689  it('sets ordinary mappings', function()
    690    api.nvim_set_keymap('n', 'lhs', 'rhs', {})
    691    eq(generate_mapargs('n', 'lhs', 'rhs'), get_mapargs('n', 'lhs'))
    692 
    693    api.nvim_set_keymap('v', 'lhs', 'rhs', {})
    694    eq(generate_mapargs('v', 'lhs', 'rhs'), get_mapargs('v', 'lhs'))
    695  end)
    696 
    697  it('does not throw when LHS or RHS have leading/trailing whitespace', function()
    698    api.nvim_set_keymap('n', '   lhs', 'rhs', {})
    699    eq(generate_mapargs('n', '<Space><Space><Space>lhs', 'rhs'), get_mapargs('n', '   lhs'))
    700 
    701    api.nvim_set_keymap('n', 'lhs    ', 'rhs', {})
    702    eq(generate_mapargs('n', 'lhs<Space><Space><Space><Space>', 'rhs'), get_mapargs('n', 'lhs    '))
    703 
    704    api.nvim_set_keymap('v', ' lhs  ', '\trhs\t\f', {})
    705    eq(generate_mapargs('v', '<Space>lhs<Space><Space>', '\trhs\t\f'), get_mapargs('v', ' lhs  '))
    706  end)
    707 
    708  it('can set noremap mappings', function()
    709    api.nvim_set_keymap('x', 'lhs', 'rhs', { noremap = true })
    710    eq(generate_mapargs('x', 'lhs', 'rhs', { noremap = true }), get_mapargs('x', 'lhs'))
    711 
    712    api.nvim_set_keymap('t', 'lhs', 'rhs', { noremap = true })
    713    eq(generate_mapargs('t', 'lhs', 'rhs', { noremap = true }), get_mapargs('t', 'lhs'))
    714  end)
    715 
    716  it('can unmap mappings', function()
    717    api.nvim_set_keymap('v', 'lhs', 'rhs', {})
    718    api.nvim_del_keymap('v', 'lhs')
    719    eq({}, get_mapargs('v', 'lhs'))
    720 
    721    api.nvim_set_keymap('t', 'lhs', 'rhs', { noremap = true })
    722    api.nvim_del_keymap('t', 'lhs')
    723    eq({}, get_mapargs('t', 'lhs'))
    724  end)
    725 
    726  -- Test some edge cases
    727  it('"!" and empty string are synonyms for mapmode-nvo', function()
    728    local nvo_shortnames = { '', '!' }
    729    for _, name in ipairs(nvo_shortnames) do
    730      api.nvim_set_keymap(name, 'lhs', 'rhs', {})
    731      api.nvim_del_keymap(name, 'lhs')
    732      eq({}, get_mapargs(name, 'lhs'))
    733    end
    734  end)
    735 
    736  local special_chars = { '<C-U>', '<S-Left>', '<F12><F2><Tab>', '<Space><Tab>' }
    737  for _, lhs in ipairs(special_chars) do
    738    for _, rhs in ipairs(special_chars) do
    739      local mapmode = '!'
    740      it('can set mappings with special characters, lhs: ' .. lhs .. ', rhs: ' .. rhs, function()
    741        api.nvim_set_keymap(mapmode, lhs, rhs, {})
    742        eq(generate_mapargs(mapmode, lhs, rhs), get_mapargs(mapmode, lhs))
    743      end)
    744    end
    745  end
    746 
    747  it('can set mappings containing C0 control codes', function()
    748    api.nvim_set_keymap('n', '\n\r\n', 'rhs', {})
    749    local expected = generate_mapargs('n', '<NL><CR><NL>', 'rhs')
    750    eq(expected, get_mapargs('n', '<NL><CR><NL>'))
    751  end)
    752 
    753  it('can set mappings whose RHS is a <Nop>', function()
    754    api.nvim_set_keymap('i', 'lhs', '<Nop>', {})
    755    command('normal ilhs')
    756    eq({ '' }, api.nvim_buf_get_lines(0, 0, -1, 0)) -- imap to <Nop> does nothing
    757    eq(generate_mapargs('i', 'lhs', '<Nop>', {}), get_mapargs('i', 'lhs'))
    758 
    759    -- also test for case insensitivity
    760    api.nvim_set_keymap('i', 'lhs', '<nOp>', {})
    761    command('normal ilhs')
    762    eq({ '' }, api.nvim_buf_get_lines(0, 0, -1, 0))
    763    -- note: RHS in returned mapargs() dict reflects the original RHS
    764    -- provided by the user
    765    eq(generate_mapargs('i', 'lhs', '<nOp>', {}), get_mapargs('i', 'lhs'))
    766 
    767    api.nvim_set_keymap('i', 'lhs', '<NOP>', {})
    768    command('normal ilhs')
    769    eq({ '' }, api.nvim_buf_get_lines(0, 0, -1, 0))
    770    eq(generate_mapargs('i', 'lhs', '<NOP>', {}), get_mapargs('i', 'lhs'))
    771 
    772    -- a single ^V in RHS is also <Nop> (see :h map-empty-rhs)
    773    api.nvim_set_keymap('i', 'lhs', '\022', {})
    774    command('normal ilhs')
    775    eq({ '' }, api.nvim_buf_get_lines(0, 0, -1, 0))
    776    eq(generate_mapargs('i', 'lhs', '\022', {}), get_mapargs('i', 'lhs'))
    777  end)
    778 
    779  it('treats an empty RHS in a mapping like a <Nop>', function()
    780    api.nvim_set_keymap('i', 'lhs', '', {})
    781    command('normal ilhs')
    782    eq({ '' }, api.nvim_buf_get_lines(0, 0, -1, 0))
    783    eq(generate_mapargs('i', 'lhs', '', {}), get_mapargs('i', 'lhs'))
    784  end)
    785 
    786  it('can set and unset <M-">', function()
    787    -- Taken from the legacy test: test_mapping.vim. Exposes a bug in which
    788    -- replace_termcodes changes the length of the mapping's LHS, but
    789    -- do_map continues to use the *old* length of LHS.
    790    api.nvim_set_keymap('i', '<M-">', 'foo', {})
    791    api.nvim_del_keymap('i', '<M-">')
    792    eq({}, get_mapargs('i', '<M-">'))
    793  end)
    794 
    795  it(
    796    'interprets control sequences in expr-quotes correctly when called ' .. 'inside vim',
    797    function()
    798      command([[call nvim_set_keymap('i', "\<space>", "\<tab>", {})]])
    799      eq(generate_mapargs('i', '<Space>', '\t', { sid = 0 }), get_mapargs('i', '<Space>'))
    800      feed('i ')
    801      eq({ '\t' }, api.nvim_buf_get_lines(0, 0, -1, 0))
    802    end
    803  )
    804 
    805  it('throws appropriate error messages when setting <unique> maps', function()
    806    api.nvim_set_keymap('l', 'lhs', 'rhs', {})
    807    eq(
    808      'E227: Mapping already exists for lhs',
    809      pcall_err(api.nvim_set_keymap, 'l', 'lhs', 'rhs', { unique = true })
    810    )
    811    -- different mapmode, no error should be thrown
    812    api.nvim_set_keymap('t', 'lhs', 'rhs', { unique = true })
    813 
    814    api.nvim_set_keymap('n', '<tab>', 'rhs', {})
    815    eq(
    816      'E227: Mapping already exists for <tab>',
    817      pcall_err(api.nvim_set_keymap, 'n', '<tab>', 'rhs', { unique = true })
    818    )
    819 
    820    api.nvim_set_keymap('ia', 'lhs', 'rhs', {})
    821    eq(
    822      'E226: Abbreviation already exists for lhs',
    823      pcall_err(api.nvim_set_keymap, 'ia', 'lhs', 'rhs', { unique = true })
    824    )
    825  end)
    826 
    827  it('can set <expr> mappings whose RHS change dynamically', function()
    828    exec([[
    829        function! FlipFlop() abort
    830          if !exists('g:flip') | let g:flip = 0 | endif
    831          let g:flip = !g:flip
    832          return g:flip
    833        endfunction
    834        ]])
    835    eq(1, api.nvim_call_function('FlipFlop', {}))
    836    eq(0, api.nvim_call_function('FlipFlop', {}))
    837    eq(1, api.nvim_call_function('FlipFlop', {}))
    838    eq(0, api.nvim_call_function('FlipFlop', {}))
    839 
    840    api.nvim_set_keymap('i', 'lhs', 'FlipFlop()', { expr = true })
    841    command('normal ilhs')
    842    eq({ '1' }, api.nvim_buf_get_lines(0, 0, -1, 0))
    843 
    844    command('normal! ggVGd')
    845 
    846    command('normal ilhs')
    847    eq({ '0' }, api.nvim_buf_get_lines(0, 0, -1, 0))
    848  end)
    849 
    850  it('can set mappings that do trigger other mappings', function()
    851    api.nvim_set_keymap('i', 'mhs', 'rhs', {})
    852    api.nvim_set_keymap('i', 'lhs', 'mhs', {})
    853 
    854    command('normal imhs')
    855    eq({ 'rhs' }, api.nvim_buf_get_lines(0, 0, -1, 0))
    856 
    857    command('normal! ggVGd')
    858 
    859    command('normal ilhs')
    860    eq({ 'rhs' }, api.nvim_buf_get_lines(0, 0, -1, 0))
    861  end)
    862 
    863  it("can set noremap mappings that don't trigger other mappings", function()
    864    api.nvim_set_keymap('i', 'mhs', 'rhs', {})
    865    api.nvim_set_keymap('i', 'lhs', 'mhs', { noremap = true })
    866 
    867    command('normal imhs')
    868    eq({ 'rhs' }, api.nvim_buf_get_lines(0, 0, -1, 0))
    869 
    870    command('normal! ggVGd')
    871 
    872    command('normal ilhs') -- shouldn't trigger mhs-to-rhs mapping
    873    eq({ 'mhs' }, api.nvim_buf_get_lines(0, 0, -1, 0))
    874  end)
    875 
    876  it('can set nowait mappings that fire without waiting', function()
    877    api.nvim_set_keymap('i', '123456', 'longer', {})
    878    api.nvim_set_keymap('i', '123', 'shorter', { nowait = true })
    879 
    880    -- feed keys one at a time; if all keys arrive atomically, the longer
    881    -- mapping will trigger
    882    local keys = 'i123456'
    883    for c in string.gmatch(keys, '.') do
    884      feed(c)
    885      sleep(5)
    886    end
    887    eq({ 'shorter456' }, api.nvim_buf_get_lines(0, 0, -1, 0))
    888  end)
    889 
    890  -- Perform exhaustive tests of basic functionality
    891  local mapmodes = { 'n', 'v', 'x', 's', 'o', '!', 'i', 'l', 'c', 't', '', 'ia', 'ca', '!a' }
    892  for _, mapmode in ipairs(mapmodes) do
    893    it('can set/unset normal mappings in mapmode ' .. mapmode, function()
    894      api.nvim_set_keymap(mapmode, 'lhs', 'rhs', {})
    895      eq(generate_mapargs(mapmode, 'lhs', 'rhs'), get_mapargs(mapmode, 'lhs'))
    896 
    897      -- some mapmodes (like 'o') will prevent other mapmodes (like '!') from
    898      -- taking effect, so unmap after each mapping
    899      api.nvim_del_keymap(mapmode, 'lhs')
    900      eq({}, get_mapargs(mapmode, 'lhs'))
    901    end)
    902  end
    903 
    904  for _, mapmode in ipairs(mapmodes) do
    905    it('can set/unset noremap mappings using mapmode ' .. mapmode, function()
    906      api.nvim_set_keymap(mapmode, 'lhs', 'rhs', { noremap = true })
    907      eq(generate_mapargs(mapmode, 'lhs', 'rhs', { noremap = true }), get_mapargs(mapmode, 'lhs'))
    908 
    909      api.nvim_del_keymap(mapmode, 'lhs')
    910      eq({}, get_mapargs(mapmode, 'lhs'))
    911    end)
    912  end
    913 
    914  -- Test map-arguments, using optnames from above
    915  -- remove some map arguments that are harder to test, or were already tested
    916  optnames = { 'nowait', 'silent', 'expr', 'noremap' }
    917  for _, mapmode in ipairs(mapmodes) do
    918    -- Test with single mappings
    919    for _, maparg in ipairs(optnames) do
    920      it('can set/unset ' .. mapmode .. '-mappings with maparg: ' .. maparg, function()
    921        api.nvim_set_keymap(mapmode, 'lhs', 'rhs', { [maparg] = true })
    922        eq(
    923          generate_mapargs(mapmode, 'lhs', 'rhs', { [maparg] = true }),
    924          get_mapargs(mapmode, 'lhs')
    925        )
    926        api.nvim_del_keymap(mapmode, 'lhs')
    927        eq({}, get_mapargs(mapmode, 'lhs'))
    928      end)
    929      it(
    930        'can set/unset '
    931          .. mapmode
    932          .. '-mode mappings with maparg '
    933          .. maparg
    934          .. ', whose value is false',
    935        function()
    936          api.nvim_set_keymap(mapmode, 'lhs', 'rhs', { [maparg] = false })
    937          eq(generate_mapargs(mapmode, 'lhs', 'rhs'), get_mapargs(mapmode, 'lhs'))
    938          api.nvim_del_keymap(mapmode, 'lhs')
    939          eq({}, get_mapargs(mapmode, 'lhs'))
    940        end
    941      )
    942    end
    943 
    944    -- Test with triplets of mappings, one of which is false
    945    for i = 1, (#optnames - 2) do
    946      local opt1, opt2, opt3 = optnames[i], optnames[i + 1], optnames[i + 2]
    947      it(
    948        'can set/unset '
    949          .. mapmode
    950          .. '-mode mappings with mapargs '
    951          .. opt1
    952          .. ', '
    953          .. opt2
    954          .. ', '
    955          .. opt3,
    956        function()
    957          local opts = { [opt1] = true, [opt2] = false, [opt3] = true }
    958          api.nvim_set_keymap(mapmode, 'lhs', 'rhs', opts)
    959          eq(generate_mapargs(mapmode, 'lhs', 'rhs', opts), get_mapargs(mapmode, 'lhs'))
    960          api.nvim_del_keymap(mapmode, 'lhs')
    961          eq({}, get_mapargs(mapmode, 'lhs'))
    962        end
    963      )
    964    end
    965  end
    966 
    967  it('can make lua mappings', function()
    968    eq(
    969      0,
    970      exec_lua [[
    971      GlobalCount = 0
    972      vim.api.nvim_set_keymap('n', 'asdf', '', {
    973        callback = function() GlobalCount = GlobalCount + 1 end,
    974      })
    975      return GlobalCount
    976    ]]
    977    )
    978 
    979    feed('asdf\n')
    980 
    981    eq(1, exec_lua [[return GlobalCount]])
    982  end)
    983 
    984  it(':map command shows lua mapping correctly', function()
    985    exec_lua [[
    986      vim.api.nvim_set_keymap('n', 'asdf', '', {
    987        callback = function() print('jkl;') end,
    988      })
    989    ]]
    990    matches(
    991      '^\nn  asdf          <Lua %d+>',
    992      exec_lua [[return vim.api.nvim_exec2(':nmap asdf', { output = true }).output]]
    993    )
    994  end)
    995 
    996  it('mapcheck() returns lua mapping correctly', function()
    997    exec_lua [[
    998      vim.api.nvim_set_keymap('n', 'asdf', '', {
    999        callback = function() print('jkl;') end,
   1000      })
   1001    ]]
   1002    matches('^<Lua %d+>', fn.mapcheck('asdf', 'n'))
   1003  end)
   1004 
   1005  it('maparg() and maplist() return lua mapping correctly', function()
   1006    eq(
   1007      0,
   1008      exec_lua([[
   1009      GlobalCount = 0
   1010      vim.api.nvim_set_keymap('n', 'asdf', '', {
   1011        callback = function() GlobalCount = GlobalCount + 1 end,
   1012      })
   1013      return GlobalCount
   1014    ]])
   1015    )
   1016 
   1017    matches('^<Lua %d+>', fn.maparg('asdf', 'n'))
   1018 
   1019    local mapargs = fn.maparg('asdf', 'n', false, true)
   1020    mapargs.callback = nil
   1021    mapargs.lhsraw = nil
   1022    mapargs.lhsrawalt = nil
   1023    eq(generate_mapargs('n', 'asdf', nil, { sid = sid_lua }), mapargs)
   1024 
   1025    eq(
   1026      1,
   1027      exec_lua([[
   1028      vim.fn.maparg('asdf', 'n', false, true).callback()
   1029      return GlobalCount
   1030    ]])
   1031    )
   1032 
   1033    exec([[
   1034      call maparg('asdf', 'n', v:false, v:true).callback()
   1035    ]])
   1036    eq(2, exec_lua([[return GlobalCount]]))
   1037 
   1038    api.nvim_eval([[
   1039      maplist()->filter({_, m -> m.lhs == 'asdf'})->foreach({_, m -> m.callback()})
   1040    ]])
   1041    eq(3, exec_lua([[return GlobalCount]]))
   1042  end)
   1043 
   1044  it('can make lua expr mappings replacing keycodes', function()
   1045    exec_lua [[
   1046      vim.api.nvim_set_keymap('n', 'aa', '', {
   1047        callback = function() return '<Insert>π<C-V><M-π>foo<lt><Esc>' end,
   1048        expr = true,
   1049        replace_keycodes = true,
   1050      })
   1051    ]]
   1052 
   1053    feed('aa')
   1054 
   1055    eq({ 'π<M-π>foo<' }, api.nvim_buf_get_lines(0, 0, -1, false))
   1056  end)
   1057 
   1058  it('can make lua expr mappings without replacing keycodes', function()
   1059    exec_lua [[
   1060      vim.api.nvim_set_keymap('i', 'aa', '', {
   1061        callback = function() return '<space>' end,
   1062        expr = true,
   1063      })
   1064    ]]
   1065 
   1066    feed('iaa<esc>')
   1067 
   1068    eq({ '<space>' }, api.nvim_buf_get_lines(0, 0, -1, false))
   1069  end)
   1070 
   1071  it('lua expr mapping returning nil is equivalent to returning an empty string', function()
   1072    exec_lua [[
   1073      vim.api.nvim_set_keymap('i', 'aa', '', {
   1074        callback = function() return nil end,
   1075        expr = true,
   1076      })
   1077    ]]
   1078 
   1079    feed('iaa<esc>')
   1080 
   1081    eq({ '' }, api.nvim_buf_get_lines(0, 0, -1, false))
   1082  end)
   1083 
   1084  it('does not reset pum in lua mapping', function()
   1085    eq(
   1086      0,
   1087      exec_lua [[
   1088      VisibleCount = 0
   1089      vim.api.nvim_set_keymap('i', '<F2>', '', {
   1090        callback = function() VisibleCount = VisibleCount + vim.fn.pumvisible() end,
   1091      })
   1092      return VisibleCount
   1093    ]]
   1094    )
   1095    feed('i<C-X><C-V><F2><F2><esc>')
   1096    eq(2, exec_lua [[return VisibleCount]])
   1097  end)
   1098 
   1099  it('redo of lua mappings in op-pending mode work', function()
   1100    eq(
   1101      0,
   1102      exec_lua [[
   1103      OpCount = 0
   1104      vim.api.nvim_set_keymap('o', '<F2>', '', {
   1105        callback = function() OpCount = OpCount + 1 end,
   1106      })
   1107      return OpCount
   1108    ]]
   1109    )
   1110    feed('d<F2>')
   1111    eq(1, exec_lua [[return OpCount]])
   1112    feed('.')
   1113    eq(2, exec_lua [[return OpCount]])
   1114  end)
   1115 
   1116  it('can overwrite lua mappings', function()
   1117    eq(
   1118      0,
   1119      exec_lua [[
   1120      GlobalCount = 0
   1121      vim.api.nvim_set_keymap('n', 'asdf', '', {
   1122        callback = function() GlobalCount = GlobalCount + 1 end,
   1123      })
   1124      return GlobalCount
   1125    ]]
   1126    )
   1127 
   1128    feed('asdf\n')
   1129 
   1130    eq(1, exec_lua [[return GlobalCount]])
   1131 
   1132    exec_lua [[
   1133      vim.api.nvim_set_keymap('n', 'asdf', '', {
   1134        callback = function() GlobalCount = GlobalCount - 1 end,
   1135      })
   1136    ]]
   1137 
   1138    feed('asdf\n')
   1139 
   1140    eq(0, exec_lua [[return GlobalCount]])
   1141  end)
   1142 
   1143  it('can unmap lua mappings', function()
   1144    eq(
   1145      0,
   1146      exec_lua [[
   1147      GlobalCount = 0
   1148      vim.api.nvim_set_keymap('n', 'asdf', '', {
   1149        callback = function() GlobalCount = GlobalCount + 1 end,
   1150      })
   1151      return GlobalCount
   1152    ]]
   1153    )
   1154 
   1155    feed('asdf\n')
   1156 
   1157    eq(1, exec_lua [[return GlobalCount]])
   1158 
   1159    exec_lua [[vim.api.nvim_del_keymap('n', 'asdf' )]]
   1160 
   1161    feed('asdf\n')
   1162 
   1163    eq(1, exec_lua [[return GlobalCount]])
   1164    eq('\nNo mapping found', n.exec_capture('nmap asdf'))
   1165  end)
   1166 
   1167  it('no double-free when unmapping simplifiable lua mappings', function()
   1168    eq(
   1169      0,
   1170      exec_lua [[
   1171      GlobalCount = 0
   1172      vim.api.nvim_set_keymap('n', '<C-I>', '', {
   1173        callback = function() GlobalCount = GlobalCount + 1 end,
   1174      })
   1175      return GlobalCount
   1176    ]]
   1177    )
   1178 
   1179    feed('<C-I>\n')
   1180 
   1181    eq(1, exec_lua [[return GlobalCount]])
   1182 
   1183    exec_lua [[vim.api.nvim_del_keymap('n', '<C-I>')]]
   1184 
   1185    feed('<C-I>\n')
   1186 
   1187    eq(1, exec_lua [[return GlobalCount]])
   1188    eq('\nNo mapping found', n.exec_capture('nmap <C-I>'))
   1189  end)
   1190 
   1191  it('can set descriptions on mappings', function()
   1192    api.nvim_set_keymap('n', 'lhs', 'rhs', { desc = 'map description' })
   1193    eq(generate_mapargs('n', 'lhs', 'rhs', { desc = 'map description' }), get_mapargs('n', 'lhs'))
   1194    eq('\nn  lhs           rhs\n                 map description', n.exec_capture('nmap lhs'))
   1195  end)
   1196 
   1197  it('can define !-mode abbreviations with lua callbacks', function()
   1198    exec_lua [[
   1199      GlobalCount = 0
   1200      vim.api.nvim_set_keymap('!a', 'foo', '', {
   1201        expr = true,
   1202        callback = function()
   1203          GlobalCount = GlobalCount + 1
   1204          return tostring(GlobalCount)
   1205        end,
   1206      })
   1207    ]]
   1208 
   1209    feed 'iThe foo and the bar and the foo again<esc>'
   1210    eq('The 1 and the bar and the 2 again', api.nvim_get_current_line())
   1211 
   1212    feed ':let x = "The foo is the one"<cr>'
   1213    eq('The 3 is the one', api.nvim_eval 'x')
   1214  end)
   1215 
   1216  it('can define insert mode abbreviations with lua callbacks', function()
   1217    exec_lua [[
   1218      GlobalCount = 0
   1219      vim.api.nvim_set_keymap('ia', 'foo', '', {
   1220        expr = true,
   1221        callback = function()
   1222          GlobalCount = GlobalCount + 1
   1223          return tostring(GlobalCount)
   1224        end,
   1225      })
   1226    ]]
   1227 
   1228    feed 'iThe foo and the bar and the foo again<esc>'
   1229    eq('The 1 and the bar and the 2 again', api.nvim_get_current_line())
   1230 
   1231    feed ':let x = "The foo is the one"<cr>'
   1232    eq('The foo is the one', api.nvim_eval 'x')
   1233  end)
   1234 
   1235  it('can define cmdline mode abbreviations with lua callbacks', function()
   1236    exec_lua [[
   1237      GlobalCount = 0
   1238      vim.api.nvim_set_keymap('ca', 'foo', '', {
   1239        expr = true,
   1240        callback = function()
   1241          GlobalCount = GlobalCount + 1
   1242          return tostring(GlobalCount)
   1243        end,
   1244      })
   1245    ]]
   1246 
   1247    feed 'iThe foo and the bar and the foo again<esc>'
   1248    eq('The foo and the bar and the foo again', api.nvim_get_current_line())
   1249 
   1250    feed ':let x = "The foo is the one"<cr>'
   1251    eq('The 1 is the one', api.nvim_eval 'x')
   1252  end)
   1253 end)
   1254 
   1255 describe('nvim_buf_set_keymap, nvim_buf_del_keymap', function()
   1256  before_each(clear)
   1257 
   1258  -- nvim_set_keymap is implemented as a wrapped call to nvim_buf_set_keymap,
   1259  -- so its tests also effectively test nvim_buf_set_keymap
   1260 
   1261  -- here, we mainly test for buffer specificity and other special cases
   1262 
   1263  -- switch to the given buffer, abandoning any changes in the current buffer
   1264  local function switch_to_buf(bufnr)
   1265    command(bufnr .. 'buffer!')
   1266  end
   1267 
   1268  -- `set hidden`, then create two buffers and return their bufnr's
   1269  -- If start_from_first is truthy, the first buffer will be open when
   1270  -- the function returns; if falsy, the second buffer will be open.
   1271  local function make_two_buffers(start_from_first)
   1272    command('set hidden')
   1273 
   1274    local first_buf = api.nvim_call_function('bufnr', { '%' })
   1275    command('new')
   1276    local second_buf = api.nvim_call_function('bufnr', { '%' })
   1277    neq(second_buf, first_buf) -- sanity check
   1278 
   1279    if start_from_first then
   1280      switch_to_buf(first_buf)
   1281    end
   1282 
   1283    return first_buf, second_buf
   1284  end
   1285 
   1286  it('rejects negative bufnr values', function()
   1287    eq(
   1288      'Wrong type for argument 1 when calling nvim_buf_set_keymap, expecting Buffer',
   1289      pcall_err(api.nvim_buf_set_keymap, -1, '', 'lhs', 'rhs', {})
   1290    )
   1291  end)
   1292 
   1293  it('can set mappings active in the current buffer but not others', function()
   1294    local first, second = make_two_buffers(true)
   1295 
   1296    api.nvim_buf_set_keymap(0, '', 'lhs', 'irhs<Esc>', {})
   1297    command('normal lhs')
   1298    eq({ 'rhs' }, api.nvim_buf_get_lines(0, 0, 1, 1))
   1299 
   1300    -- mapping should have no effect in new buffer
   1301    switch_to_buf(second)
   1302    command('normal lhs')
   1303    eq({ '' }, api.nvim_buf_get_lines(0, 0, 1, 1))
   1304 
   1305    -- mapping should remain active in old buffer
   1306    switch_to_buf(first)
   1307    command('normal ^lhs')
   1308    eq({ 'rhsrhs' }, api.nvim_buf_get_lines(0, 0, 1, 1))
   1309  end)
   1310 
   1311  it('can set local mappings in buffer other than current', function()
   1312    local first = make_two_buffers(false)
   1313    api.nvim_buf_set_keymap(first, '', 'lhs', 'irhs<Esc>', {})
   1314 
   1315    -- shouldn't do anything
   1316    command('normal lhs')
   1317    eq({ '' }, api.nvim_buf_get_lines(0, 0, 1, 1))
   1318 
   1319    -- should take effect
   1320    switch_to_buf(first)
   1321    command('normal lhs')
   1322    eq({ 'rhs' }, api.nvim_buf_get_lines(0, 0, 1, 1))
   1323  end)
   1324 
   1325  it('can disable mappings made in another buffer, inside that buffer', function()
   1326    local first = make_two_buffers(false)
   1327    api.nvim_buf_set_keymap(first, '', 'lhs', 'irhs<Esc>', {})
   1328    api.nvim_buf_del_keymap(first, '', 'lhs')
   1329    switch_to_buf(first)
   1330 
   1331    -- shouldn't do anything
   1332    command('normal lhs')
   1333    eq({ '' }, api.nvim_buf_get_lines(0, 0, 1, 1))
   1334  end)
   1335 
   1336  it("can't disable mappings given wrong buffer handle", function()
   1337    local first, second = make_two_buffers(false)
   1338    api.nvim_buf_set_keymap(first, '', 'lhs', 'irhs<Esc>', {})
   1339    eq('E31: No such mapping', pcall_err(api.nvim_buf_del_keymap, second, '', 'lhs'))
   1340 
   1341    -- should still work
   1342    switch_to_buf(first)
   1343    command('normal lhs')
   1344    eq({ 'rhs' }, api.nvim_buf_get_lines(0, 0, 1, 1))
   1345  end)
   1346 
   1347  it('does not crash when setting mapping in a non-existing buffer #13541', function()
   1348    pcall_err(api.nvim_buf_set_keymap, 100, '', 'lsh', 'irhs<Esc>', {})
   1349    n.assert_alive()
   1350  end)
   1351 
   1352  it('can make lua mappings', function()
   1353    eq(
   1354      0,
   1355      exec_lua [[
   1356      GlobalCount = 0
   1357      vim.api.nvim_buf_set_keymap(0, 'n', 'asdf', '', {
   1358        callback = function() GlobalCount = GlobalCount + 1 end,
   1359      })
   1360      return GlobalCount
   1361    ]]
   1362    )
   1363 
   1364    feed('asdf\n')
   1365 
   1366    eq(1, exec_lua [[return GlobalCount]])
   1367  end)
   1368 
   1369  it('can make lua expr mappings replacing keycodes', function()
   1370    exec_lua [[
   1371      vim.api.nvim_buf_set_keymap(0, 'n', 'aa', '', {
   1372        callback = function() return '<Insert>π<C-V><M-π>foo<lt><Esc>' end,
   1373        expr = true,
   1374        replace_keycodes = true,
   1375      })
   1376    ]]
   1377 
   1378    feed('aa')
   1379 
   1380    eq({ 'π<M-π>foo<' }, api.nvim_buf_get_lines(0, 0, -1, false))
   1381  end)
   1382 
   1383  it('can make lua expr mappings without replacing keycodes', function()
   1384    exec_lua [[
   1385      vim.api.nvim_buf_set_keymap(0, 'i', 'aa', '', {
   1386        callback = function() return '<space>' end,
   1387        expr = true,
   1388      })
   1389    ]]
   1390 
   1391    feed('iaa<esc>')
   1392 
   1393    eq({ '<space>' }, api.nvim_buf_get_lines(0, 0, -1, false))
   1394  end)
   1395 
   1396  it('can overwrite lua mappings', function()
   1397    eq(
   1398      0,
   1399      exec_lua [[
   1400      GlobalCount = 0
   1401      vim.api.nvim_buf_set_keymap(0, 'n', 'asdf', '', {
   1402        callback = function() GlobalCount = GlobalCount + 1 end,
   1403      })
   1404      return GlobalCount
   1405    ]]
   1406    )
   1407 
   1408    feed('asdf\n')
   1409 
   1410    eq(1, exec_lua [[return GlobalCount]])
   1411 
   1412    exec_lua [[
   1413      vim.api.nvim_buf_set_keymap(0, 'n', 'asdf', '', {
   1414        callback = function() GlobalCount = GlobalCount - 1 end,
   1415      })
   1416    ]]
   1417 
   1418    feed('asdf\n')
   1419 
   1420    eq(0, exec_lua [[return GlobalCount]])
   1421  end)
   1422 
   1423  it('can unmap lua mappings', function()
   1424    eq(
   1425      0,
   1426      exec_lua [[
   1427      GlobalCount = 0
   1428      vim.api.nvim_buf_set_keymap(0, 'n', 'asdf', '', {
   1429        callback = function() GlobalCount = GlobalCount + 1 end,
   1430      })
   1431      return GlobalCount
   1432    ]]
   1433    )
   1434 
   1435    feed('asdf\n')
   1436 
   1437    eq(1, exec_lua [[return GlobalCount]])
   1438 
   1439    exec_lua [[vim.api.nvim_buf_del_keymap(0, 'n', 'asdf' )]]
   1440 
   1441    feed('asdf\n')
   1442 
   1443    eq(1, exec_lua [[return GlobalCount]])
   1444    eq('\nNo mapping found', n.exec_capture('nmap asdf'))
   1445  end)
   1446 
   1447  it('no double-free when unmapping simplifiable lua mappings', function()
   1448    eq(
   1449      0,
   1450      exec_lua [[
   1451      GlobalCount = 0
   1452      vim.api.nvim_buf_set_keymap(0, 'n', '<C-I>', '', {
   1453        callback = function() GlobalCount = GlobalCount + 1 end,
   1454      })
   1455      return GlobalCount
   1456    ]]
   1457    )
   1458 
   1459    feed('<C-I>\n')
   1460 
   1461    eq(1, exec_lua [[return GlobalCount]])
   1462 
   1463    exec_lua [[vim.api.nvim_buf_del_keymap(0, 'n', '<C-I>')]]
   1464 
   1465    feed('<C-I>\n')
   1466 
   1467    eq(1, exec_lua [[return GlobalCount]])
   1468    eq('\nNo mapping found', n.exec_capture('nmap <C-I>'))
   1469  end)
   1470 
   1471  it('does not overwrite in <unique> mappings', function()
   1472    api.nvim_buf_set_keymap(0, 'i', 'lhs', 'rhs', {})
   1473    eq(
   1474      'E227: Mapping already exists for lhs',
   1475      pcall_err(api.nvim_buf_set_keymap, 0, 'i', 'lhs', 'rhs', { unique = true })
   1476    )
   1477 
   1478    api.nvim_buf_set_keymap(0, 'ia', 'lhs2', 'rhs2', {})
   1479    eq(
   1480      'E226: Abbreviation already exists for lhs2',
   1481      pcall_err(api.nvim_buf_set_keymap, 0, 'ia', 'lhs2', 'rhs2', { unique = true })
   1482    )
   1483 
   1484    api.nvim_set_keymap('n', 'lhs', 'rhs', {})
   1485    eq(
   1486      'E225: Global mapping already exists for lhs',
   1487      pcall_err(api.nvim_buf_set_keymap, 0, 'n', 'lhs', 'rhs', { unique = true })
   1488    )
   1489  end)
   1490 end)