feat(lsp): Add codelens support

This commit is contained in:
Mathias Fussenegger 2020-10-26 11:50:57 +01:00
parent 2f0e5e7e67
commit 2bdd553c9e
6 changed files with 343 additions and 0 deletions

View File

@ -1558,6 +1558,50 @@ show_line_diagnostics({opts}, {bufnr}, {line_nr}, {client_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*

View File

@ -20,6 +20,7 @@ local lsp = {
buf = require'vim.lsp.buf';
diagnostic = require'vim.lsp.diagnostic';
codelens = require'vim.lsp.codelens';
util = util;
-- Allow raw RPC access.

View 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

View File

@ -187,6 +187,10 @@ M['textDocument/publishDiagnostics'] = function(...)
return require('vim.lsp.diagnostic').on_publish_diagnostics(...)
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
M['textDocument/references'] = function(_, _, result)
if not result then return end

View File

@ -154,6 +154,7 @@ CONFIG = {
'lsp.lua',
'buf.lua',
'diagnostic.lua',
'codelens.lua',
'handlers.lua',
'util.lua',
'log.lua',

View 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)