snacks.nvim/lua/snacks/keymap.lua
Folke Lemaitre 0bf34afe34
feat(keymap): add filetype and LSP-aware keymap management
Add `Snacks.keymap.set()` and `Snacks.keymap.del()` as enhanced alternatives
to `vim.keymap.set/del` with support for:

- **Filetype-specific keymaps**: Set keymaps that only apply to buffers with
  specific filetypes using the `ft` option
- **LSP-aware keymaps**: Set keymaps based on LSP client capabilities using
  the `lsp` option (leverages Snacks.util.lsp)
- **Automatic application**: Keymaps are applied to existing buffers and
  automatically set up for new buffers via FileType autocmd

Drop-in replacement API with smart defaults (silent by default).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 16:32:11 +02:00

161 lines
4.7 KiB
Lua

---@class snacks.keymap
local M = {}
M.meta = {
desc = "Better `vim.keymap` with support for filetypes and LSP clients",
needs_setup = false,
}
---@class snacks.keymap.set.Opts: vim.keymap.set.Opts
---@field ft? string|string[] Filetype(s) to set the keymap for.
---@field lsp? vim.lsp.get_clients.Filter Set for buffers with LSP clients matching this filter.
---@class snacks.keymap.del.Opts: vim.keymap.del.Opts
---@field buffer? boolean|number If true or 0, use the current buffer.
---@field ft? string|string[] Filetype(s) to set the keymap for.
---@field lsp? vim.lsp.get_clients.Filter Set for buffers with LSP clients matching this filter.
---@class snacks.Keymap
---@field mode string|string[] Mode "short-name" (see |nvim_set_keymap()|), or a list thereof.
---@field lhs string Left-hand side |{lhs}| of the mapping.
---@field rhs string|function Right-hand side |{rhs}| of the mapping, can be a Lua function.
---@field opts? snacks.keymap.set.Opts
local by_ft = {} ---@type table<string, table<string,snacks.Keymap>>
local by_lsp = {} ---@type table<string, table<string,snacks.Keymap>>
local valid = {
buffer = true,
desc = true,
callback = true,
remap = true,
silent = true,
expr = true,
nowait = true,
unique = true,
script = true,
replace_keycodes = true,
noremap = true,
}
local did_setup = false
---@param buf number
local function on_ft(buf)
local ft = vim.bo[buf].filetype
for _, map in pairs(by_ft[ft] or {}) do
vim.keymap.set(map.mode, map.lhs, map.rhs, Snacks.config.merge(map.opts or {}, { buffer = buf }))
end
end
local function setup()
if did_setup then
return
end
did_setup = true
vim.api.nvim_create_autocmd("FileType", {
group = vim.api.nvim_create_augroup("snacks.keymap.ft", { clear = true }),
callback = function(ev)
on_ft(ev.buf)
end,
})
end
---@param filter vim.lsp.get_clients.Filter
local function lsp_key(filter)
return vim.inspect(filter)
end
---@generic T: snacks.keymap.set.Opts|snacks.keymap.del.Opts
---@param ... T
---@return T opts, string[]? fts, vim.lsp.get_clients.Filter? lsp
local function get_opts(...)
---@type snacks.keymap.set.Opts|snacks.keymap.del.Opts
local opts = Snacks.config.merge(...)
opts.silent = opts.silent ~= false
opts.buffer = (opts.buffer == 0 or opts.buffer == true) and vim.api.nvim_get_current_buf() or opts.buffer
local fts = opts.ft and (type(opts.ft) == "table" and opts.ft or { opts.ft }) or nil --[[@as string[] ]]
local lsp = opts.lsp
local ret = vim.deepcopy(opts) ---@type table<string, any>
for k in pairs(ret) do
if not valid[k] then
ret[k] = nil
end
end
return ret, fts, lsp
end
---@param mode string|string[] Mode "short-name" (see |nvim_set_keymap()|), or a list thereof.
---@param lhs string Left-hand side |{lhs}| of the mapping.
---@param rhs string|function Right-hand side |{rhs}| of the mapping, can be a Lua function.
---@param opts? snacks.keymap.set.Opts
function M.set(mode, lhs, rhs, opts)
if type(mode) == "table" then
for _, m in ipairs(mode) do
M.set(m, lhs, rhs, opts)
end
return
end
local _opts, fts, lsp = get_opts(opts)
---@type snacks.Keymap
local km = { mode = mode, lhs = lhs, rhs = rhs, opts = _opts }
local key = ("%s:%s"):format(mode, lhs)
if lsp then
local lkey = lsp_key(lsp)
if not by_lsp[lkey] then
by_lsp[lkey] = {}
Snacks.util.lsp.on(lsp, function(buf)
for _, map in pairs(by_lsp[lkey]) do
vim.keymap.set(map.mode, map.lhs, map.rhs, Snacks.config.merge(map.opts or {}, { buffer = buf }))
end
end)
end
by_lsp[lkey][key] = km
elseif fts then
setup()
for _, ft in ipairs(fts) do
by_ft[ft] = by_ft[ft] or {}
by_ft[ft][key] = km
end
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
if vim.api.nvim_buf_is_loaded(buf) and vim.tbl_contains(fts, vim.bo[buf].filetype) then
on_ft(buf)
end
end
else
vim.keymap.set(mode, lhs, rhs, _opts)
end
end
---@param mode string|string[] Mode "short-name" (see |nvim_set_keymap()|), or a list thereof.
---@param lhs string Left-hand side |{lhs}| of the mapping.
---@param opts? snacks.keymap.del.Opts
function M.del(mode, lhs, opts)
if type(mode) == "table" then
for _, m in ipairs(mode) do
M.del(m, lhs, opts)
end
return
end
local _opts, fts, lsp = get_opts(opts)
local key = ("%s:%s"):format(mode, lhs)
if lsp then
local lkey = lsp_key(lsp)
if by_lsp[lkey] then
by_lsp[lkey][key] = nil
end
elseif fts then
for _, ft in ipairs(fts) do
if by_ft[ft] then
by_ft[ft][key] = nil
end
end
else
vim.keymap.del(mode, lhs, _opts)
end
end
return M