From e728c100b5bd4b0c6671d8eb524e15d67cacb42a Mon Sep 17 00:00:00 2001 From: Olivia Kinnear Date: Fri, 29 May 2026 13:45:33 -0500 Subject: [PATCH] feat(lua): support table `lhs` in vim.keymap.set()/del() #39948 Problem: It is repetitive to map multiple keymaps to do the same thing. Here are some cases where being able to do this would be useful: -- Visual movement for both j/k and down/up: vim.keymap.set({ 'n', 'x' }, { 'j', '' }, 'v:count == 0 ? "gj" : "j"', { expr = true }) vim.keymap.set({ 'n', 'x' }, { 'k', '' }, 'v:count == 0 ? "gk" : "k"', { expr = true }) -- Map multiple keys to `` concisely: vim.keymap.set({ 'n', 'x' }, { '', '', '' }, '') -- Remove multiple keymaps at once: vim.keymap.del('n', { 'gri', 'grn', 'grr' }) Solution: Support the `lhs` of `vim.keymap.set()` and `vim.keymap.del()` being a table, in the same way that `modes` can be. --- runtime/doc/lua.txt | 5 ++-- runtime/doc/news.txt | 1 + runtime/lua/vim/keymap.lua | 43 ++++++++++++++++++-------------- test/functional/lua/vim_spec.lua | 21 +++++++++++++--- 4 files changed, 45 insertions(+), 25 deletions(-) diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index 1c659c14a8..997f2dcc21 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -3742,7 +3742,7 @@ vim.keymap.del({modes}, {lhs}, {opts}) *vim.keymap.del()* Parameters: ~ • {modes} (`string|string[]`) - • {lhs} (`string`) + • {lhs} (`string|string[]`) • {opts} (`table?`) A table with the following fields: • {buf}? (`integer`) Remove a mapping from the given buffer. `0` for current. @@ -3781,7 +3781,8 @@ vim.keymap.set({modes}, {lhs}, {rhs}, {opts}) *vim.keymap.set()* Parameters: ~ • {modes} (`string|string[]`) Mode "short-name" (see |nvim_set_keymap()|), or a list thereof. - • {lhs} (`string`) Left-hand side |{lhs}| of the mapping. + • {lhs} (`string|string[]`) Left-hand side |{lhs}| of the mapping, or + a list thereof. • {rhs} (`string|function`) Right-hand side |{rhs}| of the mapping, can be a Lua function. • {opts} (`table?`) Table of |:map-arguments|. Same as diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 024e30ca06..7541f116d0 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -288,6 +288,7 @@ These existing features changed their behavior. • |nvim_create_autocmd()| • |nvim_exec_autocmds()| • |nvim_get_autocmds()| +• |vim.keymap.set()| and |vim.keymap.del()| accept a list of strings for `lhs`. ============================================================================== REMOVED FEATURES *news-removed* diff --git a/runtime/lua/vim/keymap.lua b/runtime/lua/vim/keymap.lua index 7680196d5d..ade3c752d6 100644 --- a/runtime/lua/vim/keymap.lua +++ b/runtime/lua/vim/keymap.lua @@ -46,7 +46,7 @@ local keymap = {} --- ``` --- ---@param modes string|string[] Mode "short-name" (see |nvim_set_keymap()|), or a list thereof. ----@param lhs string Left-hand side |{lhs}| of the mapping. +---@param lhs string|string[] Left-hand side |{lhs}| of the mapping, or a list thereof. ---@param rhs string|function Right-hand side |{rhs}| of the mapping, can be a Lua function. ---@param opts? vim.keymap.set.Opts --- @@ -56,7 +56,7 @@ local keymap = {} ---@see |mapset()| function keymap.set(modes, lhs, rhs, opts) vim.validate('modes', modes, { 'string', 'table' }) - vim.validate('lhs', lhs, 'string') + vim.validate('lhs', lhs, { 'string', 'table' }) vim.validate('rhs', rhs, { 'string', 'function' }) vim.validate('opts', opts, 'table', true) @@ -64,6 +64,8 @@ function keymap.set(modes, lhs, rhs, opts) ---@cast modes string[] modes = type(modes) == 'string' and { modes } or modes + ---@cast lhs string[] + lhs = type(lhs) == 'string' and { lhs } or lhs if opts.expr and opts.replace_keycodes ~= false then opts.replace_keycodes = true @@ -93,13 +95,13 @@ function keymap.set(modes, lhs, rhs, opts) opts.buffer = nil end - if buf then - for _, m in ipairs(modes) do - vim.api.nvim_buf_set_keymap(buf, m, lhs, rhs, opts) - end - else - for _, m in ipairs(modes) do - vim.api.nvim_set_keymap(m, lhs, rhs, opts) + for _, m in ipairs(modes) do + for _, l in ipairs(lhs) do + if buf then + vim.api.nvim_buf_set_keymap(buf, m, l, rhs, opts) + else + vim.api.nvim_set_keymap(m, l, rhs, opts) + end end end end @@ -120,17 +122,20 @@ end --- ``` --- ---@param modes string|string[] ----@param lhs string +---@param lhs string|string[] ---@param opts? vim.keymap.del.Opts ---@see |vim.keymap.set()| function keymap.del(modes, lhs, opts) vim.validate('mode', modes, { 'string', 'table' }) - vim.validate('lhs', lhs, 'string') + vim.validate('lhs', lhs, { 'string', 'table' }) vim.validate('opts', opts, 'table', true) opts = opts or {} - modes = type(modes) == 'string' and { modes } or modes + --- @cast modes string[] + modes = type(modes) == 'string' and { modes } or modes + ---@cast lhs string[] + lhs = type(lhs) == 'string' and { lhs } or lhs local buf = opts.buf --- @cast opts +{buffer?:integer|boolean} @@ -140,13 +145,13 @@ function keymap.del(modes, lhs, opts) buf = opts.buffer == true and 0 or opts.buffer --[[@as integer?]] end - if buf then - for _, mode in ipairs(modes) do - vim.api.nvim_buf_del_keymap(buf, mode, lhs) - end - else - for _, mode in ipairs(modes) do - vim.api.nvim_del_keymap(mode, lhs) + for _, m in ipairs(modes) do + for _, l in ipairs(lhs) do + if buf then + vim.api.nvim_buf_del_keymap(buf, m, l) + else + vim.api.nvim_del_keymap(m, l) + end end end end diff --git a/test/functional/lua/vim_spec.lua b/test/functional/lua/vim_spec.lua index d5912ad1ea..d5dc3c9659 100644 --- a/test/functional/lua/vim_spec.lua +++ b/test/functional/lua/vim_spec.lua @@ -2929,8 +2929,8 @@ describe('vim.keymap', function() ) matches( - 'lhs: expected string, got table', - pcall_err(exec_lua, [[vim.keymap.set('n', {}, print)]]) + 'lhs: expected string|table, got number', + pcall_err(exec_lua, [[vim.keymap.set('n', 5, print)]]) ) matches( @@ -2957,13 +2957,16 @@ describe('vim.keymap', function() exec_lua [[ GlobalCount = 0 vim.keymap.set('n', 'asdf', function() GlobalCount = GlobalCount + 1 end) + vim.keymap.set('n', { 'ghjk', 'qwer' }, function() GlobalCount = GlobalCount + 1 end) return GlobalCount ]] ) feed('asdf\n') - eq(1, exec_lua [[return GlobalCount]]) + + feed('ghjk\nqwer\n') + eq(3, exec_lua [[return GlobalCount]]) end) it('expr mapping', function() @@ -3004,7 +3007,7 @@ describe('vim.keymap', function() 0, exec_lua [[ GlobalCount = 0 - vim.keymap.set('n', 'asdf', function() GlobalCount = GlobalCount + 1 end) + vim.keymap.set('n', { 'asdf', 'ghjk', 'qwer' }, function() GlobalCount = GlobalCount + 1 end) return GlobalCount ]] ) @@ -3021,6 +3024,16 @@ describe('vim.keymap', function() eq(1, exec_lua [[return GlobalCount]]) eq('\nNo mapping found', n.exec_capture('nmap asdf')) + + exec_lua [[ + vim.keymap.del('n', { 'ghjk', 'qwer' }) + ]] + + feed('ghjk\nqwer\n') + + eq(1, exec_lua [[return GlobalCount]]) + eq('\nNo mapping found', n.exec_capture('nmap ghjk')) + eq('\nNo mapping found', n.exec_capture('nmap qwer')) end) it('buffer-local mappings', function()