feat(picker.lsp): added lsp_incoming_calls and lsp_outgoing_calls. Closes #1843

This commit is contained in:
Folke Lemaitre 2025-10-22 09:32:50 +02:00
parent a45503b957
commit 55d6670a7e
No known key found for this signature in database
GPG key ID: 9B52594D560070AB
4 changed files with 143 additions and 27 deletions

View file

@ -65,6 +65,8 @@ M.examples.general = {
{ "gr", function() Snacks.picker.lsp_references() end, nowait = true, desc = "References" },
{ "gI", function() Snacks.picker.lsp_implementations() end, desc = "Goto Implementation" },
{ "gy", function() Snacks.picker.lsp_type_definitions() end, desc = "Goto T[y]pe Definition" },
{ "gai", function() Snacks.picker.lsp_incoming_calls() end, desc = "C[a]lls Incoming" },
{ "gao", function() Snacks.picker.lsp_outgoing_calls() end, desc = "C[a]lls Outgoing" },
{ "<leader>ss", function() Snacks.picker.lsp_symbols() end, desc = "LSP Symbols" },
{ "<leader>sS", function() Snacks.picker.lsp_workspace_symbols() end, desc = "LSP Workspace Symbols" },
},

View file

@ -550,6 +550,28 @@ M.lsp_implementations = {
jump = { tagstack = true, reuse_win = true },
}
-- LSP incoming calls
---@type snacks.picker.lsp.Config
M.lsp_incoming_calls = {
finder = "lsp_incoming_calls",
format = "lsp_symbol",
include_current = false,
workspace = true, -- this ensures the file is included in the formatter
auto_confirm = true,
jump = { tagstack = true, reuse_win = true },
}
-- LSP outgoing calls
---@type snacks.picker.lsp.Config
M.lsp_outgoing_calls = {
finder = "lsp_outgoing_calls",
format = "lsp_symbol",
include_current = false,
workspace = true, -- this ensures the file is included in the formatter
auto_confirm = true,
jump = { tagstack = true, reuse_win = true },
}
-- LSP references
---@class snacks.picker.lsp.references.Config: snacks.picker.lsp.Config
---@field include_declaration? boolean default true

View file

@ -96,50 +96,83 @@ function M.get_clients(buf, method)
end, clients)
end
---@param buf number
---@class snacks.picker.lsp.Requester
---@field async snacks.picker.Async
---@field requests {client_id:number, request_id:number}[]
---@field completed number
local R = {}
R.__index = R
function R.new()
local self = setmetatable({}, R)
self.async = Async.running()
self.requests = {}
self.completed = 0
self.async:on(
"abort",
vim.schedule_wrap(function()
self:cancel()
end)
)
return self
end
function R:cancel()
while #self.requests > 0 do
local req = table.remove(self.requests)
local client = vim.lsp.get_client_by_id(req.client_id)
if client then
client:cancel_request(req.request_id)
end
end
end
---@param buf number|vim.lsp.Client
---@param method string
---@param params fun(client:vim.lsp.Client):table
---@param cb fun(client:vim.lsp.Client, result:table, params:table)
---@async
function M.request(buf, method, params, cb)
local async = Async.running()
local cancel = {} ---@type fun()[]
function R:request(buf, method, params, cb)
local clients = type(buf) == "number" and M.get_clients(buf, method) or {
wrap(buf --[[@as vim.lsp.Client]]),
}
if vim.tbl_isempty(clients) then
return self.async:resume()
end
async:on(
"abort",
vim.schedule_wrap(function()
vim.tbl_map(pcall, cancel)
cancel = {}
end)
)
vim.schedule(function()
local clients = M.get_clients(buf, method)
if vim.tbl_isempty(clients) then
return async:resume()
end
local remaining = #clients
for _, client in ipairs(clients) do
local p = params(client)
local status, request_id = client:request(method, p, function(_, result)
if result then
cb(client, result, p)
end
remaining = remaining - 1
if remaining == 0 then
async:resume()
end
self.completed = self.completed + 1
self.async:resume()
end)
if status and request_id then
table.insert(cancel, function()
client:cancel_request(request_id)
end)
table.insert(self.requests, { client_id = client.id, request_id = request_id })
end
end
self.async:resume()
end)
self.async:suspend()
return self
end
async:suspend()
cancel = {}
async = Async.nop()
function R:wait()
while self.completed < #self.requests do
self.async:suspend()
end
end
---@param buf number
---@param method string
---@param params fun(client:vim.lsp.Client):table
---@param cb fun(client:vim.lsp.Client, result:table, params:table)
---@async
function M.request(buf, method, params, cb)
R.new():request(buf, method, params, cb):wait()
end
-- Support for older versions of neovim
@ -258,7 +291,7 @@ function M.results_to_items(client, results, opts)
detail = result.detail,
name = result.name,
text = "",
range = result.range,
range = result.range or result.selectionRange,
item = result,
}
local uri = result.location and result.location.uri or result.uri or opts.default_uri
@ -389,6 +422,51 @@ function M.symbols(opts, ctx)
end
end
---@param opts snacks.picker.lsp.Config
---@param filter snacks.picker.Filter
---@param incoming? boolean
function M.call_hierarchy(opts, filter, incoming)
local method = ("callHierarchy/%sCalls"):format(incoming and "incoming" or "outgoing")
local buf = filter.current_buf
local win = filter.current_win
---@async
---@param cb async fun(item: snacks.picker.finder.Item)
return function(cb)
local requester = R.new()
requester:request(buf, "textDocument/prepareCallHierarchy", function(client)
return vim.lsp.util.make_position_params(win, client.offset_encoding)
end, function(client, result)
---@cast result lsp.CallHierarchyItem[]
for _, res in ipairs(result or {}) do
requester:request(client, method, function()
return { item = res }
end, function(_, calls)
---@cast calls (lsp.CallHierarchyIncomingCall|lsp.CallHierarchyOutgoingCall)[]
local call_items = {} ---@type lsp.CallHierarchyItem[]
---@param call lsp.CallHierarchyIncomingCall|lsp.CallHierarchyOutgoingCall
for _, call in ipairs(calls) do
if incoming then
for _, range in ipairs(call.fromRanges or {}) do
local from = vim.deepcopy(call.from)
from.selectionRange = range or from.selectionRange
table.insert(call_items, from)
end
else
table.insert(call_items, call.to)
end
end
local items = M.results_to_items(client, call_items, { default_uri = res.uri })
vim.tbl_map(cb, items)
end)
end
end)
requester:wait()
end
end
---@param opts snacks.picker.lsp.references.Config
---@type snacks.picker.finder
function M.references(opts, ctx)
@ -402,6 +480,18 @@ function M.references(opts, ctx)
)
end
---@param opts snacks.picker.lsp.Config
---@type snacks.picker.finder
function M.incoming_calls(opts, ctx)
return M.call_hierarchy(opts, ctx.filter, true)
end
---@param opts snacks.picker.lsp.Config
---@type snacks.picker.finder
function M.outgoing_calls(opts, ctx)
return M.call_hierarchy(opts, ctx.filter, false)
end
---@param opts snacks.picker.lsp.Config
---@type snacks.picker.finder
function M.definitions(opts, ctx)

View file

@ -35,6 +35,8 @@
---@field lsp_declarations fun(opts?: snacks.picker.lsp.Config|{}): snacks.Picker
---@field lsp_definitions fun(opts?: snacks.picker.lsp.Config|{}): snacks.Picker
---@field lsp_implementations fun(opts?: snacks.picker.lsp.Config|{}): snacks.Picker
---@field lsp_incoming_calls fun(opts?: snacks.picker.lsp.Config|{}): snacks.Picker
---@field lsp_outgoing_calls fun(opts?: snacks.picker.lsp.Config|{}): snacks.Picker
---@field lsp_references fun(opts?: snacks.picker.lsp.references.Config|{}): snacks.Picker
---@field lsp_symbols fun(opts?: snacks.picker.lsp.symbols.Config|{}): snacks.Picker
---@field lsp_type_definitions fun(opts?: snacks.picker.lsp.Config|{}): snacks.Picker