mirror of
https://github.com/neovim/neovim.git
synced 2024-09-17 20:58:20 -04:00
feat(lsp): initial support for dynamic capabilities (#23681)
- `client.dynamic_capabilities` is an object that tracks client register/unregister - `client.supports_method` will additionally check if a dynamic capability supports the method, taking document filters into account. But only if the client enabled `dynamicRegistration` for the capability - updated the default client capabilities to include dynamicRegistration for: - formatting - rangeFormatting - hover - codeAction - hover - rename
This commit is contained in:
parent
e41b2e34b4
commit
ddd92a70d2
@ -36,6 +36,7 @@ ADDED FEATURES *news-added*
|
||||
|
||||
The following new APIs or features were added.
|
||||
|
||||
• Dynamic registration of LSP capabilities. An implication of this change is that checking a client's `server_capabilities` is no longer a sufficient indicator to see if a server supports a feature. Instead use `client.supports_method(<method>)`. It considers both the dynamic capabilities and static `server_capabilities`.
|
||||
• |vim.iter()| provides a generic iterator interface for tables and Lua
|
||||
iterators |luaref-in|.
|
||||
|
||||
|
@ -50,6 +50,7 @@ lsp._request_name_to_capability = {
|
||||
['textDocument/codeAction'] = { 'codeActionProvider' },
|
||||
['textDocument/codeLens'] = { 'codeLensProvider' },
|
||||
['codeLens/resolve'] = { 'codeLensProvider', 'resolveProvider' },
|
||||
['codeAction/resolve'] = { 'codeActionProvider', 'resolveProvider' },
|
||||
['workspace/executeCommand'] = { 'executeCommandProvider' },
|
||||
['workspace/symbol'] = { 'workspaceSymbolProvider' },
|
||||
['textDocument/references'] = { 'referencesProvider' },
|
||||
@ -886,6 +887,47 @@ function lsp.start(config, opts)
|
||||
return client_id
|
||||
end
|
||||
|
||||
---@private
|
||||
-- Determines whether the given option can be set by `set_defaults`.
|
||||
local function is_empty_or_default(bufnr, option)
|
||||
if vim.bo[bufnr][option] == '' then
|
||||
return true
|
||||
end
|
||||
|
||||
local info = vim.api.nvim_get_option_info2(option, { buf = bufnr })
|
||||
local scriptinfo = vim.tbl_filter(function(e)
|
||||
return e.sid == info.last_set_sid
|
||||
end, vim.fn.getscriptinfo())
|
||||
|
||||
if #scriptinfo ~= 1 then
|
||||
return false
|
||||
end
|
||||
|
||||
return vim.startswith(scriptinfo[1].name, vim.fn.expand('$VIMRUNTIME'))
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param client lsp.Client
|
||||
function lsp._set_defaults(client, bufnr)
|
||||
if
|
||||
client.supports_method('textDocument/definition') and is_empty_or_default(bufnr, 'tagfunc')
|
||||
then
|
||||
vim.bo[bufnr].tagfunc = 'v:lua.vim.lsp.tagfunc'
|
||||
end
|
||||
if
|
||||
client.supports_method('textDocument/completion') and is_empty_or_default(bufnr, 'omnifunc')
|
||||
then
|
||||
vim.bo[bufnr].omnifunc = 'v:lua.vim.lsp.omnifunc'
|
||||
end
|
||||
if
|
||||
client.supports_method('textDocument/rangeFormatting')
|
||||
and is_empty_or_default(bufnr, 'formatprg')
|
||||
and is_empty_or_default(bufnr, 'formatexpr')
|
||||
then
|
||||
vim.bo[bufnr].formatexpr = 'v:lua.vim.lsp.formatexpr()'
|
||||
end
|
||||
end
|
||||
|
||||
-- FIXME: DOC: Currently all methods on the `vim.lsp.client` object are
|
||||
-- documented twice: Here, and on the methods themselves (e.g.
|
||||
-- `client.request()`). This is a workaround for the vimdoc generator script
|
||||
@ -1090,43 +1132,6 @@ function lsp.start_client(config)
|
||||
end
|
||||
end
|
||||
|
||||
---@private
|
||||
-- Determines whether the given option can be set by `set_defaults`.
|
||||
local function is_empty_or_default(bufnr, option)
|
||||
if vim.bo[bufnr][option] == '' then
|
||||
return true
|
||||
end
|
||||
|
||||
local info = vim.api.nvim_get_option_info2(option, { buf = bufnr })
|
||||
local scriptinfo = vim.tbl_filter(function(e)
|
||||
return e.sid == info.last_set_sid
|
||||
end, vim.fn.getscriptinfo())
|
||||
|
||||
if #scriptinfo ~= 1 then
|
||||
return false
|
||||
end
|
||||
|
||||
return vim.startswith(scriptinfo[1].name, vim.fn.expand('$VIMRUNTIME'))
|
||||
end
|
||||
|
||||
---@private
|
||||
local function set_defaults(client, bufnr)
|
||||
local capabilities = client.server_capabilities
|
||||
if capabilities.definitionProvider and is_empty_or_default(bufnr, 'tagfunc') then
|
||||
vim.bo[bufnr].tagfunc = 'v:lua.vim.lsp.tagfunc'
|
||||
end
|
||||
if capabilities.completionProvider and is_empty_or_default(bufnr, 'omnifunc') then
|
||||
vim.bo[bufnr].omnifunc = 'v:lua.vim.lsp.omnifunc'
|
||||
end
|
||||
if
|
||||
capabilities.documentRangeFormattingProvider
|
||||
and is_empty_or_default(bufnr, 'formatprg')
|
||||
and is_empty_or_default(bufnr, 'formatexpr')
|
||||
then
|
||||
vim.bo[bufnr].formatexpr = 'v:lua.vim.lsp.formatexpr()'
|
||||
end
|
||||
end
|
||||
|
||||
---@private
|
||||
--- Reset defaults set by `set_defaults`.
|
||||
--- Must only be called if the last client attached to a buffer exits.
|
||||
@ -1228,7 +1233,9 @@ function lsp.start_client(config)
|
||||
requests = {},
|
||||
-- for $/progress report
|
||||
messages = { name = name, messages = {}, progress = {}, status = {} },
|
||||
dynamic_capabilities = require('vim.lsp._dynamic').new(client_id),
|
||||
}
|
||||
client.config.capabilities = config.capabilities or protocol.make_client_capabilities()
|
||||
|
||||
-- Store the uninitialized_clients for cleanup in case we exit before initialize finishes.
|
||||
uninitialized_clients[client_id] = client
|
||||
@ -1291,7 +1298,7 @@ function lsp.start_client(config)
|
||||
-- User provided initialization options.
|
||||
initializationOptions = config.init_options,
|
||||
-- The capabilities provided by the client (editor or tool)
|
||||
capabilities = config.capabilities or protocol.make_client_capabilities(),
|
||||
capabilities = config.capabilities,
|
||||
-- The initial trace setting. If omitted trace is disabled ("off").
|
||||
-- trace = "off" | "messages" | "verbose";
|
||||
trace = valid_traces[config.trace] or 'off',
|
||||
@ -1300,6 +1307,26 @@ function lsp.start_client(config)
|
||||
-- TODO(ashkan) handle errors here.
|
||||
pcall(config.before_init, initialize_params, config)
|
||||
end
|
||||
|
||||
--- @param method string
|
||||
--- @param opts? {bufnr?: number}
|
||||
client.supports_method = function(method, opts)
|
||||
opts = opts or {}
|
||||
local required_capability = lsp._request_name_to_capability[method]
|
||||
-- if we don't know about the method, assume that the client supports it.
|
||||
if not required_capability then
|
||||
return true
|
||||
end
|
||||
if vim.tbl_get(client.server_capabilities or {}, unpack(required_capability)) then
|
||||
return true
|
||||
else
|
||||
if client.dynamic_capabilities:supports_registration(method) then
|
||||
return client.dynamic_capabilities:supports(method, opts)
|
||||
end
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
local _ = log.trace() and log.trace(log_prefix, 'initialize_params', initialize_params)
|
||||
rpc.request('initialize', initialize_params, function(init_err, result)
|
||||
assert(not init_err, tostring(init_err))
|
||||
@ -1314,18 +1341,6 @@ function lsp.start_client(config)
|
||||
client.server_capabilities =
|
||||
assert(result.capabilities, "initialize result doesn't contain capabilities")
|
||||
client.server_capabilities = protocol.resolve_capabilities(client.server_capabilities)
|
||||
client.supports_method = function(method)
|
||||
local required_capability = lsp._request_name_to_capability[method]
|
||||
-- if we don't know about the method, assume that the client supports it.
|
||||
if not required_capability then
|
||||
return true
|
||||
end
|
||||
if vim.tbl_get(client.server_capabilities, unpack(required_capability)) then
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
if next(config.settings) then
|
||||
client.notify('workspace/didChangeConfiguration', { settings = config.settings })
|
||||
@ -1522,7 +1537,7 @@ function lsp.start_client(config)
|
||||
function client._on_attach(bufnr)
|
||||
text_document_did_open_handler(bufnr, client)
|
||||
|
||||
set_defaults(client, bufnr)
|
||||
lsp._set_defaults(client, bufnr)
|
||||
|
||||
nvim_exec_autocmds('LspAttach', {
|
||||
buffer = bufnr,
|
||||
@ -1946,7 +1961,7 @@ function lsp.buf_request(bufnr, method, params, handler)
|
||||
local supported_clients = {}
|
||||
local method_supported = false
|
||||
for_each_buffer_client(bufnr, function(client, client_id)
|
||||
if client.supports_method(method) then
|
||||
if client.supports_method(method, { bufnr = bufnr }) then
|
||||
method_supported = true
|
||||
table.insert(supported_clients, client_id)
|
||||
end
|
||||
@ -2002,7 +2017,7 @@ function lsp.buf_request_all(bufnr, method, params, callback)
|
||||
|
||||
local set_expected_result_count = once(function()
|
||||
for_each_buffer_client(bufnr, function(client)
|
||||
if client.supports_method(method) then
|
||||
if client.supports_method(method, { bufnr = bufnr }) then
|
||||
expected_result_count = expected_result_count + 1
|
||||
end
|
||||
end)
|
||||
|
109
runtime/lua/vim/lsp/_dynamic.lua
Normal file
109
runtime/lua/vim/lsp/_dynamic.lua
Normal file
@ -0,0 +1,109 @@
|
||||
local wf = require('vim.lsp._watchfiles')
|
||||
|
||||
--- @class lsp.DynamicCapabilities
|
||||
--- @field capabilities table<string, lsp.Registration[]>
|
||||
--- @field client_id number
|
||||
local M = {}
|
||||
|
||||
--- @param client_id number
|
||||
function M.new(client_id)
|
||||
return setmetatable({
|
||||
capabilities = {},
|
||||
client_id = client_id,
|
||||
}, { __index = M })
|
||||
end
|
||||
|
||||
function M:supports_registration(method)
|
||||
local client = vim.lsp.get_client_by_id(self.client_id)
|
||||
if not client then
|
||||
return false
|
||||
end
|
||||
local capability = vim.tbl_get(client.config.capabilities, unpack(vim.split(method, '/')))
|
||||
return type(capability) == 'table' and capability.dynamicRegistration
|
||||
end
|
||||
|
||||
--- @param registrations lsp.Registration[]
|
||||
--- @private
|
||||
function M:register(registrations)
|
||||
-- remove duplicates
|
||||
self:unregister(registrations)
|
||||
for _, reg in ipairs(registrations) do
|
||||
local method = reg.method
|
||||
if not self.capabilities[method] then
|
||||
self.capabilities[method] = {}
|
||||
end
|
||||
table.insert(self.capabilities[method], reg)
|
||||
end
|
||||
end
|
||||
|
||||
--- @param unregisterations lsp.Unregistration[]
|
||||
--- @private
|
||||
function M:unregister(unregisterations)
|
||||
for _, unreg in ipairs(unregisterations) do
|
||||
local method = unreg.method
|
||||
if not self.capabilities[method] then
|
||||
return
|
||||
end
|
||||
local id = unreg.id
|
||||
for i, reg in ipairs(self.capabilities[method]) do
|
||||
if reg.id == id then
|
||||
table.remove(self.capabilities[method], i)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @param method string
|
||||
--- @param opts? {bufnr?: number}
|
||||
--- @return lsp.Registration? (table|nil) the registration if found
|
||||
--- @private
|
||||
function M:get(method, opts)
|
||||
opts = opts or {}
|
||||
opts.bufnr = opts.bufnr or vim.api.nvim_get_current_buf()
|
||||
for _, reg in ipairs(self.capabilities[method] or {}) do
|
||||
if not reg.registerOptions then
|
||||
return reg
|
||||
end
|
||||
local documentSelector = reg.registerOptions.documentSelector
|
||||
if not documentSelector then
|
||||
return reg
|
||||
end
|
||||
if M.match(opts.bufnr, documentSelector) then
|
||||
return reg
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @param method string
|
||||
--- @param opts? {bufnr?: number}
|
||||
--- @private
|
||||
function M:supports(method, opts)
|
||||
return self:get(method, opts) ~= nil
|
||||
end
|
||||
|
||||
--- @param bufnr number
|
||||
--- @param documentSelector lsp.DocumentSelector
|
||||
--- @private
|
||||
function M.match(bufnr, documentSelector)
|
||||
local ft = vim.bo[bufnr].filetype
|
||||
local uri = vim.uri_from_bufnr(bufnr)
|
||||
local fname = vim.uri_to_fname(uri)
|
||||
for _, filter in ipairs(documentSelector) do
|
||||
local matches = true
|
||||
if filter.language and ft ~= filter.language then
|
||||
matches = false
|
||||
end
|
||||
if matches and filter.scheme and not vim.startswith(uri, filter.scheme .. ':') then
|
||||
matches = false
|
||||
end
|
||||
if matches and filter.pattern and not wf._match(filter.pattern, fname) then
|
||||
matches = false
|
||||
end
|
||||
if matches then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
@ -683,11 +683,7 @@ local function on_code_action_results(results, ctx, options)
|
||||
--
|
||||
local client = vim.lsp.get_client_by_id(action_tuple[1])
|
||||
local action = action_tuple[2]
|
||||
if
|
||||
not action.edit
|
||||
and client
|
||||
and vim.tbl_get(client.server_capabilities, 'codeActionProvider', 'resolveProvider')
|
||||
then
|
||||
if not action.edit and client and client.supports_method('codeAction/resolve') then
|
||||
client.request('codeAction/resolve', action, function(err, resolved_action)
|
||||
if err then
|
||||
vim.notify(err.code .. ': ' .. err.message, vim.log.levels.ERROR)
|
||||
|
@ -118,22 +118,30 @@ end
|
||||
|
||||
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#client_registerCapability
|
||||
M['client/registerCapability'] = function(_, result, ctx)
|
||||
local log_unsupported = false
|
||||
local client_id = ctx.client_id
|
||||
---@type lsp.Client
|
||||
local client = vim.lsp.get_client_by_id(client_id)
|
||||
|
||||
client.dynamic_capabilities:register(result.registrations)
|
||||
for bufnr, _ in ipairs(client.attached_buffers) do
|
||||
vim.lsp._set_defaults(client, bufnr)
|
||||
end
|
||||
|
||||
---@type string[]
|
||||
local unsupported = {}
|
||||
for _, reg in ipairs(result.registrations) do
|
||||
if reg.method == 'workspace/didChangeWatchedFiles' then
|
||||
require('vim.lsp._watchfiles').register(reg, ctx)
|
||||
else
|
||||
log_unsupported = true
|
||||
elseif not client.dynamic_capabilities:supports_registration(reg.method) then
|
||||
unsupported[#unsupported + 1] = reg.method
|
||||
end
|
||||
end
|
||||
if log_unsupported then
|
||||
local client_id = ctx.client_id
|
||||
if #unsupported > 0 then
|
||||
local warning_tpl = 'The language server %s triggers a registerCapability '
|
||||
.. 'handler despite dynamicRegistration set to false. '
|
||||
.. 'handler for %s despite dynamicRegistration set to false. '
|
||||
.. 'Report upstream, this warning is harmless'
|
||||
local client = vim.lsp.get_client_by_id(client_id)
|
||||
local client_name = client and client.name or string.format('id=%d', client_id)
|
||||
local warning = string.format(warning_tpl, client_name)
|
||||
local warning = string.format(warning_tpl, client_name, table.concat(unsupported, ', '))
|
||||
log.warn(warning)
|
||||
end
|
||||
return vim.NIL
|
||||
@ -141,6 +149,10 @@ end
|
||||
|
||||
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#client_unregisterCapability
|
||||
M['client/unregisterCapability'] = function(_, result, ctx)
|
||||
local client_id = ctx.client_id
|
||||
local client = vim.lsp.get_client_by_id(client_id)
|
||||
client.dynamic_capabilities:unregister(result.unregisterations)
|
||||
|
||||
for _, unreg in ipairs(result.unregisterations) do
|
||||
if unreg.method == 'workspace/didChangeWatchedFiles' then
|
||||
require('vim.lsp._watchfiles').unregister(unreg, ctx)
|
||||
|
@ -697,7 +697,7 @@ function protocol.make_client_capabilities()
|
||||
didSave = true,
|
||||
},
|
||||
codeAction = {
|
||||
dynamicRegistration = false,
|
||||
dynamicRegistration = true,
|
||||
|
||||
codeActionLiteralSupport = {
|
||||
codeActionKind = {
|
||||
@ -714,6 +714,12 @@ function protocol.make_client_capabilities()
|
||||
properties = { 'edit' },
|
||||
},
|
||||
},
|
||||
formatting = {
|
||||
dynamicRegistration = true,
|
||||
},
|
||||
rangeFormatting = {
|
||||
dynamicRegistration = true,
|
||||
},
|
||||
completion = {
|
||||
dynamicRegistration = false,
|
||||
completionItem = {
|
||||
@ -747,6 +753,7 @@ function protocol.make_client_capabilities()
|
||||
},
|
||||
definition = {
|
||||
linkSupport = true,
|
||||
dynamicRegistration = true,
|
||||
},
|
||||
implementation = {
|
||||
linkSupport = true,
|
||||
@ -755,7 +762,7 @@ function protocol.make_client_capabilities()
|
||||
linkSupport = true,
|
||||
},
|
||||
hover = {
|
||||
dynamicRegistration = false,
|
||||
dynamicRegistration = true,
|
||||
contentFormat = { protocol.MarkupKind.Markdown, protocol.MarkupKind.PlainText },
|
||||
},
|
||||
signatureHelp = {
|
||||
@ -790,7 +797,7 @@ function protocol.make_client_capabilities()
|
||||
hierarchicalDocumentSymbolSupport = true,
|
||||
},
|
||||
rename = {
|
||||
dynamicRegistration = false,
|
||||
dynamicRegistration = true,
|
||||
prepareSupport = true,
|
||||
},
|
||||
publishDiagnostics = {
|
||||
|
@ -35,3 +35,31 @@
|
||||
---@field source string
|
||||
---@field tags? lsp.DiagnosticTag[]
|
||||
---@field relatedInformation DiagnosticRelatedInformation[]
|
||||
|
||||
--- @class lsp.DocumentFilter
|
||||
--- @field language? string
|
||||
--- @field scheme? string
|
||||
--- @field pattern? string
|
||||
|
||||
--- @alias lsp.DocumentSelector lsp.DocumentFilter[]
|
||||
|
||||
--- @alias lsp.RegisterOptions any | lsp.StaticRegistrationOptions | lsp.TextDocumentRegistrationOptions
|
||||
|
||||
--- @class lsp.Registration
|
||||
--- @field id string
|
||||
--- @field method string
|
||||
--- @field registerOptions? lsp.RegisterOptions
|
||||
|
||||
--- @alias lsp.RegistrationParams {registrations: lsp.Registration[]}
|
||||
|
||||
--- @class lsp.StaticRegistrationOptions
|
||||
--- @field id? string
|
||||
|
||||
--- @class lsp.TextDocumentRegistrationOptions
|
||||
--- @field documentSelector? lsp.DocumentSelector
|
||||
|
||||
--- @class lsp.Unregistration
|
||||
--- @field id string
|
||||
--- @field method string
|
||||
|
||||
--- @alias lsp.UnregistrationParams {unregisterations: lsp.Unregistration[]}
|
||||
|
@ -3765,6 +3765,96 @@ describe('LSP', function()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('#dynamic vim.lsp._dynamic', function()
|
||||
it('supports dynamic registration', function()
|
||||
local root_dir = helpers.tmpname()
|
||||
os.remove(root_dir)
|
||||
mkdir(root_dir)
|
||||
local tmpfile = root_dir .. '/dynamic.foo'
|
||||
local file = io.open(tmpfile, 'w')
|
||||
file:close()
|
||||
|
||||
exec_lua(create_server_definition)
|
||||
local result = exec_lua([[
|
||||
local root_dir, tmpfile = ...
|
||||
|
||||
local server = _create_server()
|
||||
local client_id = vim.lsp.start({
|
||||
name = 'dynamic-test',
|
||||
cmd = server.cmd,
|
||||
root_dir = root_dir,
|
||||
capabilities = {
|
||||
textDocument = {
|
||||
formatting = {
|
||||
dynamicRegistration = true,
|
||||
},
|
||||
rangeFormatting = {
|
||||
dynamicRegistration = true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
local expected_messages = 2 -- initialize, initialized
|
||||
|
||||
vim.lsp.handlers['client/registerCapability'](nil, {
|
||||
registrations = {
|
||||
{
|
||||
id = 'formatting',
|
||||
method = 'textDocument/formatting',
|
||||
registerOptions = {
|
||||
documentSelector = {{
|
||||
pattern = root_dir .. '/*.foo',
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, { client_id = client_id })
|
||||
|
||||
vim.lsp.handlers['client/registerCapability'](nil, {
|
||||
registrations = {
|
||||
{
|
||||
id = 'range-formatting',
|
||||
method = 'textDocument/rangeFormatting',
|
||||
},
|
||||
},
|
||||
}, { client_id = client_id })
|
||||
|
||||
vim.lsp.handlers['client/registerCapability'](nil, {
|
||||
registrations = {
|
||||
{
|
||||
id = 'completion',
|
||||
method = 'textDocument/completion',
|
||||
},
|
||||
},
|
||||
}, { client_id = client_id })
|
||||
|
||||
local result = {}
|
||||
local function check(method, fname)
|
||||
local bufnr = fname and vim.fn.bufadd(fname) or nil
|
||||
local client = vim.lsp.get_client_by_id(client_id)
|
||||
result[#result + 1] = {method = method, fname = fname, supported = client.supports_method(method, {bufnr = bufnr})}
|
||||
end
|
||||
|
||||
|
||||
check("textDocument/formatting")
|
||||
check("textDocument/formatting", tmpfile)
|
||||
check("textDocument/rangeFormatting")
|
||||
check("textDocument/rangeFormatting", tmpfile)
|
||||
check("textDocument/completion")
|
||||
|
||||
return result
|
||||
]], root_dir, tmpfile)
|
||||
|
||||
eq(5, #result)
|
||||
eq({method = 'textDocument/formatting', supported = false}, result[1])
|
||||
eq({method = 'textDocument/formatting', supported = true, fname = tmpfile}, result[2])
|
||||
eq({method = 'textDocument/rangeFormatting', supported = true}, result[3])
|
||||
eq({method = 'textDocument/rangeFormatting', supported = true, fname = tmpfile}, result[4])
|
||||
eq({method = 'textDocument/completion', supported = false}, result[5])
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('vim.lsp._watchfiles', function()
|
||||
it('sends notifications when files change', function()
|
||||
local root_dir = helpers.tmpname()
|
||||
|
Loading…
Reference in New Issue
Block a user