codelens.lua (14775B)
1 local util = require('vim.lsp.util') 2 local log = require('vim.lsp.log') 3 local api = vim.api 4 local M = {} 5 6 local Capability = require('vim.lsp._capability') 7 8 ---@class (private) vim.lsp.codelens.ClientState 9 ---@field row_lenses table<integer, lsp.CodeLens[]?> row -> lens 10 ---@field namespace integer 11 12 ---@class (private) vim.lsp.codelens.Provider : vim.lsp.Capability 13 ---@field active table<integer, vim.lsp.codelens.Provider?> 14 --- 15 --- `TextDocument` version current state corresponds to. 16 ---@field version? integer 17 --- 18 --- Last version of codelens applied to this line. 19 --- 20 --- Index In the form of row -> true? 21 ---@field row_version table<integer, integer?> 22 --- 23 --- Index In the form of client_id -> client_state 24 ---@field client_state? table<integer, vim.lsp.codelens.ClientState?> 25 --- 26 --- Timer for debouncing automatic requests. 27 --- 28 ---@field timer? uv.uv_timer_t 29 local Provider = { 30 name = 'codelens', 31 method = 'textDocument/codeLens', 32 active = {}, 33 } 34 Provider.__index = Provider 35 setmetatable(Provider, Capability) 36 Capability.all[Provider.name] = Provider 37 38 ---@package 39 ---@param bufnr integer 40 ---@return vim.lsp.codelens.Provider 41 function Provider:new(bufnr) 42 ---@type vim.lsp.codelens.Provider 43 self = Capability.new(self, bufnr) 44 self.client_state = {} 45 self.row_version = {} 46 47 api.nvim_buf_attach(bufnr, false, { 48 on_lines = function(_, buf) 49 local provider = Provider.active[buf] 50 if not provider then 51 return true 52 end 53 provider:automatic_request() 54 end, 55 on_reload = function(_, buf) 56 local provider = Provider.active[buf] 57 if provider then 58 provider:automatic_request() 59 end 60 end, 61 }) 62 63 return self 64 end 65 66 ---@package 67 ---@param client_id integer 68 function Provider:on_attach(client_id) 69 local state = self.client_state[client_id] 70 if not state then 71 state = { 72 namespace = api.nvim_create_namespace('nvim.lsp.codelens:' .. client_id), 73 row_lenses = {}, 74 } 75 self.client_state[client_id] = state 76 end 77 self:request(client_id) 78 end 79 80 ---@package 81 ---@param client_id integer 82 function Provider:on_detach(client_id) 83 local state = self.client_state[client_id] 84 if state then 85 api.nvim_buf_clear_namespace(self.bufnr, state.namespace, 0, -1) 86 self.client_state[client_id] = nil 87 end 88 end 89 90 --- `lsp.Handler` for `textDocument/codeLens`. 91 --- 92 ---@package 93 ---@param err? lsp.ResponseError 94 ---@param result? lsp.CodeLens[] 95 ---@param ctx lsp.HandlerContext 96 function Provider:handler(err, result, ctx) 97 local state = self.client_state[ctx.client_id] 98 if not state then 99 return 100 end 101 102 if err then 103 log.error('codelens', err) 104 return 105 end 106 107 if util.buf_versions[self.bufnr] ~= ctx.version then 108 return 109 end 110 111 ---@type table<integer, lsp.CodeLens[]> 112 local row_lenses = {} 113 114 -- Code lenses should only span a single line. 115 for _, lens in ipairs(result or {}) do 116 local row = lens.range.start.line 117 local lenses = row_lenses[row] or {} 118 table.insert(lenses, lens) 119 row_lenses[row] = lenses 120 end 121 122 state.row_lenses = row_lenses 123 self.version = ctx.version 124 end 125 126 ---@package 127 ---@param client_id? integer 128 ---@param on_response? function 129 function Provider:request(client_id, on_response) 130 ---@type lsp.CodeLensParams 131 local params = { textDocument = util.make_text_document_params(self.bufnr) } 132 for id in pairs(self.client_state) do 133 if not client_id or client_id == id then 134 local client = assert(vim.lsp.get_client_by_id(id)) 135 client:request('textDocument/codeLens', params, function(...) 136 self:handler(...) 137 138 if on_response then 139 on_response() 140 end 141 end, self.bufnr) 142 end 143 end 144 end 145 146 ---@private 147 function Provider:reset_timer() 148 local timer = self.timer 149 if timer then 150 self.timer = nil 151 if not timer:is_closing() then 152 timer:stop() 153 timer:close() 154 end 155 end 156 end 157 158 --- Automatically request with debouncing, used as callbacks in autocmd events. 159 --- 160 ---@package 161 function Provider:automatic_request() 162 self:reset_timer() 163 self.timer = vim.defer_fn(function() 164 self:request() 165 end, 200) 166 end 167 168 ---@private 169 ---@param client vim.lsp.Client 170 ---@param unresolved_lens lsp.CodeLens 171 function Provider:resolve(client, unresolved_lens) 172 ---@param resolved_lens lsp.CodeLens 173 client:request('codeLens/resolve', unresolved_lens, function(err, resolved_lens, ctx) 174 local state = self.client_state[client.id] 175 if not state then 176 return 177 end 178 179 if err then 180 log.error('codelens/resolve', err) 181 return 182 end 183 184 if util.buf_versions[self.bufnr] ~= ctx.version then 185 return 186 end 187 188 local row = unresolved_lens.range.start.line 189 local lenses = assert(state.row_lenses[row]) 190 for i, lens in ipairs(lenses) do 191 if lens == unresolved_lens then 192 lenses[i] = resolved_lens 193 end 194 end 195 196 self.row_version[row] = nil 197 api.nvim__redraw({ 198 buf = self.bufnr, 199 range = { row, row + 1 }, 200 valid = true, 201 flush = false, 202 }) 203 end, self.bufnr) 204 end 205 206 ---@package 207 ---@param toprow integer 208 ---@param botrow integer 209 function Provider:on_win(toprow, botrow) 210 for row = toprow, botrow do 211 if self.row_version[row] ~= self.version then 212 for client_id, state in pairs(self.client_state) do 213 local bufnr = self.bufnr 214 local namespace = state.namespace 215 216 api.nvim_buf_clear_namespace(bufnr, namespace, row, row + 1) 217 218 local lenses = state.row_lenses[row] 219 if lenses then 220 table.sort(lenses, function(a, b) 221 return a.range.start.character < b.range.start.character 222 end) 223 224 ---@type integer 225 local indent = api.nvim_buf_call(bufnr, function() 226 return vim.fn.indent(row + 1) 227 end) 228 229 ---@type [string, string|integer][][] 230 local virt_lines = { { { string.rep(' ', indent), 'LspCodeLensSeparator' } } } 231 local virt_text = virt_lines[1] 232 for _, lens in ipairs(lenses) do 233 -- A code lens is unresolved when no command is associated to it. 234 if not lens.command then 235 local client = assert(vim.lsp.get_client_by_id(client_id)) ---@type vim.lsp.Client 236 self:resolve(client, lens) 237 else 238 virt_text[#virt_text + 1] = { lens.command.title, 'LspCodeLens' } 239 virt_text[#virt_text + 1] = { ' | ', 'LspCodeLensSeparator' } 240 end 241 end 242 243 if #virt_text > 1 then 244 -- Remove trailing separator. 245 virt_text[#virt_text] = nil 246 else 247 -- Use a placeholder to prevent flickering caused by layout shifts. 248 virt_text[#virt_text + 1] = { '...', 'LspCodeLens' } 249 end 250 251 api.nvim_buf_set_extmark(bufnr, namespace, row, 0, { 252 virt_lines = virt_lines, 253 virt_lines_above = true, 254 virt_lines_overflow = 'scroll', 255 hl_mode = 'combine', 256 }) 257 end 258 self.row_version[row] = self.version 259 end 260 end 261 end 262 263 if botrow == api.nvim_buf_line_count(self.bufnr) - 1 then 264 for _, state in pairs(self.client_state) do 265 api.nvim_buf_clear_namespace(self.bufnr, state.namespace, botrow, -1) 266 end 267 end 268 end 269 270 local namespace = api.nvim_create_namespace('nvim.lsp.codelens') 271 api.nvim_set_decoration_provider(namespace, { 272 on_win = function(_, _, bufnr, toprow, botrow) 273 local provider = Provider.active[bufnr] 274 if provider then 275 provider:on_win(toprow, botrow) 276 end 277 end, 278 }) 279 280 --- Query whether code lens is enabled in the {filter}ed scope 281 --- 282 ---@param filter? vim.lsp.capability.enable.Filter 283 ---@return boolean whether code lens is enabled. 284 function M.is_enabled(filter) 285 return vim.lsp._capability.is_enabled('codelens', filter) 286 end 287 288 --- Enables or disables code lens for the {filter}ed scope. 289 --- 290 --- To "toggle", pass the inverse of `is_enabled()`: 291 --- 292 --- ```lua 293 --- vim.lsp.codelens.enable(not vim.lsp.codelens.is_enabled()) 294 --- ``` 295 --- 296 --- To run a code lens, see |vim.lsp.codelens.run()|. 297 --- 298 ---@param enable? boolean true/nil to enable, false to disable 299 ---@param filter? vim.lsp.capability.enable.Filter 300 function M.enable(enable, filter) 301 vim.lsp._capability.enable('codelens', enable, filter) 302 end 303 304 --- Optional filters |kwargs|: 305 ---@class vim.lsp.codelens.get.Filter 306 ---@inlinedoc 307 --- 308 --- Buffer handle, or 0 for current. 309 --- (default: 0) 310 ---@field bufnr? integer 311 --- 312 --- Client ID, or nil for all. 313 --- (default: all) 314 ---@field client_id? integer 315 316 ---@class vim.lsp.codelens.get.Result 317 ---@inlinedoc 318 ---@field client_id integer 319 ---@field lens lsp.CodeLens 320 321 --- Get all code lenses in the {filter}ed scope. 322 --- 323 ---@param filter? vim.lsp.codelens.get.Filter 324 ---@return vim.lsp.codelens.get.Result[] 325 function M.get(filter) 326 if type(filter) == 'number' then 327 vim.deprecate( 328 'vim.lsp.codelens.get(bufnr)', 329 'vim.lsp.codelens.get({ bufnr = bufnr })', 330 '0.13.0' 331 ) 332 local bufnr = vim._resolve_bufnr(filter) 333 local provider = Provider.active[bufnr] 334 if not provider then 335 return {} 336 end 337 ---@type lsp.CodeLens[] 338 local result = {} 339 for _, state in pairs(provider.client_state) do 340 for _, lenses in pairs(state.row_lenses) do 341 result = vim.list_extend(result, lenses) 342 end 343 end 344 return result 345 end 346 347 vim.validate('filter', filter, 'table', true) 348 filter = filter or {} 349 350 local bufnr = vim._resolve_bufnr(filter.bufnr) 351 local provider = Provider.active[bufnr] 352 if not provider then 353 return {} 354 end 355 356 local result = {} 357 for client_id, state in pairs(provider.client_state) do 358 if not filter.client_id or filter.client_id == client_id then 359 for _, lenses in pairs(state.row_lenses) do 360 for _, lens in ipairs(lenses) do 361 table.insert(result, { client_id = client_id, lens = lens }) 362 end 363 end 364 end 365 end 366 return result 367 end 368 369 ---@param lnum integer 370 ---@param opts vim.lsp.codelens.run.Opts 371 ---@param results table<integer, {err: lsp.ResponseError?, result: lsp.CodeLens[]?}> 372 ---@param context lsp.HandlerContext 373 local function on_lenses_run(lnum, opts, results, context) 374 local bufnr = context.bufnr or 0 375 376 ---@type {client: vim.lsp.Client, lens: lsp.CodeLens}[] 377 local candidates = {} 378 local pending_resolve = 1 379 local function on_resolved() 380 pending_resolve = pending_resolve - 1 381 if pending_resolve > 0 then 382 return 383 end 384 if #candidates == 0 then 385 vim.notify('No codelens at current line') 386 elseif #candidates == 1 then 387 local candidate = candidates[1] 388 candidate.client:exec_cmd(candidate.lens.command, { bufnr = bufnr }) 389 else 390 local selectopts = { 391 prompt = 'Code lenses: ', 392 kind = 'codelens', 393 ---@param candidate {client: vim.lsp.Client, lens: lsp.CodeLens} 394 format_item = function(candidate) 395 return string.format('%s [%s]', candidate.lens.command.title, candidate.client.name) 396 end, 397 } 398 vim.ui.select(candidates, selectopts, function(candidate) 399 if candidate then 400 candidate.client:exec_cmd(candidate.lens.command, { bufnr = bufnr }) 401 end 402 end) 403 end 404 end 405 for client_id, result in pairs(results) do 406 if opts.client_id == nil or opts.client_id == client_id then 407 local client = assert(vim.lsp.get_client_by_id(client_id)) 408 for _, lens in ipairs(result.result or {}) do 409 if lens.range.start.line == lnum then 410 if lens.command then 411 table.insert(candidates, { client = client, lens = lens }) 412 else 413 pending_resolve = pending_resolve + 1 414 client:request('codeLens/resolve', lens, function(_, resolved_lens) 415 if resolved_lens then 416 table.insert(candidates, { client = client, lens = resolved_lens }) 417 end 418 on_resolved() 419 end, bufnr) 420 end 421 end 422 end 423 end 424 end 425 on_resolved() 426 end 427 428 --- Optional parameters |kwargs|: 429 ---@class vim.lsp.codelens.run.Opts 430 ---@inlinedoc 431 --- 432 --- Client ID, or nil for all. 433 --- (default: all) 434 ---@field client_id? integer 435 436 --- Run code lens at the current cursor position. 437 --- 438 ---@param opts? vim.lsp.codelens.run.Opts 439 function M.run(opts) 440 vim.validate('opts', opts, 'table', true) 441 opts = opts or {} 442 443 local winid = api.nvim_get_current_win() 444 local bufnr = api.nvim_win_get_buf(winid) 445 local pos = vim.pos.cursor(api.nvim_win_get_cursor(winid)) 446 local params = { 447 textDocument = vim.lsp.util.make_text_document_params(bufnr), 448 } 449 vim.lsp.buf_request_all(bufnr, 'textDocument/codeLens', params, function(results, context) 450 on_lenses_run(pos.row, opts, results, context) 451 end) 452 end 453 454 --- |lsp-handler| for the method `workspace/codeLens/refresh` 455 --- 456 ---@private 457 ---@type lsp.Handler 458 function M.on_refresh(err, _, ctx) 459 if err then 460 return vim.NIL 461 end 462 463 for bufnr, provider in pairs(Provider.active) do 464 for client_id in pairs(provider.client_state) do 465 if client_id == ctx.client_id then 466 provider:request(client_id, function() 467 provider.row_version = {} 468 vim.api.nvim__redraw({ buf = bufnr, valid = true, flush = false }) 469 end) 470 end 471 end 472 end 473 return vim.NIL 474 end 475 476 ---@deprecated 477 ---@param client_id? integer 478 ---@param bufnr? integer 479 function M.clear(client_id, bufnr) 480 vim.deprecate( 481 'vim.lsp.codelens.clear(client_id, bufnr)', 482 'vim.lsp.codelens.enable(false, { bufnr = bufnr, client_id = client_id })', 483 '0.13.0' 484 ) 485 M.enable(false, { bufnr = bufnr, client_id = client_id }) 486 end 487 488 ---@deprecated 489 ---@param lenses? lsp.CodeLens[] lenses to display 490 ---@param bufnr integer 491 ---@param client_id integer 492 function M.display(lenses, bufnr, client_id) 493 vim.deprecate('vim.lsp.codelens.display()', nil, '0.13.0') 494 local _, _, _ = lenses, bufnr, client_id 495 end 496 497 ---@deprecated 498 ---@param lenses? lsp.CodeLens[] lenses to store 499 ---@param bufnr integer 500 ---@param client_id integer 501 function M.save(lenses, bufnr, client_id) 502 vim.deprecate('vim.lsp.codelens.save()', nil, '0.13.0') 503 local _, _, _ = lenses, bufnr, client_id 504 end 505 506 ---@deprecated 507 ---@param err? lsp.ResponseError 508 ---@param result lsp.CodeLens[] 509 ---@param ctx lsp.HandlerContext 510 function M.on_codelens(err, result, ctx) 511 vim.deprecate('vim.lsp.codelens.on_codelens()', nil, '0.13.0') 512 local _, _, _ = err, result, ctx 513 end 514 515 ---@class vim.lsp.codelens.refresh.Opts 516 ---@inlinedoc 517 ---@field bufnr? integer 518 519 ---@deprecated 520 ---@param opts? vim.lsp.codelens.refresh.Opts Optional fields 521 function M.refresh(opts) 522 vim.deprecate( 523 'vim.lsp.codelens.refresh({ bufnr = bufnr})', 524 'vim.lsp.codelens.enable(true, { bufnr = bufnr })', 525 '0.13.0' 526 ) 527 528 vim.validate('opts', opts, 'table', true) 529 M.enable(true, { bufnr = opts and opts.bufnr }) 530 end 531 532 return M