feat(util): add LSP utility module with dynamic capability handlers

Add `Snacks.util.lsp.on()` to register handlers that fire when LSP clients
attach with specific capabilities. Supports filtering by:
- LSP method/capability
- Client name
- Buffer ID
- Any vim.lsp.get_clients() filter

Features:
- Handles both LspAttach and client/registerCapability events
- Ensures handlers only fire once per buffer
- Lazy-loaded via Snacks.util metatable

This provides a foundation for LSP-aware features like conditional keymaps.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Folke Lemaitre 2025-10-25 16:31:13 +02:00
parent f75eaf1e18
commit 7a63ba5d37
No known key found for this signature in database
GPG key ID: 9B52594D560070AB
2 changed files with 95 additions and 1 deletions

View file

@ -1,5 +1,15 @@
---@class snacks.util
local M = {}
---@field spawn snacks.spawn
---@field lsp snacks.lsp
local M = setmetatable({}, {
---@param M snacks.util
__index = function(M, k)
if vim.tbl_contains({ "spawn", "lsp" }, k) then
M[k] = require("snacks.util." .. k)
end
return rawget(M, k)
end,
})
M.meta = {
desc = "Utility functions for Snacks _(library)_",

84
lua/snacks/util/lsp.lua Normal file
View file

@ -0,0 +1,84 @@
---@class snacks.lsp
local M = {}
---@alias snacks.lsp.handler.cb fun(buf: number, client: vim.lsp.Client):any?
---@class snacks.lsp.Handler
---@field filter vim.lsp.get_clients.Filter
---@field cb snacks.lsp.handler.cb
---@field done table<number, boolean>
local _handlers = {} ---@type snacks.lsp.Handler[]
local did_setup = false
---@param filter vim.lsp.get_clients.Filter
local function _handle(filter)
---@param h snacks.lsp.Handler
local handlers = vim.tbl_filter(function(h)
---@diagnostic disable-next-line: no-unknown
for k, v in pairs(filter) do
if h.filter[k] ~= nil and h.filter[k] ~= v then
return false
end
end
return true
end, _handlers)
if #handlers == 0 then
return
end
for _, state in ipairs(handlers) do
local f = vim.deepcopy(state.filter)
f = vim.tbl_extend("force", f, filter)
local clients = vim.lsp.get_clients(f)
for _, client in ipairs(clients) do
for buf in pairs(client.attached_buffers) do
if not state.done[buf] then
state.done[buf] = true
local ok, err = pcall(state.cb, buf, client)
if not ok then
vim.schedule(function()
Snacks.notify.error(("Error in handler:\n%s\n```lua\n%s\n```"):format(err, vim.inspect(state.filter)))
end)
end
end
end
end
end
end
local function setup()
if did_setup then
return
end
did_setup = true
local register_capability = vim.lsp.handlers["client/registerCapability"]
vim.lsp.handlers["client/registerCapability"] = function(err, res, ctx)
---@cast res lsp.RegistrationParams
local ret = register_capability(err, res, ctx) ---@type any
vim.schedule(function()
for _, m in ipairs(res.registrations or {}) do
_handle({ method = m.method, id = ctx.client_id })
end
end)
return ret
end
vim.api.nvim_create_autocmd("LspAttach", {
group = vim.api.nvim_create_augroup("snacks.lsp.on_attach", { clear = true }),
callback = function(ev)
_handle({ id = ev.data.client_id, buffer = ev.buf })
end,
})
end
---@param filter vim.lsp.get_clients.Filter
---@param cb snacks.lsp.handler.cb
function M.on(filter, cb)
setup()
table.insert(_handlers, { filter = filter, cb = cb, done = {} })
_handle(filter)
end
return M