mirror of
https://github.com/neovim/neovim.git
synced 2026-03-28 03:12:00 +00:00
Problem The format of LSP log messages is inconsistent; some include underscores, while others are not logged at all. Solution Standardize log recording and unify the log message prefixes with the module names.
436 lines
12 KiB
Lua
436 lines
12 KiB
Lua
local util = require('vim.lsp.util')
|
|
local log = require('vim.lsp.log')
|
|
local api = vim.api
|
|
local M = {}
|
|
|
|
---@class (private) vim.lsp.inlay_hint.globalstate Global state for inlay hints
|
|
---@field enabled boolean Whether inlay hints are enabled for this scope
|
|
---@type vim.lsp.inlay_hint.globalstate
|
|
local globalstate = {
|
|
enabled = false,
|
|
}
|
|
|
|
---@class (private) vim.lsp.inlay_hint.bufstate: vim.lsp.inlay_hint.globalstate Buffer local state for inlay hints
|
|
---@field version? integer
|
|
---@field client_hints? table<integer, table<integer, lsp.InlayHint[]>> client_id -> (lnum -> hints)
|
|
---@field applied table<integer, integer> Last version of hints applied to this line
|
|
|
|
---@type table<integer, vim.lsp.inlay_hint.bufstate>
|
|
local bufstates = vim.defaulttable(function(_)
|
|
return setmetatable({ applied = {} }, {
|
|
__index = globalstate,
|
|
__newindex = function(state, key, value)
|
|
if globalstate[key] == value then
|
|
rawset(state, key, nil)
|
|
else
|
|
rawset(state, key, value)
|
|
end
|
|
end,
|
|
})
|
|
end)
|
|
|
|
local namespace = api.nvim_create_namespace('nvim.lsp.inlayhint')
|
|
local augroup = api.nvim_create_augroup('nvim.lsp.inlayhint', {})
|
|
|
|
--- |lsp-handler| for the method `textDocument/inlayHint`
|
|
--- Store hints for a specific buffer and client
|
|
---@param result lsp.InlayHint[]?
|
|
---@param ctx lsp.HandlerContext
|
|
---@private
|
|
function M.on_inlayhint(err, result, ctx)
|
|
if err then
|
|
log.error('inlay_hint', err)
|
|
return
|
|
end
|
|
local bufnr = assert(ctx.bufnr)
|
|
|
|
if
|
|
util.buf_versions[bufnr] ~= ctx.version
|
|
or not api.nvim_buf_is_loaded(bufnr)
|
|
or not bufstates[bufnr].enabled
|
|
then
|
|
return
|
|
end
|
|
local client_id = ctx.client_id
|
|
local bufstate = bufstates[bufnr]
|
|
if not (bufstate.client_hints and bufstate.version) then
|
|
bufstate.client_hints = vim.defaulttable()
|
|
bufstate.version = ctx.version
|
|
end
|
|
local client_hints = bufstate.client_hints
|
|
local client = assert(vim.lsp.get_client_by_id(client_id))
|
|
|
|
-- If there's no error but the result is nil, clear existing hints.
|
|
result = result or {}
|
|
|
|
local new_lnum_hints = vim.defaulttable()
|
|
local num_unprocessed = #result
|
|
if num_unprocessed == 0 then
|
|
client_hints[client_id] = {}
|
|
bufstate.version = ctx.version
|
|
api.nvim__redraw({ buf = bufnr, valid = true, flush = false })
|
|
return
|
|
end
|
|
|
|
local lines = api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
|
|
|
for _, hint in ipairs(result) do
|
|
local lnum = hint.position.line
|
|
local line = lines and lines[lnum + 1] or ''
|
|
hint.position.character =
|
|
vim.str_byteindex(line, client.offset_encoding, hint.position.character, false)
|
|
table.insert(new_lnum_hints[lnum], hint)
|
|
end
|
|
|
|
client_hints[client_id] = new_lnum_hints
|
|
bufstate.version = ctx.version
|
|
api.nvim__redraw({ buf = bufnr, valid = true, flush = false })
|
|
end
|
|
|
|
--- Refresh inlay hints, only if we have attached clients that support it
|
|
---@param bufnr (integer) Buffer handle, or 0 for current
|
|
---@param client_id? (integer) Client ID, or nil for all
|
|
local function refresh(bufnr, client_id)
|
|
for _, client in
|
|
ipairs(vim.lsp.get_clients({
|
|
bufnr = bufnr,
|
|
id = client_id,
|
|
method = 'textDocument/inlayHint',
|
|
}))
|
|
do
|
|
client:request('textDocument/inlayHint', {
|
|
textDocument = util.make_text_document_params(bufnr),
|
|
range = util._make_line_range_params(
|
|
bufnr,
|
|
0,
|
|
api.nvim_buf_line_count(bufnr) - 1,
|
|
client.offset_encoding
|
|
),
|
|
}, nil, bufnr)
|
|
end
|
|
end
|
|
|
|
--- |lsp-handler| for the method `workspace/inlayHint/refresh`
|
|
---@param ctx lsp.HandlerContext
|
|
---@private
|
|
function M.on_refresh(err, _, ctx)
|
|
if err then
|
|
return vim.NIL
|
|
end
|
|
for bufnr in pairs(vim.lsp.get_client_by_id(ctx.client_id).attached_buffers or {}) do
|
|
for _, winid in ipairs(api.nvim_list_wins()) do
|
|
if api.nvim_win_get_buf(winid) == bufnr then
|
|
if bufstates[bufnr] and bufstates[bufnr].enabled then
|
|
bufstates[bufnr].applied = {}
|
|
refresh(bufnr)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return vim.NIL
|
|
end
|
|
|
|
--- Optional filters |kwargs|:
|
|
--- @class vim.lsp.inlay_hint.get.Filter
|
|
--- @inlinedoc
|
|
--- @field bufnr integer?
|
|
--- @field range lsp.Range?
|
|
|
|
--- @class vim.lsp.inlay_hint.get.ret
|
|
--- @inlinedoc
|
|
--- @field bufnr integer
|
|
--- @field client_id integer
|
|
--- @field inlay_hint lsp.InlayHint
|
|
|
|
--- Get the list of inlay hints, (optionally) restricted by buffer or range.
|
|
---
|
|
--- Example usage:
|
|
---
|
|
--- ```lua
|
|
--- local hint = vim.lsp.inlay_hint.get({ bufnr = 0 })[1] -- 0 for current buffer
|
|
---
|
|
--- local client = vim.lsp.get_client_by_id(hint.client_id)
|
|
--- local resp = client:request_sync('inlayHint/resolve', hint.inlay_hint, 100, 0)
|
|
--- local resolved_hint = assert(resp and resp.result, resp.err)
|
|
--- vim.lsp.util.apply_text_edits(resolved_hint.textEdits, 0, client.encoding)
|
|
---
|
|
--- location = resolved_hint.label[1].location
|
|
--- client:request('textDocument/hover', {
|
|
--- textDocument = { uri = location.uri },
|
|
--- position = location.range.start,
|
|
--- })
|
|
--- ```
|
|
---
|
|
--- @param filter vim.lsp.inlay_hint.get.Filter?
|
|
--- @return vim.lsp.inlay_hint.get.ret[]
|
|
--- @since 12
|
|
function M.get(filter)
|
|
vim.validate('filter', filter, 'table', true)
|
|
filter = filter or {}
|
|
|
|
local bufnr = filter.bufnr
|
|
if not bufnr then
|
|
--- @type vim.lsp.inlay_hint.get.ret[]
|
|
local hints = {}
|
|
--- @param buf integer
|
|
vim.tbl_map(function(buf)
|
|
vim.list_extend(hints, M.get(vim.tbl_extend('keep', { bufnr = buf }, filter)))
|
|
end, api.nvim_list_bufs())
|
|
return hints
|
|
else
|
|
bufnr = vim._resolve_bufnr(bufnr)
|
|
end
|
|
|
|
local bufstate = bufstates[bufnr]
|
|
if not bufstate.client_hints then
|
|
return {}
|
|
end
|
|
|
|
local clients = vim.lsp.get_clients({
|
|
bufnr = bufnr,
|
|
method = 'textDocument/inlayHint',
|
|
})
|
|
if #clients == 0 then
|
|
return {}
|
|
end
|
|
|
|
local range = filter.range
|
|
if not range then
|
|
range = {
|
|
start = { line = 0, character = 0 },
|
|
['end'] = { line = api.nvim_buf_line_count(bufnr), character = 0 },
|
|
}
|
|
end
|
|
|
|
--- @type vim.lsp.inlay_hint.get.ret[]
|
|
local result = {}
|
|
for _, client in pairs(clients) do
|
|
local lnum_hints = bufstate.client_hints[client.id]
|
|
if lnum_hints then
|
|
for lnum = range.start.line, range['end'].line do
|
|
local hints = lnum_hints[lnum] or {}
|
|
for _, hint in pairs(hints) do
|
|
local line, char = hint.position.line, hint.position.character
|
|
if
|
|
(line > range.start.line or char >= range.start.character)
|
|
and (line < range['end'].line or char <= range['end'].character)
|
|
then
|
|
table.insert(result, {
|
|
bufnr = bufnr,
|
|
client_id = client.id,
|
|
inlay_hint = hint,
|
|
})
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return result
|
|
end
|
|
|
|
--- Clear inlay hints
|
|
---@param bufnr (integer) Buffer handle, or 0 for current
|
|
local function clear(bufnr)
|
|
bufnr = vim._resolve_bufnr(bufnr)
|
|
local bufstate = bufstates[bufnr]
|
|
local client_lens = (bufstate or {}).client_hints or {}
|
|
local client_ids = vim.tbl_keys(client_lens) --- @type integer[]
|
|
for _, iter_client_id in ipairs(client_ids) do
|
|
if bufstate then
|
|
bufstate.client_hints[iter_client_id] = {}
|
|
end
|
|
end
|
|
api.nvim_buf_clear_namespace(bufnr, namespace, 0, -1)
|
|
api.nvim__redraw({ buf = bufnr, valid = true, flush = false })
|
|
end
|
|
|
|
--- Disable inlay hints for a buffer
|
|
---@param bufnr (integer) Buffer handle, or 0 for current
|
|
local function _disable(bufnr)
|
|
bufnr = vim._resolve_bufnr(bufnr)
|
|
clear(bufnr)
|
|
bufstates[bufnr] = nil
|
|
bufstates[bufnr].enabled = false
|
|
end
|
|
|
|
--- Enable inlay hints for a buffer
|
|
---@param bufnr (integer) Buffer handle, or 0 for current
|
|
local function _enable(bufnr)
|
|
bufnr = vim._resolve_bufnr(bufnr)
|
|
bufstates[bufnr] = nil
|
|
bufstates[bufnr].enabled = true
|
|
refresh(bufnr)
|
|
end
|
|
|
|
api.nvim_create_autocmd('LspNotify', {
|
|
callback = function(ev)
|
|
---@type integer
|
|
local bufnr = ev.buf
|
|
|
|
if ev.data.method ~= 'textDocument/didChange' and ev.data.method ~= 'textDocument/didOpen' then
|
|
return
|
|
end
|
|
if bufstates[bufnr].enabled then
|
|
refresh(bufnr, ev.data.client_id)
|
|
end
|
|
end,
|
|
group = augroup,
|
|
})
|
|
api.nvim_create_autocmd('LspAttach', {
|
|
callback = function(ev)
|
|
---@type integer
|
|
local bufnr = ev.buf
|
|
|
|
api.nvim_buf_attach(bufnr, false, {
|
|
on_reload = function(_, cb_bufnr)
|
|
clear(cb_bufnr)
|
|
if bufstates[cb_bufnr] and bufstates[cb_bufnr].enabled then
|
|
bufstates[cb_bufnr].applied = {}
|
|
refresh(cb_bufnr)
|
|
end
|
|
end,
|
|
on_detach = function(_, cb_bufnr)
|
|
_disable(cb_bufnr)
|
|
bufstates[cb_bufnr] = nil
|
|
end,
|
|
})
|
|
end,
|
|
group = augroup,
|
|
})
|
|
api.nvim_create_autocmd('LspDetach', {
|
|
callback = function(ev)
|
|
---@type integer
|
|
local bufnr = ev.buf
|
|
local clients = vim.lsp.get_clients({ bufnr = bufnr, method = 'textDocument/inlayHint' })
|
|
|
|
if not vim.iter(clients):any(function(c)
|
|
return c.id ~= ev.data.client_id
|
|
end) then
|
|
_disable(bufnr)
|
|
end
|
|
end,
|
|
group = augroup,
|
|
})
|
|
api.nvim_set_decoration_provider(namespace, {
|
|
on_win = function(_, _, bufnr, topline, botline)
|
|
---@type vim.lsp.inlay_hint.bufstate
|
|
local bufstate = rawget(bufstates, bufnr)
|
|
if not bufstate then
|
|
return
|
|
end
|
|
|
|
if bufstate.version ~= util.buf_versions[bufnr] then
|
|
return
|
|
end
|
|
|
|
if not bufstate.client_hints then
|
|
return
|
|
end
|
|
local client_hints = assert(bufstate.client_hints)
|
|
|
|
for lnum = topline, botline do
|
|
if bufstate.applied[lnum] ~= bufstate.version then
|
|
api.nvim_buf_clear_namespace(bufnr, namespace, lnum, lnum + 1)
|
|
|
|
local hint_virtual_texts = {} --- @type table<integer, [string, string?][]>
|
|
for _, lnum_hints in pairs(client_hints) do
|
|
local hints = lnum_hints[lnum] or {}
|
|
for _, hint in pairs(hints) do
|
|
local text = ''
|
|
local label = hint.label
|
|
if type(label) == 'string' then
|
|
text = label
|
|
else
|
|
for _, part in ipairs(label) do
|
|
text = text .. part.value
|
|
end
|
|
end
|
|
local vt = hint_virtual_texts[hint.position.character] or {}
|
|
if hint.paddingLeft then
|
|
vt[#vt + 1] = { ' ' }
|
|
end
|
|
vt[#vt + 1] = { text, 'LspInlayHint' }
|
|
if hint.paddingRight then
|
|
vt[#vt + 1] = { ' ' }
|
|
end
|
|
hint_virtual_texts[hint.position.character] = vt
|
|
end
|
|
end
|
|
|
|
for pos, vt in pairs(hint_virtual_texts) do
|
|
api.nvim_buf_set_extmark(bufnr, namespace, lnum, pos, {
|
|
virt_text_pos = 'inline',
|
|
ephemeral = false,
|
|
virt_text = vt,
|
|
})
|
|
end
|
|
|
|
bufstate.applied[lnum] = bufstate.version
|
|
end
|
|
end
|
|
end,
|
|
})
|
|
|
|
--- Query whether inlay hint is enabled in the {filter}ed scope
|
|
--- @param filter? vim.lsp.inlay_hint.enable.Filter
|
|
--- @return boolean
|
|
--- @since 12
|
|
function M.is_enabled(filter)
|
|
vim.validate('filter', filter, 'table', true)
|
|
filter = filter or {}
|
|
local bufnr = filter.bufnr
|
|
|
|
if bufnr == nil then
|
|
return globalstate.enabled
|
|
end
|
|
return bufstates[vim._resolve_bufnr(bufnr)].enabled
|
|
end
|
|
|
|
--- Optional filters |kwargs|, or `nil` for all.
|
|
--- @class vim.lsp.inlay_hint.enable.Filter
|
|
--- @inlinedoc
|
|
--- Buffer number, or 0 for current buffer, or nil for all.
|
|
--- @field bufnr integer?
|
|
|
|
--- Enables or disables inlay hints for the {filter}ed scope.
|
|
---
|
|
--- To "toggle", pass the inverse of `is_enabled()`:
|
|
---
|
|
--- ```lua
|
|
--- vim.lsp.inlay_hint.enable(not vim.lsp.inlay_hint.is_enabled())
|
|
--- ```
|
|
---
|
|
--- @param enable (boolean|nil) true/nil to enable, false to disable
|
|
--- @param filter vim.lsp.inlay_hint.enable.Filter?
|
|
--- @since 12
|
|
function M.enable(enable, filter)
|
|
vim.validate('enable', enable, 'boolean', true)
|
|
vim.validate('filter', filter, 'table', true)
|
|
enable = enable == nil or enable
|
|
filter = filter or {}
|
|
|
|
if filter.bufnr == nil then
|
|
globalstate.enabled = enable
|
|
for _, bufnr in ipairs(api.nvim_list_bufs()) do
|
|
if api.nvim_buf_is_loaded(bufnr) then
|
|
if enable == false then
|
|
_disable(bufnr)
|
|
else
|
|
_enable(bufnr)
|
|
end
|
|
else
|
|
bufstates[bufnr] = nil
|
|
end
|
|
end
|
|
else
|
|
if enable == false then
|
|
_disable(filter.bufnr)
|
|
else
|
|
_enable(filter.bufnr)
|
|
end
|
|
end
|
|
end
|
|
|
|
return M
|