Merge #40579 from barrettruth/fix/del-keymap-lhs-only-30258

This commit is contained in:
Justin M. Keyes
2026-07-25 16:00:25 -04:00
committed by GitHub
15 changed files with 160 additions and 42 deletions

View File

@@ -620,7 +620,7 @@ nvim_del_current_line() *nvim_del_current_line()*
not allowed when |textlock| is active
Since: 0.1.0
nvim_del_keymap({mode}, {lhs}) *nvim_del_keymap()*
nvim_del_keymap({mode}, {lhs}, {opts}) *nvim_del_keymap()*
Unmaps a global |mapping| for the given mode.
To unmap a buffer-local mapping, use |nvim_buf_del_keymap()|.
@@ -629,8 +629,10 @@ nvim_del_keymap({mode}, {lhs}) *nvim_del_keymap()*
Since: 0.4.0
Parameters: ~
• {mode} (`string`)
• {lhs} (`string`)
• {mode} (`string`) Mode short-name ("n", "i", "v", ...)
• {lhs} (`string`) Left-hand-side |{lhs}| of the mapping.
• {opts} (`vim.api.keyset.keymap_del?`) Optional parameters.
• lhs: When true, only match {lhs}, not {rhs}.
See also: ~
• |nvim_set_keymap()|
@@ -2447,7 +2449,8 @@ nvim_buf_call({buf}, {fn}) *nvim_buf_call()*
Return: ~
(`any`) Value(s) returned by `fn()`.
nvim_buf_del_keymap({buf}, {mode}, {lhs}) *nvim_buf_del_keymap()*
*nvim_buf_del_keymap()*
nvim_buf_del_keymap({buf}, {mode}, {lhs}, {opts})
Unmaps a buffer-local |mapping| for the given mode.
Attributes: ~
@@ -2455,8 +2458,10 @@ nvim_buf_del_keymap({buf}, {mode}, {lhs}) *nvim_buf_del_keymap()*
Parameters: ~
• {buf} (`integer`) Buffer id, or 0 for current buffer
• {mode} (`string`)
• {lhs} (`string`)
• {mode} (`string`) Mode short-name ("n", "i", "v", ...)
• {lhs} (`string`) Left-hand-side |{lhs}| of the mapping.
• {opts} (`vim.api.keyset.keymap_del?`) Optional parameters.
• lhs: When true, only match {lhs}, not {rhs}.
See also: ~
• |nvim_del_keymap()|

View File

@@ -3798,6 +3798,7 @@ Lua module: vim.keymap *vim.keymap*
vim.keymap.del({modes}, {lhs}, {opts}) *vim.keymap.del()*
Removes a mapping, or removes each (mode, lhs) pair if `lhs` is a list.
Examples: >lua
vim.keymap.del('n', 'lhs')
vim.keymap.del({'n', 'i', 'v'}, '<leader>w', { buf = 5 })
@@ -3809,6 +3810,7 @@ vim.keymap.del({modes}, {lhs}, {opts}) *vim.keymap.del()*
• {opts} (`table?`) A table with the following fields:
• {buf}? (`integer`) Remove a mapping from the given buffer.
`0` for current.
• {lhs}? (`boolean`) When true, only match {lhs}, not {rhs}.
See also: ~
• |vim.keymap.set()|

View File

@@ -86,8 +86,10 @@ modes.
Remove the mapping of {lhs} for the modes where the
map command applies. The mapping may remain defined
for other modes where it applies.
It also works when {lhs} matches the {rhs} of a
mapping. This is for when an abbreviation applied.
If no mapping with {lhs} exists, it also works when
{lhs} matches the {rhs} of a mapping (see
|vim.keymap.del()| to avoid that). This is for
when an abbreviation applied.
Note: Trailing spaces are included in the {lhs}.
See |map-trailing-white|.

View File

@@ -432,6 +432,8 @@ These existing features changed their behavior.
• |vim.lsp.util.open_floating_preview()| windows (e.g. |vim.lsp.buf.hover()|)
converted to normal windows (e.g. with |CTRL-W_H|) are no longer closed
automatically.
• |nvim_del_keymap()|, |nvim_buf_del_keymap()| and |vim.keymap.del()| can
match only {lhs}, not {rhs}, with `opts.lhs=true`.
==============================================================================
REMOVED FEATURES *news-removed*

View File

@@ -333,9 +333,11 @@ function vim.api.nvim_buf_del_extmark(buf, ns_id, id) end
---
--- @see vim.api.nvim_del_keymap
--- @param buf integer Buffer id, or 0 for current buffer
--- @param mode string
--- @param lhs string
function vim.api.nvim_buf_del_keymap(buf, mode, lhs) end
--- @param mode string Mode short-name ("n", "i", "v", ...)
--- @param lhs string Left-hand-side `{lhs}` of the mapping.
--- @param opts vim.api.keyset.keymap_del? Optional parameters.
--- - lhs: When true, only match {lhs}, not {rhs}.
function vim.api.nvim_buf_del_keymap(buf, mode, lhs, opts) end
--- Deletes a named mark in the buffer. See `mark-motions`.
---
@@ -1069,9 +1071,11 @@ function vim.api.nvim_del_current_line() end
--- To unmap a buffer-local mapping, use `nvim_buf_del_keymap()`.
---
--- @see vim.api.nvim_set_keymap
--- @param mode string
--- @param lhs string
function vim.api.nvim_del_keymap(mode, lhs) end
--- @param mode string Mode short-name ("n", "i", "v", ...)
--- @param lhs string Left-hand-side `{lhs}` of the mapping.
--- @param opts vim.api.keyset.keymap_del? Optional parameters.
--- - lhs: When true, only match {lhs}, not {rhs}.
function vim.api.nvim_del_keymap(mode, lhs, opts) end
--- Deletes an uppercase/file named mark. See `mark-motions`.
---

View File

@@ -373,6 +373,9 @@ error('Cannot require a meta file')
--- @field silent? boolean
--- @field unique? boolean
--- @class vim.api.keyset.keymap_del
--- @field lhs? boolean
--- @class vim.api.keyset.ns_opts
--- @field wins? any[]

View File

@@ -119,8 +119,11 @@ end
---
--- Remove a mapping from the given buffer. `0` for current.
--- @field buf? integer
--- When true, only match {lhs}, not {rhs}.
--- @field lhs? boolean
--- Removes a mapping, or removes each (mode, lhs) pair if `lhs` is a list.
---
--- Examples:
---
--- ```lua
@@ -152,12 +155,14 @@ function keymap.del(modes, lhs, opts)
buf = opts.buffer == true and 0 or opts.buffer --[[@as integer?]]
end
local api_opts = { lhs = opts.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)
vim.api.nvim_buf_del_keymap(buf, m, l, api_opts)
else
vim.api.nvim_del_keymap(m, l)
vim.api.nvim_del_keymap(m, l, api_opts)
end
end
end

View File

@@ -143,26 +143,36 @@ local function process_proto(item, state)
cur_obj.name = item.name
cur_obj.params = cur_obj.params or {}
local documented = {} --- @type table<string,nvim.cdoc.parser.param>
local matched = {} --- @type table<nvim.cdoc.parser.param,true>
local params = {} --- @type nvim.cdoc.parser.param[]
for _, p in ipairs(cur_obj.params) do
documented[p.name] = documented[p.name] or p
end
for _, p in ipairs(item.parameters) do
local event_type = 'vim.api.keyset.events|vim.api.keyset.events[]'
local event = (item.name == 'nvim_create_autocmd' or item.name == 'nvim_exec_autocmds')
and p[2] == 'event'
local param = { name = p[2], type = event and event_type or api_type(p[1]) }
local added = false
for _, cp in ipairs(cur_obj.params) do
if cp.name == param.name then
cp.type = param.type
added = true
break
end
local param = documented[p[2]]
if param then
matched[param] = true
else
param = { name = p[2] } --[[@as nvim.cdoc.parser.param]]
end
param.type = event and event_type or api_type(p[1])
params[#params + 1] = param
end
if not added then
table.insert(cur_obj.params, param)
for _, p in ipairs(cur_obj.params) do
if not matched[p] then
params[#params + 1] = p
end
end
cur_obj.params = params
cur_obj.returns = cur_obj.returns or { {
name = '',
type = '',

View File

@@ -890,19 +890,25 @@ void nvim_buf_set_keymap(uint64_t channel_id, Buffer buf, String mode, String lh
Dict(keymap) *opts, Error *err)
FUNC_API_SINCE(6)
{
modify_keymap(channel_id, buf, false, mode, lhs, rhs, opts, err);
modify_keymap(channel_id, buf, MAPTYPE_MAP, mode, lhs, rhs, opts, err);
}
/// Unmaps a buffer-local |mapping| for the given mode.
///
/// @see |nvim_del_keymap()|
///
/// @param buf Buffer id, or 0 for current buffer
void nvim_buf_del_keymap(uint64_t channel_id, Buffer buf, String mode, String lhs, Error *err)
/// @param buf Buffer id, or 0 for current buffer
/// @param mode Mode short-name ("n", "i", "v", ...)
/// @param lhs Left-hand-side |{lhs}| of the mapping.
/// @param opts Optional parameters.
/// - lhs: When true, only match {lhs}, not {rhs}.
void nvim_buf_del_keymap(uint64_t channel_id, Buffer buf, String mode, String lhs,
Dict(keymap_del) *opts, Error *err)
FUNC_API_SINCE(6)
{
String rhs = { .data = "", .size = 0 };
modify_keymap(channel_id, buf, true, mode, lhs, rhs, NULL, err);
int maptype = opts && opts->lhs ? MAPTYPE_UNMAP_LHS : MAPTYPE_UNMAP;
modify_keymap(channel_id, buf, maptype, mode, lhs, rhs, NULL, err);
}
/// Sets a buffer-scoped (b:) variable

View File

@@ -94,6 +94,11 @@ typedef struct {
Boolean replace_keycodes;
} Dict(keymap);
typedef struct {
OptionalKeys is_set__keymap_del_;
Boolean lhs;
} Dict(keymap_del);
typedef struct {
Boolean builtin;
} Dict(get_commands);

View File

@@ -1630,18 +1630,24 @@ void nvim_set_keymap(uint64_t channel_id, String mode, String lhs, String rhs, D
Error *err)
FUNC_API_SINCE(6)
{
modify_keymap(channel_id, -1, false, mode, lhs, rhs, opts, err);
modify_keymap(channel_id, -1, MAPTYPE_MAP, mode, lhs, rhs, opts, err);
}
/// Unmaps a global |mapping| for the given mode.
///
/// To unmap a buffer-local mapping, use |nvim_buf_del_keymap()|.
///
/// @param mode Mode short-name ("n", "i", "v", ...)
/// @param lhs Left-hand-side |{lhs}| of the mapping.
/// @param opts Optional parameters.
/// - lhs: When true, only match {lhs}, not {rhs}.
///
/// @see |nvim_set_keymap()|
void nvim_del_keymap(uint64_t channel_id, String mode, String lhs, Error *err)
void nvim_del_keymap(uint64_t channel_id, String mode, String lhs, Dict(keymap_del) *opts,
Error *err)
FUNC_API_SINCE(6)
{
nvim_buf_del_keymap(channel_id, -1, mode, lhs, err);
nvim_buf_del_keymap(channel_id, -1, mode, lhs, opts, err);
}
/// Returns a 2-tuple (Array), where item 0 is the current channel id and item

View File

@@ -2738,11 +2738,13 @@ void ex_abclear(exarg_T *eap)
/// Arguments are handled like @ref nvim_set_keymap unless noted.
/// @param buffer Buffer handle for a specific buffer, or 0 for the current
/// buffer, or -1 to signify global behavior ("all buffers")
/// @param is_unmap When true, removes the mapping that matches {lhs}.
void modify_keymap(uint64_t channel_id, Buffer buffer, bool is_unmap, String mode, String lhs,
/// @param maptype MAPTYPE_MAP to set a mapping, MAPTYPE_UNMAP to remove the mapping that
/// matches {lhs} or {rhs}, MAPTYPE_UNMAP_LHS to only match {lhs}.
void modify_keymap(uint64_t channel_id, Buffer buffer, int maptype, String mode, String lhs,
String rhs, Dict(keymap) *opts, Error *err)
{
LuaRef lua_funcref = LUA_NOREF;
bool is_unmap = maptype == MAPTYPE_UNMAP || maptype == MAPTYPE_UNMAP_LHS;
bool global = (buffer == -1);
if (global) {
buffer = 0;
@@ -2834,14 +2836,11 @@ void modify_keymap(uint64_t channel_id, Buffer buffer, bool is_unmap, String mod
}
// buf_do_map() reads noremap/unmap as its own argument.
int maptype_val = MAPTYPE_MAP;
if (is_unmap) {
maptype_val = MAPTYPE_UNMAP;
} else if (is_noremap) {
maptype_val = MAPTYPE_NOREMAP;
if (maptype == MAPTYPE_MAP && is_noremap) {
maptype = MAPTYPE_NOREMAP;
}
switch (buf_do_map(maptype_val, &parsed_args, mode_val, is_abbrev, target_buf)) {
switch (buf_do_map(maptype, &parsed_args, mode_val, is_abbrev, target_buf)) {
case 0:
break;
case 1:

View File

@@ -594,6 +594,39 @@ describe('nvim_set_keymap, nvim_del_keymap', function()
eq('Invalid (empty) LHS', pcall_err(api.nvim_del_keymap, '', ''))
end)
it('del_keymap preserves {rhs} fallback by default #30258', function()
api.nvim_set_keymap('n', 'ge', 'q', {})
api.nvim_del_keymap('n', 'q')
eq({}, get_mapargs('n', 'ge'))
end)
it('del_keymap with opts.lhs matches only {lhs}, never {rhs} #30258', function()
api.nvim_set_keymap('n', 'ge', 'q', {})
eq('E31: No such mapping', pcall_err(api.nvim_del_keymap, 'n', 'q', { lhs = true }))
eq(generate_mapargs('n', 'ge', 'q'), get_mapargs('n', 'ge'))
api.nvim_del_keymap('n', 'ge', { lhs = true })
eq({}, get_mapargs('n', 'ge'))
end)
it('del_keymap for abbreviations with opts.lhs matches only {lhs}, never {rhs} #30258', function()
api.nvim_set_keymap('ia', 'foo', 'bar', {})
eq('E31: No such mapping', pcall_err(api.nvim_del_keymap, 'ia', 'bar', { lhs = true }))
eq(generate_mapargs('ia', 'foo', 'bar'), get_mapargs('ia', 'foo'))
api.nvim_del_keymap('ia', 'foo', { lhs = true })
eq({}, get_mapargs('ia', 'foo'))
end)
it('buf_del_keymap with opts.lhs matches only {lhs}, never {rhs} #30258', function()
api.nvim_buf_set_keymap(0, 'n', 'ge', 'q', {})
eq('E31: No such mapping', pcall_err(api.nvim_buf_del_keymap, 0, 'n', 'q', { lhs = true }))
eq(generate_mapargs('n', 'ge', 'q', { buffer = 1 }), get_mapargs('n', 'ge'))
api.nvim_buf_del_keymap(0, 'n', 'ge', { lhs = true })
eq({}, get_mapargs('n', 'ge'))
end)
it('error if LHS longer than MAXMAPLEN', function()
-- assume MAXMAPLEN of 50 chars, as declared in mapping_defs.h
local MAXMAPLEN = 50

View File

@@ -93,6 +93,9 @@ describe('api metadata', function()
error(('"%s": parameter %d was optional, now required'):format(old_fn.name, idx))
end
end
while #new_fn.parameters > #old_fn.parameters and new_fn.parameters[#new_fn.parameters][3] do
table.remove(new_fn.parameters)
end
old_fn = normalize_func_metadata(old_fn)
new_fn = normalize_func_metadata(new_fn)
if old_fn.return_type == 'void' then
@@ -248,6 +251,22 @@ describe('api metadata', function()
)
end)
it('optional param may be added', function()
assert_func_backcompat({
name = 'nvim_example',
method = false,
since = 1,
return_type = 'void',
parameters = { { 'String', 'src', false } },
}, {
name = 'nvim_example',
method = false,
since = 1,
return_type = 'void',
parameters = { { 'String', 'src', false }, { 'Dict', 'opts', true } },
})
end)
it('UI events are compatible with old metadata or have new level', function()
local ui_events_new = name_table(api_info.ui_events)
local ui_events_compat = {}

View File

@@ -3049,6 +3049,23 @@ describe('vim.keymap', function()
eq('\nNo mapping found', n.exec_capture('nmap qwer'))
end)
it('unmap with lhs option', function()
eq(
'q',
exec_lua [[
vim.keymap.set('n', 'ge', 'q')
local ok, err = pcall(vim.keymap.del, 'n', 'q', { lhs = true })
assert(not ok and err:match('E31: No such mapping'), err)
return vim.fn.maparg('ge', 'n')
]]
)
exec_lua [[
vim.keymap.del('n', 'ge', { lhs = true })
]]
eq('\nNo mapping found', n.exec_capture('nmap ge'))
end)
it('buffer-local mappings', function()
eq(
0,