codelens.lua 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. local util = require('vim.lsp.util')
  2. local log = require('vim.lsp.log')
  3. local api = vim.api
  4. local M = {}
  5. --- bufnr → true|nil
  6. --- to throttle refreshes to at most one at a time
  7. local active_refreshes = {}
  8. --- bufnr -> client_id -> lenses
  9. local lens_cache_by_buf = setmetatable({}, {
  10. __index = function(t, b)
  11. local key = b > 0 and b or api.nvim_get_current_buf()
  12. return rawget(t, key)
  13. end,
  14. })
  15. local namespaces = setmetatable({}, {
  16. __index = function(t, key)
  17. local value = api.nvim_create_namespace('vim_lsp_codelens:' .. key)
  18. rawset(t, key, value)
  19. return value
  20. end,
  21. })
  22. ---@private
  23. M.__namespaces = namespaces
  24. ---@private
  25. local function execute_lens(lens, bufnr, client_id)
  26. local line = lens.range.start.line
  27. api.nvim_buf_clear_namespace(bufnr, namespaces[client_id], line, line + 1)
  28. local client = vim.lsp.get_client_by_id(client_id)
  29. assert(client, 'Client is required to execute lens, client_id=' .. client_id)
  30. local command = lens.command
  31. local fn = client.commands[command.command] or vim.lsp.commands[command.command]
  32. if fn then
  33. fn(command, { bufnr = bufnr, client_id = client_id })
  34. return
  35. end
  36. -- Need to use the client that returned the lens → must not use buf_request
  37. local command_provider = client.server_capabilities.executeCommandProvider
  38. local commands = type(command_provider) == 'table' and command_provider.commands or {}
  39. if not vim.tbl_contains(commands, command.command) then
  40. vim.notify(
  41. string.format(
  42. 'Language server does not support command `%s`. This command may require a client extension.',
  43. command.command
  44. ),
  45. vim.log.levels.WARN
  46. )
  47. return
  48. end
  49. client.request('workspace/executeCommand', command, function(...)
  50. local result = vim.lsp.handlers['workspace/executeCommand'](...)
  51. M.refresh()
  52. return result
  53. end, bufnr)
  54. end
  55. --- Return all lenses for the given buffer
  56. ---
  57. ---@param bufnr number Buffer number. 0 can be used for the current buffer.
  58. ---@return table (`CodeLens[]`)
  59. function M.get(bufnr)
  60. local lenses_by_client = lens_cache_by_buf[bufnr or 0]
  61. if not lenses_by_client then
  62. return {}
  63. end
  64. local lenses = {}
  65. for _, client_lenses in pairs(lenses_by_client) do
  66. vim.list_extend(lenses, client_lenses)
  67. end
  68. return lenses
  69. end
  70. --- Run the code lens in the current line
  71. ---
  72. function M.run()
  73. local line = api.nvim_win_get_cursor(0)[1]
  74. local bufnr = api.nvim_get_current_buf()
  75. local options = {}
  76. local lenses_by_client = lens_cache_by_buf[bufnr] or {}
  77. for client, lenses in pairs(lenses_by_client) do
  78. for _, lens in pairs(lenses) do
  79. if lens.range.start.line == (line - 1) then
  80. table.insert(options, { client = client, lens = lens })
  81. end
  82. end
  83. end
  84. if #options == 0 then
  85. vim.notify('No executable codelens found at current line')
  86. elseif #options == 1 then
  87. local option = options[1]
  88. execute_lens(option.lens, bufnr, option.client)
  89. else
  90. vim.ui.select(options, {
  91. prompt = 'Code lenses:',
  92. format_item = function(option)
  93. return option.lens.command.title
  94. end,
  95. }, function(option)
  96. if option then
  97. execute_lens(option.lens, bufnr, option.client)
  98. end
  99. end)
  100. end
  101. end
  102. --- Display the lenses using virtual text
  103. ---
  104. ---@param lenses table of lenses to display (`CodeLens[] | null`)
  105. ---@param bufnr number
  106. ---@param client_id number
  107. function M.display(lenses, bufnr, client_id)
  108. if not lenses or not next(lenses) then
  109. return
  110. end
  111. local lenses_by_lnum = {}
  112. for _, lens in pairs(lenses) do
  113. local line_lenses = lenses_by_lnum[lens.range.start.line]
  114. if not line_lenses then
  115. line_lenses = {}
  116. lenses_by_lnum[lens.range.start.line] = line_lenses
  117. end
  118. table.insert(line_lenses, lens)
  119. end
  120. local ns = namespaces[client_id]
  121. local num_lines = api.nvim_buf_line_count(bufnr)
  122. for i = 0, num_lines do
  123. local line_lenses = lenses_by_lnum[i] or {}
  124. api.nvim_buf_clear_namespace(bufnr, ns, i, i + 1)
  125. local chunks = {}
  126. local num_line_lenses = #line_lenses
  127. table.sort(line_lenses, function(a, b)
  128. return a.range.start.character < b.range.start.character
  129. end)
  130. for j, lens in ipairs(line_lenses) do
  131. local text = lens.command and lens.command.title or 'Unresolved lens ...'
  132. table.insert(chunks, { text, 'LspCodeLens' })
  133. if j < num_line_lenses then
  134. table.insert(chunks, { ' | ', 'LspCodeLensSeparator' })
  135. end
  136. end
  137. if #chunks > 0 then
  138. api.nvim_buf_set_extmark(bufnr, ns, i, 0, {
  139. virt_text = chunks,
  140. hl_mode = 'combine',
  141. })
  142. end
  143. end
  144. end
  145. --- Store lenses for a specific buffer and client
  146. ---
  147. ---@param lenses table of lenses to store (`CodeLens[] | null`)
  148. ---@param bufnr number
  149. ---@param client_id number
  150. function M.save(lenses, bufnr, client_id)
  151. local lenses_by_client = lens_cache_by_buf[bufnr]
  152. if not lenses_by_client then
  153. lenses_by_client = {}
  154. lens_cache_by_buf[bufnr] = lenses_by_client
  155. local ns = namespaces[client_id]
  156. api.nvim_buf_attach(bufnr, false, {
  157. on_detach = function(b)
  158. lens_cache_by_buf[b] = nil
  159. end,
  160. on_lines = function(_, b, _, first_lnum, last_lnum)
  161. api.nvim_buf_clear_namespace(b, ns, first_lnum, last_lnum)
  162. end,
  163. })
  164. end
  165. lenses_by_client[client_id] = lenses
  166. end
  167. ---@private
  168. local function resolve_lenses(lenses, bufnr, client_id, callback)
  169. lenses = lenses or {}
  170. local num_lens = vim.tbl_count(lenses)
  171. if num_lens == 0 then
  172. callback()
  173. return
  174. end
  175. ---@private
  176. local function countdown()
  177. num_lens = num_lens - 1
  178. if num_lens == 0 then
  179. callback()
  180. end
  181. end
  182. local ns = namespaces[client_id]
  183. local client = vim.lsp.get_client_by_id(client_id)
  184. for _, lens in pairs(lenses or {}) do
  185. if lens.command then
  186. countdown()
  187. else
  188. client.request('codeLens/resolve', lens, function(_, result)
  189. if result and result.command then
  190. lens.command = result.command
  191. -- Eager display to have some sort of incremental feedback
  192. -- Once all lenses got resolved there will be a full redraw for all lenses
  193. -- So that multiple lens per line are properly displayed
  194. api.nvim_buf_set_extmark(
  195. bufnr,
  196. ns,
  197. lens.range.start.line,
  198. 0,
  199. { virt_text = { { lens.command.title, 'LspCodeLens' } }, hl_mode = 'combine' }
  200. )
  201. end
  202. countdown()
  203. end, bufnr)
  204. end
  205. end
  206. end
  207. --- |lsp-handler| for the method `textDocument/codeLens`
  208. ---
  209. function M.on_codelens(err, result, ctx, _)
  210. if err then
  211. active_refreshes[ctx.bufnr] = nil
  212. local _ = log.error() and log.error('codelens', err)
  213. return
  214. end
  215. M.save(result, ctx.bufnr, ctx.client_id)
  216. -- Eager display for any resolved (and unresolved) lenses and refresh them
  217. -- once resolved.
  218. M.display(result, ctx.bufnr, ctx.client_id)
  219. resolve_lenses(result, ctx.bufnr, ctx.client_id, function()
  220. active_refreshes[ctx.bufnr] = nil
  221. M.display(result, ctx.bufnr, ctx.client_id)
  222. end)
  223. end
  224. --- Refresh the codelens for the current buffer
  225. ---
  226. --- It is recommended to trigger this using an autocmd or via keymap.
  227. ---
  228. --- <pre>
  229. --- autocmd BufEnter,CursorHold,InsertLeave <buffer> lua vim.lsp.codelens.refresh()
  230. --- </pre>
  231. ---
  232. function M.refresh()
  233. local params = {
  234. textDocument = util.make_text_document_params(),
  235. }
  236. local bufnr = api.nvim_get_current_buf()
  237. if active_refreshes[bufnr] then
  238. return
  239. end
  240. active_refreshes[bufnr] = true
  241. vim.lsp.buf_request(0, 'textDocument/codeLens', params, M.on_codelens)
  242. end
  243. return M