Skip to content

Commit 55d6670

Browse files
committed
feat(picker.lsp): added lsp_incoming_calls and lsp_outgoing_calls. Closes #1843
1 parent a45503b commit 55d6670

File tree

4 files changed

+143
-27
lines changed

4 files changed

+143
-27
lines changed

docs/examples/picker.lua

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ M.examples.general = {
6565
{ "gr", function() Snacks.picker.lsp_references() end, nowait = true, desc = "References" },
6666
{ "gI", function() Snacks.picker.lsp_implementations() end, desc = "Goto Implementation" },
6767
{ "gy", function() Snacks.picker.lsp_type_definitions() end, desc = "Goto T[y]pe Definition" },
68+
{ "gai", function() Snacks.picker.lsp_incoming_calls() end, desc = "C[a]lls Incoming" },
69+
{ "gao", function() Snacks.picker.lsp_outgoing_calls() end, desc = "C[a]lls Outgoing" },
6870
{ "<leader>ss", function() Snacks.picker.lsp_symbols() end, desc = "LSP Symbols" },
6971
{ "<leader>sS", function() Snacks.picker.lsp_workspace_symbols() end, desc = "LSP Workspace Symbols" },
7072
},

lua/snacks/picker/config/sources.lua

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,28 @@ M.lsp_implementations = {
550550
jump = { tagstack = true, reuse_win = true },
551551
}
552552

553+
-- LSP incoming calls
554+
---@type snacks.picker.lsp.Config
555+
M.lsp_incoming_calls = {
556+
finder = "lsp_incoming_calls",
557+
format = "lsp_symbol",
558+
include_current = false,
559+
workspace = true, -- this ensures the file is included in the formatter
560+
auto_confirm = true,
561+
jump = { tagstack = true, reuse_win = true },
562+
}
563+
564+
-- LSP outgoing calls
565+
---@type snacks.picker.lsp.Config
566+
M.lsp_outgoing_calls = {
567+
finder = "lsp_outgoing_calls",
568+
format = "lsp_symbol",
569+
include_current = false,
570+
workspace = true, -- this ensures the file is included in the formatter
571+
auto_confirm = true,
572+
jump = { tagstack = true, reuse_win = true },
573+
}
574+
553575
-- LSP references
554576
---@class snacks.picker.lsp.references.Config: snacks.picker.lsp.Config
555577
---@field include_declaration? boolean default true

lua/snacks/picker/source/lsp/init.lua

Lines changed: 117 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -96,50 +96,83 @@ function M.get_clients(buf, method)
9696
end, clients)
9797
end
9898

99-
---@param buf number
99+
---@class snacks.picker.lsp.Requester
100+
---@field async snacks.picker.Async
101+
---@field requests {client_id:number, request_id:number}[]
102+
---@field completed number
103+
local R = {}
104+
R.__index = R
105+
106+
function R.new()
107+
local self = setmetatable({}, R)
108+
self.async = Async.running()
109+
self.requests = {}
110+
self.completed = 0
111+
self.async:on(
112+
"abort",
113+
vim.schedule_wrap(function()
114+
self:cancel()
115+
end)
116+
)
117+
return self
118+
end
119+
120+
function R:cancel()
121+
while #self.requests > 0 do
122+
local req = table.remove(self.requests)
123+
local client = vim.lsp.get_client_by_id(req.client_id)
124+
if client then
125+
client:cancel_request(req.request_id)
126+
end
127+
end
128+
end
129+
130+
---@param buf number|vim.lsp.Client
100131
---@param method string
101132
---@param params fun(client:vim.lsp.Client):table
102133
---@param cb fun(client:vim.lsp.Client, result:table, params:table)
103134
---@async
104-
function M.request(buf, method, params, cb)
105-
local async = Async.running()
106-
local cancel = {} ---@type fun()[]
135+
function R:request(buf, method, params, cb)
136+
local clients = type(buf) == "number" and M.get_clients(buf, method) or {
137+
wrap(buf --[[@as vim.lsp.Client]]),
138+
}
139+
if vim.tbl_isempty(clients) then
140+
return self.async:resume()
141+
end
107142

108-
async:on(
109-
"abort",
110-
vim.schedule_wrap(function()
111-
vim.tbl_map(pcall, cancel)
112-
cancel = {}
113-
end)
114-
)
115143
vim.schedule(function()
116-
local clients = M.get_clients(buf, method)
117-
if vim.tbl_isempty(clients) then
118-
return async:resume()
119-
end
120-
local remaining = #clients
121144
for _, client in ipairs(clients) do
122145
local p = params(client)
123146
local status, request_id = client:request(method, p, function(_, result)
124147
if result then
125148
cb(client, result, p)
126149
end
127-
remaining = remaining - 1
128-
if remaining == 0 then
129-
async:resume()
130-
end
150+
self.completed = self.completed + 1
151+
self.async:resume()
131152
end)
132153
if status and request_id then
133-
table.insert(cancel, function()
134-
client:cancel_request(request_id)
135-
end)
154+
table.insert(self.requests, { client_id = client.id, request_id = request_id })
136155
end
137156
end
157+
self.async:resume()
138158
end)
159+
self.async:suspend()
160+
return self
161+
end
162+
163+
function R:wait()
164+
while self.completed < #self.requests do
165+
self.async:suspend()
166+
end
167+
end
139168

140-
async:suspend()
141-
cancel = {}
142-
async = Async.nop()
169+
---@param buf number
170+
---@param method string
171+
---@param params fun(client:vim.lsp.Client):table
172+
---@param cb fun(client:vim.lsp.Client, result:table, params:table)
173+
---@async
174+
function M.request(buf, method, params, cb)
175+
R.new():request(buf, method, params, cb):wait()
143176
end
144177

145178
-- Support for older versions of neovim
@@ -258,7 +291,7 @@ function M.results_to_items(client, results, opts)
258291
detail = result.detail,
259292
name = result.name,
260293
text = "",
261-
range = result.range,
294+
range = result.range or result.selectionRange,
262295
item = result,
263296
}
264297
local uri = result.location and result.location.uri or result.uri or opts.default_uri
@@ -389,6 +422,51 @@ function M.symbols(opts, ctx)
389422
end
390423
end
391424

425+
---@param opts snacks.picker.lsp.Config
426+
---@param filter snacks.picker.Filter
427+
---@param incoming? boolean
428+
function M.call_hierarchy(opts, filter, incoming)
429+
local method = ("callHierarchy/%sCalls"):format(incoming and "incoming" or "outgoing")
430+
local buf = filter.current_buf
431+
local win = filter.current_win
432+
433+
---@async
434+
---@param cb async fun(item: snacks.picker.finder.Item)
435+
return function(cb)
436+
local requester = R.new()
437+
requester:request(buf, "textDocument/prepareCallHierarchy", function(client)
438+
return vim.lsp.util.make_position_params(win, client.offset_encoding)
439+
end, function(client, result)
440+
---@cast result lsp.CallHierarchyItem[]
441+
for _, res in ipairs(result or {}) do
442+
requester:request(client, method, function()
443+
return { item = res }
444+
end, function(_, calls)
445+
---@cast calls (lsp.CallHierarchyIncomingCall|lsp.CallHierarchyOutgoingCall)[]
446+
447+
local call_items = {} ---@type lsp.CallHierarchyItem[]
448+
---@param call lsp.CallHierarchyIncomingCall|lsp.CallHierarchyOutgoingCall
449+
for _, call in ipairs(calls) do
450+
if incoming then
451+
for _, range in ipairs(call.fromRanges or {}) do
452+
local from = vim.deepcopy(call.from)
453+
from.selectionRange = range or from.selectionRange
454+
table.insert(call_items, from)
455+
end
456+
else
457+
table.insert(call_items, call.to)
458+
end
459+
end
460+
461+
local items = M.results_to_items(client, call_items, { default_uri = res.uri })
462+
vim.tbl_map(cb, items)
463+
end)
464+
end
465+
end)
466+
requester:wait()
467+
end
468+
end
469+
392470
---@param opts snacks.picker.lsp.references.Config
393471
---@type snacks.picker.finder
394472
function M.references(opts, ctx)
@@ -402,6 +480,18 @@ function M.references(opts, ctx)
402480
)
403481
end
404482

483+
---@param opts snacks.picker.lsp.Config
484+
---@type snacks.picker.finder
485+
function M.incoming_calls(opts, ctx)
486+
return M.call_hierarchy(opts, ctx.filter, true)
487+
end
488+
489+
---@param opts snacks.picker.lsp.Config
490+
---@type snacks.picker.finder
491+
function M.outgoing_calls(opts, ctx)
492+
return M.call_hierarchy(opts, ctx.filter, false)
493+
end
494+
405495
---@param opts snacks.picker.lsp.Config
406496
---@type snacks.picker.finder
407497
function M.definitions(opts, ctx)

lua/snacks/picker/types.lua

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
---@field lsp_declarations fun(opts?: snacks.picker.lsp.Config|{}): snacks.Picker
3636
---@field lsp_definitions fun(opts?: snacks.picker.lsp.Config|{}): snacks.Picker
3737
---@field lsp_implementations fun(opts?: snacks.picker.lsp.Config|{}): snacks.Picker
38+
---@field lsp_incoming_calls fun(opts?: snacks.picker.lsp.Config|{}): snacks.Picker
39+
---@field lsp_outgoing_calls fun(opts?: snacks.picker.lsp.Config|{}): snacks.Picker
3840
---@field lsp_references fun(opts?: snacks.picker.lsp.references.Config|{}): snacks.Picker
3941
---@field lsp_symbols fun(opts?: snacks.picker.lsp.symbols.Config|{}): snacks.Picker
4042
---@field lsp_type_definitions fun(opts?: snacks.picker.lsp.Config|{}): snacks.Picker

0 commit comments

Comments
 (0)