diff options
author | Mathias Fussenegger <f.mathias@zignar.net> | 2020-10-26 11:50:57 +0100 |
---|---|---|
committer | Mathias Fussenegger <f.mathias@zignar.net> | 2021-06-14 21:45:14 +0200 |
commit | 2bdd553c9ec47066c10d51f39fe16d83fbdd4e68 (patch) | |
tree | d9d375c8aef5f040aef3735bd9c516467471de52 /runtime/lua/vim/lsp/codelens.lua | |
parent | 2f0e5e7e67faa469f5d12a66ec084ab9c35d8c6b (diff) | |
download | rneovim-2bdd553c9ec47066c10d51f39fe16d83fbdd4e68.tar.gz rneovim-2bdd553c9ec47066c10d51f39fe16d83fbdd4e68.tar.bz2 rneovim-2bdd553c9ec47066c10d51f39fe16d83fbdd4e68.zip |
feat(lsp): Add codelens support
Diffstat (limited to 'runtime/lua/vim/lsp/codelens.lua')
-rw-r--r-- | runtime/lua/vim/lsp/codelens.lua | 231 |
1 files changed, 231 insertions, 0 deletions
diff --git a/runtime/lua/vim/lsp/codelens.lua b/runtime/lua/vim/lsp/codelens.lua new file mode 100644 index 0000000000..fbd37e3830 --- /dev/null +++ b/runtime/lua/vim/lsp/codelens.lua @@ -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 |