diff options
author | Chinmay Dalal <dalal.chinmay.0101@gmail.com> | 2023-06-11 15:23:37 +0530 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-06-11 11:53:37 +0200 |
commit | 643546b82b4bc0c29ca869f81af868a019723d83 (patch) | |
tree | dcabb24372fa2d5157cce4b9b8eea2b0c9927735 /runtime/lua/vim | |
parent | cce9460524aa17bcd4daa095f4706220b81f8845 (diff) | |
download | rneovim-643546b82b4bc0c29ca869f81af868a019723d83.tar.gz rneovim-643546b82b4bc0c29ca869f81af868a019723d83.tar.bz2 rneovim-643546b82b4bc0c29ca869f81af868a019723d83.zip |
feat(lsp): add handlers for inlay hints (#23736)
initial support; public API left for a follow-up PR
Diffstat (limited to 'runtime/lua/vim')
-rw-r--r-- | runtime/lua/vim/lsp.lua | 17 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/_inlay_hint.lua | 217 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/handlers.lua | 26 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/protocol.lua | 9 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/types.lua | 25 |
5 files changed, 289 insertions, 5 deletions
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 1d9a91801a..6f9a6c460b 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -17,6 +17,7 @@ local if_nil = vim.F.if_nil local lsp = { protocol = protocol, + _inlay_hint = require('vim.lsp._inlay_hint'), handlers = default_handlers, @@ -60,6 +61,8 @@ lsp._request_name_to_capability = { ['textDocument/documentHighlight'] = { 'documentHighlightProvider' }, ['textDocument/semanticTokens/full'] = { 'semanticTokensProvider' }, ['textDocument/semanticTokens/full/delta'] = { 'semanticTokensProvider' }, + ['textDocument/inlayHint'] = { 'inlayHintProvider' }, + ['inlayHint/resolve'] = { 'inlayHintProvider', 'resolveProvider' }, } -- TODO improve handling of scratch buffers with LSP attached. @@ -1498,16 +1501,20 @@ function lsp.start_client(config) end -- Ensure pending didChange notifications are sent so that the server doesn't operate on a stale state changetracking.flush(client, bufnr) + local version = util.buf_versions[bufnr] bufnr = resolve_bufnr(bufnr) if log.debug() then log.debug(log_prefix, 'client.request', client_id, method, params, handler, bufnr) end local success, request_id = rpc.request(method, params, function(err, result) - handler( - err, - result, - { method = method, client_id = client_id, bufnr = bufnr, params = params } - ) + local context = { + method = method, + client_id = client_id, + bufnr = bufnr, + params = params, + version = version, + } + handler(err, result, context) end, function(request_id) local request = client.requests[request_id] request.type = 'complete' diff --git a/runtime/lua/vim/lsp/_inlay_hint.lua b/runtime/lua/vim/lsp/_inlay_hint.lua new file mode 100644 index 0000000000..aa6ec9aca8 --- /dev/null +++ b/runtime/lua/vim/lsp/_inlay_hint.lua @@ -0,0 +1,217 @@ +local util = require('vim.lsp.util') +local log = require('vim.lsp.log') +local api = vim.api +local M = {} + +---@class lsp._inlay_hint.bufstate +---@field version integer +---@field client_hint table<integer, table<integer, lsp.InlayHint[]>> client_id -> (lnum -> hints) + +---@type table<integer, lsp._inlay_hint.bufstate> +local hint_cache_by_buf = setmetatable({}, { + __index = function(t, b) + local key = b > 0 and b or api.nvim_get_current_buf() + return rawget(t, key) + end, +}) + +local namespace = api.nvim_create_namespace('vim_lsp_inlayhint') + +M.__explicit_buffers = {} + +--- |lsp-handler| for the method `textDocument/inlayHint` +--- Store hints for a specific buffer and client +--- Resolves unresolved hints +---@private +function M.on_inlayhint(err, result, ctx, _) + if err then + local _ = log.error() and log.error('inlayhint', err) + return + end + local bufnr = ctx.bufnr + if util.buf_versions[bufnr] ~= ctx.version then + return + end + local client_id = ctx.client_id + if not result then + return + end + local bufstate = hint_cache_by_buf[bufnr] + if not bufstate then + bufstate = { + client_hint = vim.defaulttable(), + version = ctx.version, + } + hint_cache_by_buf[bufnr] = bufstate + api.nvim_buf_attach(bufnr, false, { + on_detach = function(_, b) + api.nvim_buf_clear_namespace(b, namespace, 0, -1) + hint_cache_by_buf[b] = nil + end, + on_reload = function(_, b) + api.nvim_buf_clear_namespace(b, namespace, 0, -1) + hint_cache_by_buf[b] = nil + end, + }) + end + local hints_by_client = bufstate.client_hint + local client = vim.lsp.get_client_by_id(client_id) + + local new_hints_by_lnum = vim.defaulttable() + local num_unprocessed = #result + if num_unprocessed == 0 then + hints_by_client[client_id] = {} + bufstate.version = ctx.version + api.nvim__buf_redraw_range(bufnr, 0, -1) + return + end + + local lines = api.nvim_buf_get_lines(bufnr, 0, -1, false) + ---@private + local function pos_to_byte(position) + local col = position.character + if col > 0 then + local line = lines[position.line + 1] or '' + local ok, convert_result + ok, convert_result = pcall(util._str_byteindex_enc, line, col, client.offset_encoding) + if ok then + return convert_result + end + return math.min(#line, col) + end + return col + end + + for _, hint in ipairs(result) do + local lnum = hint.position.line + hint.position.character = pos_to_byte(hint.position) + table.insert(new_hints_by_lnum[lnum], hint) + end + + hints_by_client[client_id] = new_hints_by_lnum + bufstate.version = ctx.version + api.nvim__buf_redraw_range(bufnr, 0, -1) +end + +---@private +local function resolve_bufnr(bufnr) + return bufnr == 0 and api.nvim_get_current_buf() or bufnr +end + +--- Refresh inlay hints for a buffer +--- +--- It is recommended to trigger this using an autocmd or via keymap. +---@param opts (nil|table) Optional arguments +--- - bufnr (integer, default: 0): Buffer whose hints to refresh +--- - only_visible (boolean, default: false): Whether to only refresh hints for the visible regions of the buffer +--- +--- Example: +--- <pre>vim +--- autocmd BufEnter,InsertLeave,BufWritePost <buffer> lua vim.lsp._inlay_hint.refresh() +--- </pre> +--- +---@private +function M.refresh(opts) + opts = opts or {} + local bufnr = opts.bufnr or 0 + local only_visible = opts.only_visible or false + bufnr = resolve_bufnr(bufnr) + M.__explicit_buffers[bufnr] = true + local buffer_windows = {} + for _, winid in ipairs(api.nvim_list_wins()) do + if api.nvim_win_get_buf(winid) == bufnr then + table.insert(buffer_windows, winid) + end + end + for _, window in ipairs(buffer_windows) do + local first = vim.fn.line('w0', window) + local last = vim.fn.line('w$', window) + local params = { + textDocument = util.make_text_document_params(bufnr), + range = { + start = { line = first - 1, character = 0 }, + ['end'] = { line = last, character = 0 }, + }, + } + vim.lsp.buf_request(bufnr, 'textDocument/inlayHint', params) + end + if not only_visible then + local params = { + textDocument = util.make_text_document_params(bufnr), + range = { + start = { line = 0, character = 0 }, + ['end'] = { line = api.nvim_buf_line_count(bufnr), character = 0 }, + }, + } + vim.lsp.buf_request(bufnr, 'textDocument/inlayHint', params) + end +end + +--- Clear inlay hints +--- +---@param client_id integer|nil filter by client_id. All clients if nil +---@param bufnr integer|nil filter by buffer. All buffers if nil +---@private +function M.clear(client_id, bufnr) + local buffers = bufnr and { resolve_bufnr(bufnr) } or vim.tbl_keys(hint_cache_by_buf) + for _, iter_bufnr in ipairs(buffers) do + M.__explicit_buffers[iter_bufnr] = false + local bufstate = hint_cache_by_buf[iter_bufnr] + local client_lens = (bufstate or {}).client_hint or {} + local client_ids = client_id and { client_id } or vim.tbl_keys(client_lens) + for _, iter_client_id in ipairs(client_ids) do + if bufstate then + bufstate.client_hint[iter_client_id] = {} + end + end + api.nvim_buf_clear_namespace(iter_bufnr, namespace, 0, -1) + end + vim.cmd('redraw!') +end + +api.nvim_set_decoration_provider(namespace, { + on_win = function(_, _, bufnr, topline, botline) + local bufstate = hint_cache_by_buf[bufnr] + if not bufstate then + return + end + + if bufstate.version ~= util.buf_versions[bufnr] then + return + end + local hints_by_client = bufstate.client_hint + api.nvim_buf_clear_namespace(bufnr, namespace, 0, -1) + + for lnum = topline, botline do + for _, hints_by_lnum in pairs(hints_by_client) do + local line_hints = hints_by_lnum[lnum] or {} + for _, hint in pairs(line_hints) do + local text = '' + if type(hint.label) == 'string' then + text = hint.label + else + for _, part in ipairs(hint.label) do + text = text .. part.value + end + end + if hint.paddingLeft then + text = ' ' .. text + end + if hint.paddingRight then + text = text .. ' ' + end + api.nvim_buf_set_extmark(bufnr, namespace, lnum, hint.position.character, { + virt_text_pos = 'inline', + ephemeral = false, + virt_text = { + { text, 'LspInlayHint' }, + }, + hl_mode = 'combine', + }) + end + end + end + end, +}) + +return M diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua index 19338ae8f0..44a9a58aca 100644 --- a/runtime/lua/vim/lsp/handlers.lua +++ b/runtime/lua/vim/lsp/handlers.lua @@ -219,6 +219,10 @@ M['textDocument/codeLens'] = function(...) return require('vim.lsp.codelens').on_codelens(...) end +M['textDocument/inlayHint'] = function(...) + return require('vim.lsp._inlay_hint').on_inlayhint(...) +end + --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references M['textDocument/references'] = function(_, result, ctx, config) if not result or vim.tbl_isempty(result) then @@ -612,6 +616,28 @@ M['window/showDocument'] = function(_, result, ctx, _) return { success = success or false } end +---@see https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_inlayHint_refresh +M['workspace/inlayHint/refresh'] = function(err, _, ctx) + local inlay_hint = require('vim.lsp._inlay_hint') + if not inlay_hint.__explicit_buffers[ctx.bufnr] then + return vim.NIL + end + if err then + return vim.NIL + end + + for _, bufnr in ipairs(vim.lsp.get_buffers_by_client_id(ctx.client_id)) do + for _, winid in ipairs(api.nvim_list_wins()) do + if api.nvim_win_get_buf(winid) == bufnr then + inlay_hint.refresh({ bufnr = bufnr }) + break + end + end + end + + return vim.NIL +end + -- Add boilerplate error validation and logging for all of these. for k, fn in pairs(M) do M[k] = function(err, result, ctx, config) diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index 172d43e483..b3a7903420 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -641,6 +641,12 @@ function protocol.make_client_capabilities() }, }, textDocument = { + inlayHint = { + dynamicRegistration = false, + resolveSupport = { + properties = {}, + }, + }, semanticTokens = { dynamicRegistration = false, tokenTypes = { @@ -853,6 +859,9 @@ function protocol.make_client_capabilities() dynamicRegistration = true, relativePatternSupport = true, }, + inlayHint = { + refreshSupport = true, + }, }, experimental = nil, window = { diff --git a/runtime/lua/vim/lsp/types.lua b/runtime/lua/vim/lsp/types.lua index ef85a0d10f..108aeeb922 100644 --- a/runtime/lua/vim/lsp/types.lua +++ b/runtime/lua/vim/lsp/types.lua @@ -69,3 +69,28 @@ --- @field method string --- @alias lsp.UnregistrationParams {unregisterations: lsp.Unregistration[]} + +---@class lsp.Location +---@field uri string +---@field range lsp.Range + +---@class lsp.MarkupContent +---@field kind string +---@field value string + +---@class lsp.InlayHintLabelPart +---@field value string +---@field tooltip? string | lsp.MarkupContent +---@field location? lsp.Location + +---@class lsp.TextEdit +---@field range lsp.Range +---@field newText string + +---@class lsp.InlayHint +---@field position lsp.Position +---@field label string | lsp.InlayHintLabelPart[] +---@field kind? integer +---@field textEdits? lsp.TextEdit[] +---@field paddingLeft? boolean +---@field paddingRight? boolean |