Files
nvim/lua/lsp/codelens/row.lua
T

167 lines
4.4 KiB
Lua

local NS = vim.api.nvim_create_namespace("ow.lsp.codelens")
---@alias ow.lsp.codelens.Position "eol" | "right_align" | "inline" | "above"
---@type ow.lsp.codelens.Position
local position = "above"
local separator = " | "
---@class ow.lsp.CodeLens : lsp.CodeLens
---@field command lsp.Command
---@class ow.lsp.codelens.Row
---@field row integer
---@field lenses ow.lsp.CodeLens[]
---@field pending integer
local Row = {}
Row.__index = Row
---@param row integer
---@return ow.lsp.codelens.Row
function Row.new(row)
return setmetatable({ row = row, lenses = {}, pending = 0 }, Row)
end
---@param lens ow.lsp.CodeLens
function Row:add(lens)
table.insert(self.lenses, lens)
end
function Row:expect()
self.pending = self.pending + 1
end
---@param lens? lsp.CodeLens
function Row:resolve(lens)
self.pending = self.pending - 1
if lens and lens.command then
self:add(lens)
end
end
---@return boolean
function Row:is_ready()
return self.pending == 0 and #self.lenses > 0
end
---@param buf integer
function Row:render(buf)
if not self:is_ready() or not vim.api.nvim_buf_is_valid(buf) then
return
end
table.sort(self.lenses, function(a, b)
return a.range.start.character < b.range.start.character
end)
local parts = {}
for i, lens in ipairs(self.lenses) do
table.insert(parts, { lens.command.title, "LspCodeLens" })
if i < #self.lenses then
table.insert(parts, { separator, "LspCodeLensSeparator" })
end
end
---@type vim.api.keyset.set_extmark
local opts
local col
if position == "above" then
local line = vim.api.nvim_buf_get_lines(
buf,
self.row,
self.row + 1,
false
)[1] or ""
local indent = line:match("^%s*") or ""
local chunks = parts
if indent ~= "" then
chunks = { { indent, "LspCodeLensSeparator" } }
vim.list_extend(chunks, parts)
end
col = 0
opts = {
virt_lines = { chunks },
virt_lines_above = true,
hl_mode = "combine",
}
else
local first = assert(self.lenses[1])
col = position == "inline" and first.range.start.character or 0
opts = {
virt_text = parts,
virt_text_pos = position,
hl_mode = "combine",
}
end
-- One extmark per row. Extmarks auto-shift with edits, so multiple from
-- prior refreshes can end up on the same row; pick the first and drop
-- the rest, then reuse its id for an atomic update.
local marks = vim.api.nvim_buf_get_extmarks(
buf,
NS,
{ self.row, 0 },
{ self.row, -1 },
{ details = true }
)
---@type {
--- id: integer,
--- col: integer,
--- details: vim.api.keyset.extmark_details?
---}
local primary
for i, mark in ipairs(marks) do
if i == 1 then
primary = { id = mark[1], col = mark[3], details = mark[4] }
else
vim.api.nvim_buf_del_extmark(buf, NS, mark[1])
end
end
if primary and primary.col == col and primary.details then
local d = primary.details
local same = opts.virt_lines
and vim.deep_equal(d.virt_lines, opts.virt_lines)
and d.virt_lines_above == opts.virt_lines_above
or not opts.virt_lines
and d.virt_text_pos == opts.virt_text_pos
and vim.deep_equal(d.virt_text, opts.virt_text)
if same then
return
end
end
opts.id = primary and primary.id or nil
vim.api.nvim_buf_set_extmark(buf, NS, self.row, col, opts)
end
---@param buf integer
function Row.clear_all(buf)
vim.api.nvim_buf_clear_namespace(buf, NS, 0, -1)
end
---@param buf integer
---@param rows table<integer, ow.lsp.codelens.Row>
function Row.prune(buf, rows)
for _, mark in ipairs(vim.api.nvim_buf_get_extmarks(buf, NS, 0, -1, {})) do
if not rows[mark[2]] then
vim.api.nvim_buf_del_extmark(buf, NS, mark[1])
end
end
end
---@class ow.lsp.codelens.row.ConfigureOpts
---@field position? ow.lsp.codelens.Position
---@field separator? string
---@param opts ow.lsp.codelens.row.ConfigureOpts
function Row.configure(opts)
if opts.position ~= nil then
position = opts.position
end
if opts.separator ~= nil then
separator = opts.separator
end
end
return Row