mirror of
				https://github.com/neovim/neovim.git
				synced 2025-10-26 12:27:24 +00:00 
			
		
		
		
	feat(lsp): Add codelens support
This commit is contained in:
		| @@ -1558,6 +1558,50 @@ show_line_diagnostics({opts}, {bufnr}, {line_nr}, {client_id}) | |||||||
|                     table {popup_bufnr, win_id} |                     table {popup_bufnr, win_id} | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ============================================================================== | ||||||
|  | Lua module: vim.lsp.codelens                                    *lsp-codelens* | ||||||
|  |  | ||||||
|  | display({lenses}, {bufnr}, {client_id})           *vim.lsp.codelens.display()* | ||||||
|  |                 Display the lenses using virtual text | ||||||
|  |  | ||||||
|  |                 Parameters: ~ | ||||||
|  |                     {lenses}     table of lenses to display ( `CodeLens[] | | ||||||
|  |                                  null` ) | ||||||
|  |                     {bufnr}      number | ||||||
|  |                     {client_id}  number | ||||||
|  |  | ||||||
|  | get({bufnr})                                          *vim.lsp.codelens.get()* | ||||||
|  |                 Return all lenses for the given buffer | ||||||
|  |  | ||||||
|  |                 Return: ~ | ||||||
|  |                     table ( `CodeLens[]` ) | ||||||
|  |  | ||||||
|  |                                               *vim.lsp.codelens.on_codelens()* | ||||||
|  | on_codelens({err}, {_}, {result}, {client_id}, {bufnr}) | ||||||
|  |                 |lsp-handler| for the method `textDocument/codeLens` | ||||||
|  |  | ||||||
|  | refresh()                                         *vim.lsp.codelens.refresh()* | ||||||
|  |                 Refresh the codelens for the current buffer | ||||||
|  |  | ||||||
|  |                 It is recommended to trigger this using an autocmd or via | ||||||
|  |                 keymap. | ||||||
|  | > | ||||||
|  |     autocmd BufEnter,CursorHold,InsertLeave <buffer> lua vim.lsp.codelens.refresh() | ||||||
|  | < | ||||||
|  |  | ||||||
|  | run()                                                 *vim.lsp.codelens.run()* | ||||||
|  |                 Run the code lens in the current line | ||||||
|  |  | ||||||
|  | save({lenses}, {bufnr}, {client_id})                 *vim.lsp.codelens.save()* | ||||||
|  |                 Store lenses for a specific buffer and client | ||||||
|  |  | ||||||
|  |                 Parameters: ~ | ||||||
|  |                     {lenses}     table of lenses to store ( `CodeLens[] | | ||||||
|  |                                  null` ) | ||||||
|  |                     {bufnr}      number | ||||||
|  |                     {client_id}  number | ||||||
|  |  | ||||||
|  |  | ||||||
| ============================================================================== | ============================================================================== | ||||||
| Lua module: vim.lsp.handlers                                    *lsp-handlers* | Lua module: vim.lsp.handlers                                    *lsp-handlers* | ||||||
|  |  | ||||||
|   | |||||||
| @@ -20,6 +20,7 @@ local lsp = { | |||||||
|  |  | ||||||
|   buf = require'vim.lsp.buf'; |   buf = require'vim.lsp.buf'; | ||||||
|   diagnostic = require'vim.lsp.diagnostic'; |   diagnostic = require'vim.lsp.diagnostic'; | ||||||
|  |   codelens = require'vim.lsp.codelens'; | ||||||
|   util = util; |   util = util; | ||||||
|  |  | ||||||
|   -- Allow raw RPC access. |   -- Allow raw RPC access. | ||||||
|   | |||||||
							
								
								
									
										231
									
								
								runtime/lua/vim/lsp/codelens.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										231
									
								
								runtime/lua/vim/lsp/codelens.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,231 @@ | |||||||
|  | local util = require('vim.lsp.util') | ||||||
|  | local api = vim.api | ||||||
|  | local M = {} | ||||||
|  |  | ||||||
|  | --- bufnr → true|nil | ||||||
|  | --- to throttle refreshes to at most one at a time | ||||||
|  | local active_refreshes = {} | ||||||
|  |  | ||||||
|  | --- bufnr -> client_id -> lenses | ||||||
|  | local lens_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 namespaces = setmetatable({}, { | ||||||
|  |   __index = function(t, key) | ||||||
|  |     local value = api.nvim_create_namespace('vim_lsp_codelens:' .. key) | ||||||
|  |     rawset(t, key, value) | ||||||
|  |     return value | ||||||
|  |   end; | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | --@private | ||||||
|  | M.__namespaces = namespaces | ||||||
|  |  | ||||||
|  |  | ||||||
|  | --@private | ||||||
|  | local function execute_lens(lens, bufnr, client_id) | ||||||
|  |   local line = lens.range.start.line | ||||||
|  |   api.nvim_buf_clear_namespace(bufnr, namespaces[client_id], line, line + 1) | ||||||
|  |  | ||||||
|  |   -- Need to use the client that returned the lens → must not use buf_request | ||||||
|  |   local client = vim.lsp.get_client_by_id(client_id) | ||||||
|  |   assert(client, 'Client is required to execute lens, client_id=' .. client_id) | ||||||
|  |   client.request('workspace/executeCommand', lens.command, function(...) | ||||||
|  |     local result = vim.lsp.handlers['workspace/executeCommand'](...) | ||||||
|  |     M.refresh() | ||||||
|  |     return result | ||||||
|  |   end, bufnr) | ||||||
|  | end | ||||||
|  |  | ||||||
|  |  | ||||||
|  | --- Return all lenses for the given buffer | ||||||
|  | --- | ||||||
|  | ---@return table (`CodeLens[]`) | ||||||
|  | function M.get(bufnr) | ||||||
|  |   local lenses_by_client = lens_cache_by_buf[bufnr] | ||||||
|  |   if not lenses_by_client then return {} end | ||||||
|  |   local lenses = {} | ||||||
|  |   for _, client_lenses in pairs(lenses_by_client) do | ||||||
|  |     vim.list_extend(lenses, client_lenses) | ||||||
|  |   end | ||||||
|  |   return lenses | ||||||
|  | end | ||||||
|  |  | ||||||
|  |  | ||||||
|  | --- Run the code lens in the current line | ||||||
|  | --- | ||||||
|  | function M.run() | ||||||
|  |   local line = api.nvim_win_get_cursor(0)[1] | ||||||
|  |   local bufnr = api.nvim_get_current_buf() | ||||||
|  |   local options = {} | ||||||
|  |   local lenses_by_client = lens_cache_by_buf[bufnr] or {} | ||||||
|  |   for client, lenses in pairs(lenses_by_client) do | ||||||
|  |     for _, lens in pairs(lenses) do | ||||||
|  |       if lens.range.start.line == (line - 1) then | ||||||
|  |         table.insert(options, {client=client, lens=lens}) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |   if #options == 0 then | ||||||
|  |     vim.notify('No executable codelens found at current line') | ||||||
|  |   elseif #options == 1 then | ||||||
|  |     local option = options[1] | ||||||
|  |     execute_lens(option.lens, bufnr, option.client) | ||||||
|  |   else | ||||||
|  |     local options_strings = {"Code lenses:"} | ||||||
|  |     for i, option in ipairs(options) do | ||||||
|  |        table.insert(options_strings, string.format('%d. %s', i, option.lens.command.title)) | ||||||
|  |     end | ||||||
|  |     local choice = vim.fn.inputlist(options_strings) | ||||||
|  |     if choice < 1 or choice > #options then | ||||||
|  |       return | ||||||
|  |     end | ||||||
|  |     local option = options[choice] | ||||||
|  |     execute_lens(option.lens, bufnr, option.client) | ||||||
|  |   end | ||||||
|  | end | ||||||
|  |  | ||||||
|  |  | ||||||
|  | --- Display the lenses using virtual text | ||||||
|  | --- | ||||||
|  | ---@param lenses table of lenses to display (`CodeLens[] | null`) | ||||||
|  | ---@param bufnr number | ||||||
|  | ---@param client_id number | ||||||
|  | function M.display(lenses, bufnr, client_id) | ||||||
|  |   if not lenses or not next(lenses) then | ||||||
|  |     return | ||||||
|  |   end | ||||||
|  |   local lenses_by_lnum = {} | ||||||
|  |   for _, lens in pairs(lenses) do | ||||||
|  |     local line_lenses = lenses_by_lnum[lens.range.start.line] | ||||||
|  |     if not line_lenses then | ||||||
|  |       line_lenses = {} | ||||||
|  |       lenses_by_lnum[lens.range.start.line] = line_lenses | ||||||
|  |     end | ||||||
|  |     table.insert(line_lenses, lens) | ||||||
|  |   end | ||||||
|  |   local ns = namespaces[client_id] | ||||||
|  |   local num_lines = api.nvim_buf_line_count(bufnr) | ||||||
|  |   for i = 0, num_lines do | ||||||
|  |     local line_lenses = lenses_by_lnum[i] | ||||||
|  |     api.nvim_buf_clear_namespace(bufnr, ns, i, i + 1) | ||||||
|  |     local chunks = {} | ||||||
|  |     for _, lens in pairs(line_lenses or {}) do | ||||||
|  |       local text = lens.command and lens.command.title or 'Unresolved lens ...' | ||||||
|  |       table.insert(chunks, {text, 'LspCodeLens' }) | ||||||
|  |     end | ||||||
|  |     if #chunks > 0 then | ||||||
|  |       api.nvim_buf_set_virtual_text(bufnr, ns, i, chunks, {}) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  |  | ||||||
|  |  | ||||||
|  | --- Store lenses for a specific buffer and client | ||||||
|  | --- | ||||||
|  | ---@param lenses table of lenses to store (`CodeLens[] | null`) | ||||||
|  | ---@param bufnr number | ||||||
|  | ---@param client_id number | ||||||
|  | function M.save(lenses, bufnr, client_id) | ||||||
|  |   local lenses_by_client = lens_cache_by_buf[bufnr] | ||||||
|  |   if not lenses_by_client then | ||||||
|  |     lenses_by_client = {} | ||||||
|  |     lens_cache_by_buf[bufnr] = lenses_by_client | ||||||
|  |     local ns = namespaces[client_id] | ||||||
|  |     api.nvim_buf_attach(bufnr, false, { | ||||||
|  |       on_detach = function(b) lens_cache_by_buf[b] = nil end, | ||||||
|  |       on_lines = function(_, b, _, first_lnum, last_lnum) | ||||||
|  |         api.nvim_buf_clear_namespace(b, ns, first_lnum, last_lnum) | ||||||
|  |       end | ||||||
|  |     }) | ||||||
|  |   end | ||||||
|  |   lenses_by_client[client_id] = lenses | ||||||
|  | end | ||||||
|  |  | ||||||
|  |  | ||||||
|  | --@private | ||||||
|  | local function resolve_lenses(lenses, bufnr, client_id, callback) | ||||||
|  |   lenses = lenses or {} | ||||||
|  |   local num_lens = vim.tbl_count(lenses) | ||||||
|  |   if num_lens == 0 then | ||||||
|  |     callback() | ||||||
|  |     return | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   --@private | ||||||
|  |   local function countdown() | ||||||
|  |     num_lens = num_lens - 1 | ||||||
|  |     if num_lens == 0 then | ||||||
|  |       callback() | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |   local ns = namespaces[client_id] | ||||||
|  |   local client = vim.lsp.get_client_by_id(client_id) | ||||||
|  |   for _, lens in pairs(lenses or {}) do | ||||||
|  |     if lens.command then | ||||||
|  |       countdown() | ||||||
|  |     else | ||||||
|  |       client.request('codeLens/resolve', lens, function(_, _, result) | ||||||
|  |         if result and result.command then | ||||||
|  |           lens.command = result.command | ||||||
|  |           -- Eager display to have some sort of incremental feedback | ||||||
|  |           -- Once all lenses got resolved there will be a full redraw for all lenses | ||||||
|  |           -- So that multiple lens per line are properly displayed | ||||||
|  |           api.nvim_buf_set_virtual_text( | ||||||
|  |             bufnr, | ||||||
|  |             ns, | ||||||
|  |             lens.range.start.line, | ||||||
|  |             {{ lens.command.title, 'LspCodeLens' },}, | ||||||
|  |             {} | ||||||
|  |           ) | ||||||
|  |         end | ||||||
|  |         countdown() | ||||||
|  |       end, bufnr) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  |  | ||||||
|  |  | ||||||
|  | --- |lsp-handler| for the method `textDocument/codeLens` | ||||||
|  | --- | ||||||
|  | function M.on_codelens(err, _, result, client_id, bufnr) | ||||||
|  |   assert(not err, vim.inspect(err)) | ||||||
|  |  | ||||||
|  |   M.save(result, bufnr, client_id) | ||||||
|  |  | ||||||
|  |   -- Eager display for any resolved (and unresolved) lenses and refresh them | ||||||
|  |   -- once resolved. | ||||||
|  |   M.display(result, bufnr, client_id) | ||||||
|  |   resolve_lenses(result, bufnr, client_id, function() | ||||||
|  |     M.display(result, bufnr, client_id) | ||||||
|  |     active_refreshes[bufnr] = nil | ||||||
|  |   end) | ||||||
|  | end | ||||||
|  |  | ||||||
|  |  | ||||||
|  | --- Refresh the codelens for the current buffer | ||||||
|  | --- | ||||||
|  | --- It is recommended to trigger this using an autocmd or via keymap. | ||||||
|  | --- | ||||||
|  | --- <pre> | ||||||
|  | ---   autocmd BufEnter,CursorHold,InsertLeave <buffer> lua vim.lsp.codelens.refresh() | ||||||
|  | --- </pre> | ||||||
|  | --- | ||||||
|  | function M.refresh() | ||||||
|  |   local params = { | ||||||
|  |     textDocument = util.make_text_document_params() | ||||||
|  |   } | ||||||
|  |   local bufnr = api.nvim_get_current_buf() | ||||||
|  |   if active_refreshes[bufnr] then | ||||||
|  |     return | ||||||
|  |   end | ||||||
|  |   active_refreshes[bufnr] = true | ||||||
|  |   vim.lsp.buf_request(0, 'textDocument/codeLens', params) | ||||||
|  | end | ||||||
|  |  | ||||||
|  |  | ||||||
|  | return M | ||||||
| @@ -187,6 +187,10 @@ M['textDocument/publishDiagnostics'] = function(...) | |||||||
|   return require('vim.lsp.diagnostic').on_publish_diagnostics(...) |   return require('vim.lsp.diagnostic').on_publish_diagnostics(...) | ||||||
| end | end | ||||||
|  |  | ||||||
|  | M['textDocument/codeLens'] = function(...) | ||||||
|  |   return require('vim.lsp.codelens').on_codelens(...) | ||||||
|  | end | ||||||
|  |  | ||||||
| --@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references | --@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references | ||||||
| M['textDocument/references'] = function(_, _, result) | M['textDocument/references'] = function(_, _, result) | ||||||
|   if not result then return end |   if not result then return end | ||||||
|   | |||||||
| @@ -154,6 +154,7 @@ CONFIG = { | |||||||
|             'lsp.lua', |             'lsp.lua', | ||||||
|             'buf.lua', |             'buf.lua', | ||||||
|             'diagnostic.lua', |             'diagnostic.lua', | ||||||
|  |             'codelens.lua', | ||||||
|             'handlers.lua', |             'handlers.lua', | ||||||
|             'util.lua', |             'util.lua', | ||||||
|             'log.lua', |             'log.lua', | ||||||
|   | |||||||
							
								
								
									
										62
									
								
								test/functional/plugin/lsp/codelens_spec.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								test/functional/plugin/lsp/codelens_spec.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | |||||||
|  | local helpers = require('test.functional.helpers')(after_each) | ||||||
|  |  | ||||||
|  | local exec_lua = helpers.exec_lua | ||||||
|  | local eq = helpers.eq | ||||||
|  |  | ||||||
|  | describe('vim.lsp.codelens', function() | ||||||
|  |   before_each(function() | ||||||
|  |     helpers.clear() | ||||||
|  |     exec_lua('require("vim.lsp")') | ||||||
|  |   end) | ||||||
|  |   after_each(helpers.clear) | ||||||
|  |  | ||||||
|  |   it('on_codelens_stores_and_displays_lenses', function() | ||||||
|  |     local fake_uri = "file://fake/uri" | ||||||
|  |     local bufnr = exec_lua([[ | ||||||
|  |       fake_uri = ... | ||||||
|  |       local bufnr = vim.uri_to_bufnr(fake_uri) | ||||||
|  |       local lines = {'So', 'many', 'lines'} | ||||||
|  |       vim.fn.bufload(bufnr) | ||||||
|  |       vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) | ||||||
|  |       return bufnr | ||||||
|  |     ]], fake_uri) | ||||||
|  |  | ||||||
|  |     exec_lua([[ | ||||||
|  |       local bufnr = ... | ||||||
|  |       local lenses = { | ||||||
|  |         { | ||||||
|  |           range = { | ||||||
|  |             start = { line = 0, character = 0, }, | ||||||
|  |             ['end'] = { line = 0, character = 0 } | ||||||
|  |           }, | ||||||
|  |           command = { title = 'Lens1', command = 'Dummy' } | ||||||
|  |         }, | ||||||
|  |       } | ||||||
|  |       vim.lsp.codelens.on_codelens(nil, 'textDocument/codeLens', lenses, 1, bufnr) | ||||||
|  |     ]], bufnr) | ||||||
|  |  | ||||||
|  |     local stored_lenses = exec_lua('return vim.lsp.codelens.get(...)', bufnr) | ||||||
|  |     local expected = { | ||||||
|  |       { | ||||||
|  |         range = { | ||||||
|  |           start = { line = 0, character = 0 }, | ||||||
|  |           ['end'] = { line = 0, character = 0 } | ||||||
|  |         }, | ||||||
|  |         command = { | ||||||
|  |           title = 'Lens1', | ||||||
|  |           command = 'Dummy', | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |     } | ||||||
|  |     eq(expected, stored_lenses) | ||||||
|  |  | ||||||
|  |     local virtual_text_chunks = exec_lua([[ | ||||||
|  |       local bufnr = ... | ||||||
|  |       local ns = vim.lsp.codelens.__namespaces[1] | ||||||
|  |       local extmarks = vim.api.nvim_buf_get_extmarks(bufnr, ns, 0, -1, {}) | ||||||
|  |       return vim.api.nvim_buf_get_extmark_by_id(bufnr, ns, extmarks[1][1], { details = true })[3].virt_text | ||||||
|  |     ]], bufnr) | ||||||
|  |  | ||||||
|  |     eq({[1] = {'Lens1', 'LspCodeLens'}}, virtual_text_chunks) | ||||||
|  |   end) | ||||||
|  | end) | ||||||
		Reference in New Issue
	
	Block a user
	 Mathias Fussenegger
					Mathias Fussenegger