inlay_hint.lua (12503B)
1 local util = require('vim.lsp.util') 2 local log = require('vim.lsp.log') 3 local api = vim.api 4 local M = {} 5 6 ---@class (private) vim.lsp.inlay_hint.globalstate Global state for inlay hints 7 ---@field enabled boolean Whether inlay hints are enabled for this scope 8 ---@type vim.lsp.inlay_hint.globalstate 9 local globalstate = { 10 enabled = false, 11 } 12 13 ---@class (private) vim.lsp.inlay_hint.bufstate: vim.lsp.inlay_hint.globalstate Buffer local state for inlay hints 14 ---@field version? integer 15 ---@field client_hints? table<integer, table<integer, lsp.InlayHint[]>> client_id -> (lnum -> hints) 16 ---@field applied table<integer, integer> Last version of hints applied to this line 17 18 ---@type table<integer, vim.lsp.inlay_hint.bufstate> 19 local bufstates = vim.defaulttable(function(_) 20 return setmetatable({ applied = {} }, { 21 __index = globalstate, 22 __newindex = function(state, key, value) 23 if globalstate[key] == value then 24 rawset(state, key, nil) 25 else 26 rawset(state, key, value) 27 end 28 end, 29 }) 30 end) 31 32 local namespace = api.nvim_create_namespace('nvim.lsp.inlayhint') 33 local augroup = api.nvim_create_augroup('nvim.lsp.inlayhint', {}) 34 35 --- |lsp-handler| for the method `textDocument/inlayHint` 36 --- Store hints for a specific buffer and client 37 ---@param result lsp.InlayHint[]? 38 ---@param ctx lsp.HandlerContext 39 ---@private 40 function M.on_inlayhint(err, result, ctx) 41 if err then 42 log.error('inlayhint', err) 43 return 44 end 45 local bufnr = assert(ctx.bufnr) 46 47 if 48 util.buf_versions[bufnr] ~= ctx.version 49 or not api.nvim_buf_is_loaded(bufnr) 50 or not bufstates[bufnr].enabled 51 then 52 return 53 end 54 local client_id = ctx.client_id 55 local bufstate = bufstates[bufnr] 56 if not (bufstate.client_hints and bufstate.version) then 57 bufstate.client_hints = vim.defaulttable() 58 bufstate.version = ctx.version 59 end 60 local client_hints = bufstate.client_hints 61 local client = assert(vim.lsp.get_client_by_id(client_id)) 62 63 -- If there's no error but the result is nil, clear existing hints. 64 result = result or {} 65 66 local new_lnum_hints = vim.defaulttable() 67 local num_unprocessed = #result 68 if num_unprocessed == 0 then 69 client_hints[client_id] = {} 70 bufstate.version = ctx.version 71 api.nvim__redraw({ buf = bufnr, valid = true, flush = false }) 72 return 73 end 74 75 local lines = api.nvim_buf_get_lines(bufnr, 0, -1, false) 76 77 for _, hint in ipairs(result) do 78 local lnum = hint.position.line 79 local line = lines and lines[lnum + 1] or '' 80 hint.position.character = 81 vim.str_byteindex(line, client.offset_encoding, hint.position.character, false) 82 table.insert(new_lnum_hints[lnum], hint) 83 end 84 85 client_hints[client_id] = new_lnum_hints 86 bufstate.version = ctx.version 87 api.nvim__redraw({ buf = bufnr, valid = true, flush = false }) 88 end 89 90 --- Refresh inlay hints, only if we have attached clients that support it 91 ---@param bufnr (integer) Buffer handle, or 0 for current 92 ---@param client_id? (integer) Client ID, or nil for all 93 local function refresh(bufnr, client_id) 94 for _, client in 95 ipairs(vim.lsp.get_clients({ 96 bufnr = bufnr, 97 id = client_id, 98 method = 'textDocument/inlayHint', 99 })) 100 do 101 client:request('textDocument/inlayHint', { 102 textDocument = util.make_text_document_params(bufnr), 103 range = util._make_line_range_params( 104 bufnr, 105 0, 106 api.nvim_buf_line_count(bufnr) - 1, 107 client.offset_encoding 108 ), 109 }, nil, bufnr) 110 end 111 end 112 113 --- |lsp-handler| for the method `workspace/inlayHint/refresh` 114 ---@param ctx lsp.HandlerContext 115 ---@private 116 function M.on_refresh(err, _, ctx) 117 if err then 118 return vim.NIL 119 end 120 for bufnr in pairs(vim.lsp.get_client_by_id(ctx.client_id).attached_buffers or {}) do 121 for _, winid in ipairs(api.nvim_list_wins()) do 122 if api.nvim_win_get_buf(winid) == bufnr then 123 if bufstates[bufnr] and bufstates[bufnr].enabled then 124 bufstates[bufnr].applied = {} 125 refresh(bufnr) 126 end 127 end 128 end 129 end 130 131 return vim.NIL 132 end 133 134 --- Optional filters |kwargs|: 135 --- @class vim.lsp.inlay_hint.get.Filter 136 --- @inlinedoc 137 --- @field bufnr integer? 138 --- @field range lsp.Range? 139 140 --- @class vim.lsp.inlay_hint.get.ret 141 --- @inlinedoc 142 --- @field bufnr integer 143 --- @field client_id integer 144 --- @field inlay_hint lsp.InlayHint 145 146 --- Get the list of inlay hints, (optionally) restricted by buffer or range. 147 --- 148 --- Example usage: 149 --- 150 --- ```lua 151 --- local hint = vim.lsp.inlay_hint.get({ bufnr = 0 })[1] -- 0 for current buffer 152 --- 153 --- local client = vim.lsp.get_client_by_id(hint.client_id) 154 --- local resp = client:request_sync('inlayHint/resolve', hint.inlay_hint, 100, 0) 155 --- local resolved_hint = assert(resp and resp.result, resp.err) 156 --- vim.lsp.util.apply_text_edits(resolved_hint.textEdits, 0, client.encoding) 157 --- 158 --- location = resolved_hint.label[1].location 159 --- client:request('textDocument/hover', { 160 --- textDocument = { uri = location.uri }, 161 --- position = location.range.start, 162 --- }) 163 --- ``` 164 --- 165 --- @param filter vim.lsp.inlay_hint.get.Filter? 166 --- @return vim.lsp.inlay_hint.get.ret[] 167 --- @since 12 168 function M.get(filter) 169 vim.validate('filter', filter, 'table', true) 170 filter = filter or {} 171 172 local bufnr = filter.bufnr 173 if not bufnr then 174 --- @type vim.lsp.inlay_hint.get.ret[] 175 local hints = {} 176 --- @param buf integer 177 vim.tbl_map(function(buf) 178 vim.list_extend(hints, M.get(vim.tbl_extend('keep', { bufnr = buf }, filter))) 179 end, api.nvim_list_bufs()) 180 return hints 181 else 182 bufnr = vim._resolve_bufnr(bufnr) 183 end 184 185 local bufstate = bufstates[bufnr] 186 if not bufstate.client_hints then 187 return {} 188 end 189 190 local clients = vim.lsp.get_clients({ 191 bufnr = bufnr, 192 method = 'textDocument/inlayHint', 193 }) 194 if #clients == 0 then 195 return {} 196 end 197 198 local range = filter.range 199 if not range then 200 range = { 201 start = { line = 0, character = 0 }, 202 ['end'] = { line = api.nvim_buf_line_count(bufnr), character = 0 }, 203 } 204 end 205 206 --- @type vim.lsp.inlay_hint.get.ret[] 207 local result = {} 208 for _, client in pairs(clients) do 209 local lnum_hints = bufstate.client_hints[client.id] 210 if lnum_hints then 211 for lnum = range.start.line, range['end'].line do 212 local hints = lnum_hints[lnum] or {} 213 for _, hint in pairs(hints) do 214 local line, char = hint.position.line, hint.position.character 215 if 216 (line > range.start.line or char >= range.start.character) 217 and (line < range['end'].line or char <= range['end'].character) 218 then 219 table.insert(result, { 220 bufnr = bufnr, 221 client_id = client.id, 222 inlay_hint = hint, 223 }) 224 end 225 end 226 end 227 end 228 end 229 return result 230 end 231 232 --- Clear inlay hints 233 ---@param bufnr (integer) Buffer handle, or 0 for current 234 local function clear(bufnr) 235 bufnr = vim._resolve_bufnr(bufnr) 236 local bufstate = bufstates[bufnr] 237 local client_lens = (bufstate or {}).client_hints or {} 238 local client_ids = vim.tbl_keys(client_lens) --- @type integer[] 239 for _, iter_client_id in ipairs(client_ids) do 240 if bufstate then 241 bufstate.client_hints[iter_client_id] = {} 242 end 243 end 244 api.nvim_buf_clear_namespace(bufnr, namespace, 0, -1) 245 api.nvim__redraw({ buf = bufnr, valid = true, flush = false }) 246 end 247 248 --- Disable inlay hints for a buffer 249 ---@param bufnr (integer) Buffer handle, or 0 for current 250 local function _disable(bufnr) 251 bufnr = vim._resolve_bufnr(bufnr) 252 clear(bufnr) 253 bufstates[bufnr] = nil 254 bufstates[bufnr].enabled = false 255 end 256 257 --- Enable inlay hints for a buffer 258 ---@param bufnr (integer) Buffer handle, or 0 for current 259 local function _enable(bufnr) 260 bufnr = vim._resolve_bufnr(bufnr) 261 bufstates[bufnr] = nil 262 bufstates[bufnr].enabled = true 263 refresh(bufnr) 264 end 265 266 api.nvim_create_autocmd('LspNotify', { 267 callback = function(args) 268 ---@type integer 269 local bufnr = args.buf 270 271 if 272 args.data.method ~= 'textDocument/didChange' 273 and args.data.method ~= 'textDocument/didOpen' 274 then 275 return 276 end 277 if bufstates[bufnr].enabled then 278 refresh(bufnr, args.data.client_id) 279 end 280 end, 281 group = augroup, 282 }) 283 api.nvim_create_autocmd('LspAttach', { 284 callback = function(args) 285 ---@type integer 286 local bufnr = args.buf 287 288 api.nvim_buf_attach(bufnr, false, { 289 on_reload = function(_, cb_bufnr) 290 clear(cb_bufnr) 291 if bufstates[cb_bufnr] and bufstates[cb_bufnr].enabled then 292 bufstates[cb_bufnr].applied = {} 293 refresh(cb_bufnr) 294 end 295 end, 296 on_detach = function(_, cb_bufnr) 297 _disable(cb_bufnr) 298 bufstates[cb_bufnr] = nil 299 end, 300 }) 301 end, 302 group = augroup, 303 }) 304 api.nvim_create_autocmd('LspDetach', { 305 callback = function(args) 306 ---@type integer 307 local bufnr = args.buf 308 local clients = vim.lsp.get_clients({ bufnr = bufnr, method = 'textDocument/inlayHint' }) 309 310 if not vim.iter(clients):any(function(c) 311 return c.id ~= args.data.client_id 312 end) then 313 _disable(bufnr) 314 end 315 end, 316 group = augroup, 317 }) 318 api.nvim_set_decoration_provider(namespace, { 319 on_win = function(_, _, bufnr, topline, botline) 320 ---@type vim.lsp.inlay_hint.bufstate 321 local bufstate = rawget(bufstates, bufnr) 322 if not bufstate then 323 return 324 end 325 326 if bufstate.version ~= util.buf_versions[bufnr] then 327 return 328 end 329 330 if not bufstate.client_hints then 331 return 332 end 333 local client_hints = assert(bufstate.client_hints) 334 335 for lnum = topline, botline do 336 if bufstate.applied[lnum] ~= bufstate.version then 337 api.nvim_buf_clear_namespace(bufnr, namespace, lnum, lnum + 1) 338 339 local hint_virtual_texts = {} --- @type table<integer, [string, string?][]> 340 for _, lnum_hints in pairs(client_hints) do 341 local hints = lnum_hints[lnum] or {} 342 for _, hint in pairs(hints) do 343 local text = '' 344 local label = hint.label 345 if type(label) == 'string' then 346 text = label 347 else 348 for _, part in ipairs(label) do 349 text = text .. part.value 350 end 351 end 352 local vt = hint_virtual_texts[hint.position.character] or {} 353 if hint.paddingLeft then 354 vt[#vt + 1] = { ' ' } 355 end 356 vt[#vt + 1] = { text, 'LspInlayHint' } 357 if hint.paddingRight then 358 vt[#vt + 1] = { ' ' } 359 end 360 hint_virtual_texts[hint.position.character] = vt 361 end 362 end 363 364 for pos, vt in pairs(hint_virtual_texts) do 365 api.nvim_buf_set_extmark(bufnr, namespace, lnum, pos, { 366 virt_text_pos = 'inline', 367 ephemeral = false, 368 virt_text = vt, 369 }) 370 end 371 372 bufstate.applied[lnum] = bufstate.version 373 end 374 end 375 end, 376 }) 377 378 --- Query whether inlay hint is enabled in the {filter}ed scope 379 --- @param filter? vim.lsp.inlay_hint.enable.Filter 380 --- @return boolean 381 --- @since 12 382 function M.is_enabled(filter) 383 vim.validate('filter', filter, 'table', true) 384 filter = filter or {} 385 local bufnr = filter.bufnr 386 387 if bufnr == nil then 388 return globalstate.enabled 389 end 390 return bufstates[vim._resolve_bufnr(bufnr)].enabled 391 end 392 393 --- Optional filters |kwargs|, or `nil` for all. 394 --- @class vim.lsp.inlay_hint.enable.Filter 395 --- @inlinedoc 396 --- Buffer number, or 0 for current buffer, or nil for all. 397 --- @field bufnr integer? 398 399 --- Enables or disables inlay hints for the {filter}ed scope. 400 --- 401 --- To "toggle", pass the inverse of `is_enabled()`: 402 --- 403 --- ```lua 404 --- vim.lsp.inlay_hint.enable(not vim.lsp.inlay_hint.is_enabled()) 405 --- ``` 406 --- 407 --- @param enable (boolean|nil) true/nil to enable, false to disable 408 --- @param filter vim.lsp.inlay_hint.enable.Filter? 409 --- @since 12 410 function M.enable(enable, filter) 411 vim.validate('enable', enable, 'boolean', true) 412 vim.validate('filter', filter, 'table', true) 413 enable = enable == nil or enable 414 filter = filter or {} 415 416 if filter.bufnr == nil then 417 globalstate.enabled = enable 418 for _, bufnr in ipairs(api.nvim_list_bufs()) do 419 if api.nvim_buf_is_loaded(bufnr) then 420 if enable == false then 421 _disable(bufnr) 422 else 423 _enable(bufnr) 424 end 425 else 426 bufstates[bufnr] = nil 427 end 428 end 429 else 430 if enable == false then 431 _disable(filter.bufnr) 432 else 433 _enable(filter.bufnr) 434 end 435 end 436 end 437 438 return M