fix(picker.lsp): lazy resolve item lsp locations. Fixes #650

This commit is contained in:
Folke Lemaitre 2025-01-20 09:56:10 +01:00
parent f7fddf8dfb
commit d0a0046e37
No known key found for this signature in database
GPG key ID: 41F8B1FBACAE2040
5 changed files with 101 additions and 39 deletions

View file

@ -6,6 +6,7 @@ local M = {}
---@alias snacks.picker.format fun(item:snacks.picker.Item, picker:snacks.Picker):snacks.picker.Highlight[]
---@alias snacks.picker.preview fun(ctx: snacks.picker.preview.ctx):boolean?
---@alias snacks.picker.sort fun(a:snacks.picker.Item, b:snacks.picker.Item):boolean
---@alias snacks.picker.Pos {[1]:number, [2]:number}
--- Generic filter used by finders to pre-filter items
---@class snacks.picker.filter.Config
@ -29,9 +30,11 @@ local M = {}
---@field score_add? number
---@field score_mul? number
---@field match_tick? number
---@field file? string
---@field text string
---@field pos? {[1]:number, [2]:number}
---@field end_pos? {[1]:number, [2]:number}
---@field pos? snacks.picker.Pos
---@field loc? snacks.picker.lsp.Loc
---@field end_pos? snacks.picker.Pos
---@field highlights? snacks.picker.Highlight[][]
---@field preview? snacks.picker.Item.preview
---@field resolve? fun(item:snacks.picker.Item)

View file

@ -334,6 +334,16 @@ function M:show()
end
end
---@param item snacks.picker.Item?
function M:resolve(item)
if not item then
return
end
Snacks.picker.util.resolve(item)
Snacks.picker.util.resolve_loc(item)
return item
end
--- Returns an iterator over the filtered items in the picker.
--- Items will be in sorted order.
---@return fun():snacks.picker.Item?
@ -343,7 +353,7 @@ function M:iter()
return function()
i = i + 1
if i <= n then
return self.list:get(i)
return self:resolve(self.list:get(i))
end
end
end
@ -358,20 +368,29 @@ function M:items()
end
--- Get the current item at the cursor
function M:current()
return self.list:current()
---@param opts? {resolve?: boolean} default is `true`
function M:current(opts)
opts = opts or {}
local ret = self.list:current()
if ret and opts.resolve ~= false then
ret = self:resolve(ret)
end
return ret
end
--- Get the selected items.
--- If `fallback=true` and there is no selection, return the current item.
---@param opts? {fallback?: boolean} default is `false`
---@return snacks.picker.Item[]
function M:selected(opts)
opts = opts or {}
local ret = vim.deepcopy(self.list.selected)
if #ret == 0 and opts.fallback then
return { self:current() }
ret = { self:current() }
end
return ret
return vim.tbl_map(function(item)
return self:resolve(item)
end, ret)
end
--- Total number of items in the picker

View file

@ -93,7 +93,7 @@ end
---@param picker snacks.Picker
function M:show(picker)
local item, prev = picker:current(), self.item
local item, prev = picker:current({ resolve = false }), self.item
if self.item == item then
return
end
@ -210,7 +210,10 @@ function M:loc()
if not self.item then
return
end
local line_count = vim.api.nvim_buf_line_count(self.win.buf)
Snacks.picker.util.resolve_loc(self.item, self.win.buf)
if self.item.pos and self.item.pos[1] > 0 and self.item.pos[1] <= line_count then
vim.api.nvim_win_set_cursor(self.win.win, { self.item.pos[1], 0 })
vim.api.nvim_win_call(self.win.win, function()

View file

@ -15,6 +15,10 @@ local islist = vim.islist or vim.tbl_islist
---@alias lsp.Symbol lsp.SymbolInformation|lsp.DocumentSymbol
---@alias lsp.Loc lsp.Location|lsp.LocationLink
---@class snacks.picker.lsp.Loc: lsp.Location
---@field encoding string
---@field resolved? boolean
local kinds = nil ---@type table<lsp.SymbolKind, string>
--- Gets the original symbol kind name from its number.
@ -66,6 +70,23 @@ local function wrap(client)
})
end
---@param item snacks.picker.finder.Item
---@param result lsp.Loc
---@param client vim.lsp.Client
function M.add_loc(item, result, client)
---@type snacks.picker.lsp.Loc
local loc = {
uri = result.uri or result.targetUri,
range = result.range or result.targetSelectionRange,
encoding = client.offset_encoding,
}
item.loc = loc
item.pos = { loc.range.start.line + 1, loc.range.start.character }
item.end_pos = { loc.range["end"].line + 1, loc.range["end"].character }
item.file = vim.uri_to_fname(loc.uri)
return item
end
---@param buf number
---@param method string
---@return vim.lsp.Client[]
@ -212,7 +233,6 @@ function M.results_to_items(client, results, opts)
opts = opts or {}
local items = {} ---@type snacks.picker.finder.Item[]
local locs = {} ---@type lsp.Loc[]
local processed = {} ---@type table<lsp.ResultItem, {uri:string, loc:lsp.Loc, range?:lsp.Loc}>
---@param result lsp.ResultItem
local function process(result)
@ -222,7 +242,6 @@ function M.results_to_items(client, results, opts)
if not loc.uri then
assert(loc.uri, "missing uri in result:\n" .. vim.inspect(result))
end
processed[result] = { uri = uri, loc = loc }
if not opts.filter or opts.filter(result) then
locs[#locs + 1] = loc
end
@ -235,43 +254,35 @@ function M.results_to_items(client, results, opts)
process(result)
end
local loc_items = vim.lsp.util.locations_to_items(locs, client.offset_encoding)
M.fix_locs(loc_items)
local ranges = {} ---@type table<lsp.Loc, vim.quickfix.entry>
for _, i in ipairs(loc_items) do
local loc = i.user_data ---@type lsp.Loc
ranges[loc] = i
end
local last = {} ---@type table<snacks.picker.finder.Item, snacks.picker.finder.Item>
---@param result lsp.ResultItem
---@param parent snacks.picker.finder.Item
local function add(result, parent)
local loc = processed[result].loc
local sym = ranges[loc]
---@type snacks.picker.finder.Item?
local item
if sym then
local text = table.concat({ M.symbol_kind(result.kind), result.name, result.detail or "" }, " ")
if opts.text_with_file and sym.filename then
text = text .. " " .. sym.filename
end
item = {
---@type snacks.picker.finder.Item
local item = {
kind = M.symbol_kind(result.kind),
parent = parent,
depth = (parent.depth or 0) + 1,
detail = result.detail,
name = result.name,
text = text,
file = sym.filename,
buf = sym.bufnr,
pos = { sym.lnum, sym.col - 1 },
end_pos = sym.end_lnum and sym.end_col and { sym.end_lnum, sym.end_col - 1 } or nil,
text = "",
}
local uri = result.location and result.location.uri or result.uri or opts.default_uri
local loc = result.location or { range = result.selectionRange or result.range, uri = uri }
loc.uri = loc.uri or uri
M.add_loc(item, loc, client)
local text = table.concat({ M.symbol_kind(result.kind), result.name, result.detail or "" }, " ")
if opts.text_with_file and item.file then
text = text .. " " .. item.file
end
item.text = text
if not opts.filter or opts.filter(result) then
items[#items + 1] = item
last[parent] = item
parent = item
end
for _, child in ipairs(result.children or {}) do
add(child, parent)
end

View file

@ -181,6 +181,7 @@ function M.parse(str)
return t, args
end
--- Resolves the item if it has a resolve function
---@param item snacks.picker.Item
function M.resolve(item)
if item and item.resolve then
@ -190,6 +191,31 @@ function M.resolve(item)
return item
end
--- Resolves the location of an item to byte positions
---@param item snacks.picker.Item
---@param buf? number
function M.resolve_loc(item, buf)
if not item or not item.loc or item.loc.resolved then
return item
end
local lines = {} ---@type string[]
if buf and vim.api.nvim_buf_is_valid(buf) then
lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
else
lines = vim.fn.readfile(item.file)
end
---@param pos lsp.Position?
local function resolve(pos)
return pos and { pos.line + 1, vim.str_byteindex(lines[pos.line + 1], item.loc.encoding, pos.character) } or nil
end
item.pos = resolve(item.loc.range["start"])
item.end_pos = resolve(item.loc.range["end"]) or item.end_pos
item.loc.resolved = true
return item
end
--- Returns the relative time from a given time
--- as ... ago
---@param time number in seconds