aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/lsp/client.lua
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/lua/vim/lsp/client.lua')
-rw-r--r--runtime/lua/vim/lsp/client.lua255
1 files changed, 195 insertions, 60 deletions
diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua
index e3c82f4169..11ecb87507 100644
--- a/runtime/lua/vim/lsp/client.lua
+++ b/runtime/lua/vim/lsp/client.lua
@@ -91,7 +91,7 @@ local validate = vim.validate
--- (default: client-id)
--- @field name? string
---
---- Language ID as string. Defaults to the filetype.
+--- Language ID as string. Defaults to the buffer filetype.
--- @field get_language_id? fun(bufnr: integer, filetype: string): string
---
--- The encoding that the LSP server expects. Client does not verify this is correct.
@@ -216,6 +216,7 @@ local validate = vim.validate
---
--- The capabilities provided by the client (editor or tool)
--- @field capabilities lsp.ClientCapabilities
+--- @field private registrations table<string,lsp.Registration[]>
--- @field dynamic_capabilities lsp.DynamicCapabilities
---
--- Sends a request to the server.
@@ -291,7 +292,7 @@ local client_index = 0
--- @param filename (string) path to check
--- @return boolean # true if {filename} exists and is a directory, false otherwise
local function is_dir(filename)
- validate({ filename = { filename, 's' } })
+ validate('filename', filename, 'string')
local stat = uv.fs_stat(filename)
return stat and stat.type == 'directory' or false
end
@@ -312,9 +313,7 @@ local valid_encodings = {
--- @param encoding string? Encoding to normalize
--- @return string # normalized encoding name
local function validate_encoding(encoding)
- validate({
- encoding = { encoding, 's', true },
- })
+ validate('encoding', encoding, 'string', true)
if not encoding then
return valid_encodings.UTF16
end
@@ -350,27 +349,23 @@ end
--- Validates a client configuration as given to |vim.lsp.start_client()|.
--- @param config vim.lsp.ClientConfig
local function validate_config(config)
- validate({
- config = { config, 't' },
- })
- validate({
- handlers = { config.handlers, 't', true },
- capabilities = { config.capabilities, 't', true },
- cmd_cwd = { config.cmd_cwd, optional_validator(is_dir), 'directory' },
- cmd_env = { config.cmd_env, 't', true },
- detached = { config.detached, 'b', true },
- name = { config.name, 's', true },
- on_error = { config.on_error, 'f', true },
- on_exit = { config.on_exit, { 'f', 't' }, true },
- on_init = { config.on_init, { 'f', 't' }, true },
- on_attach = { config.on_attach, { 'f', 't' }, true },
- settings = { config.settings, 't', true },
- commands = { config.commands, 't', true },
- before_init = { config.before_init, { 'f', 't' }, true },
- offset_encoding = { config.offset_encoding, 's', true },
- flags = { config.flags, 't', true },
- get_language_id = { config.get_language_id, 'f', true },
- })
+ validate('config', config, 'table')
+ validate('handlers', config.handlers, 'table', true)
+ validate('capabilities', config.capabilities, 'table', true)
+ validate('cmd_cwd', config.cmd_cwd, optional_validator(is_dir), 'directory')
+ validate('cmd_env', config.cmd_env, 'table', true)
+ validate('detached', config.detached, 'boolean', true)
+ validate('name', config.name, 'string', true)
+ validate('on_error', config.on_error, 'function', true)
+ validate('on_exit', config.on_exit, { 'function', 'table' }, true)
+ validate('on_init', config.on_init, { 'function', 'table' }, true)
+ validate('on_attach', config.on_attach, { 'function', 'table' }, true)
+ validate('settings', config.settings, 'table', true)
+ validate('commands', config.commands, 'table', true)
+ validate('before_init', config.before_init, { 'function', 'table' }, true)
+ validate('offset_encoding', config.offset_encoding, 'string', true)
+ validate('flags', config.flags, 'table', true)
+ validate('get_language_id', config.get_language_id, 'function', true)
assert(
(
@@ -409,18 +404,16 @@ local function get_name(id, config)
return tostring(id)
end
---- @param workspace_folders lsp.WorkspaceFolder[]?
---- @param root_dir string?
+--- @param workspace_folders string|lsp.WorkspaceFolder[]?
--- @return lsp.WorkspaceFolder[]?
-local function get_workspace_folders(workspace_folders, root_dir)
- if workspace_folders then
+local function get_workspace_folders(workspace_folders)
+ if type(workspace_folders) == 'table' then
return workspace_folders
- end
- if root_dir then
+ elseif type(workspace_folders) == 'string' then
return {
{
- uri = vim.uri_from_fname(root_dir),
- name = root_dir,
+ uri = vim.uri_from_fname(workspace_folders),
+ name = workspace_folders,
},
}
end
@@ -457,13 +450,13 @@ function Client.create(config)
requests = {},
attached_buffers = {},
server_capabilities = {},
- dynamic_capabilities = lsp._dynamic.new(id),
+ registrations = {},
commands = config.commands or {},
settings = config.settings or {},
flags = config.flags or {},
get_language_id = config.get_language_id or default_get_language_id,
capabilities = config.capabilities or lsp.protocol.make_client_capabilities(),
- workspace_folders = get_workspace_folders(config.workspace_folders, config.root_dir),
+ workspace_folders = get_workspace_folders(config.workspace_folders or config.root_dir),
root_dir = config.root_dir,
_before_init_cb = config.before_init,
_on_init_cbs = ensure_list(config.on_init),
@@ -484,6 +477,28 @@ function Client.create(config)
messages = { name = name, messages = {}, progress = {}, status = {} },
}
+ --- @class lsp.DynamicCapabilities
+ --- @nodoc
+ self.dynamic_capabilities = {
+ capabilities = self.registrations,
+ client_id = id,
+ register = function(_, registrations)
+ return self:_register_dynamic(registrations)
+ end,
+ unregister = function(_, unregistrations)
+ return self:_unregister_dynamic(unregistrations)
+ end,
+ get = function(_, method, opts)
+ return self:_get_registration(method, opts and opts.bufnr)
+ end,
+ supports_registration = function(_, method)
+ return self:_supports_registration(method)
+ end,
+ supports = function(_, method, opts)
+ return self:_get_registration(method, opts and opts.bufnr) ~= nil
+ end,
+ }
+
self.request = method_wrapper(self, Client._request)
self.request_sync = method_wrapper(self, Client._request_sync)
self.notify = method_wrapper(self, Client._notify)
@@ -640,7 +655,7 @@ end
--- @param bufnr (integer|nil) Buffer number to resolve. Defaults to current buffer
--- @return integer bufnr
local function resolve_bufnr(bufnr)
- validate({ bufnr = { bufnr, 'n', true } })
+ validate('bufnr', bufnr, 'number', true)
if bufnr == nil or bufnr == 0 then
return api.nvim_get_current_buf()
end
@@ -806,7 +821,7 @@ end
--- @return boolean status true if notification was successful. false otherwise
--- @see |vim.lsp.client.notify()|
function Client:_cancel_request(id)
- validate({ id = { id, 'n' } })
+ validate('id', id, 'number')
local request = self.requests[id]
if request and request.type == 'pending' then
request.type = 'cancel'
@@ -852,6 +867,105 @@ function Client:_stop(force)
end)
end
+--- Get options for a method that is registered dynamically.
+--- @param method string
+function Client:_supports_registration(method)
+ local capability = vim.tbl_get(self.capabilities, unpack(vim.split(method, '/')))
+ return type(capability) == 'table' and capability.dynamicRegistration
+end
+
+--- @private
+--- @param registrations lsp.Registration[]
+function Client:_register_dynamic(registrations)
+ -- remove duplicates
+ self:_unregister_dynamic(registrations)
+ for _, reg in ipairs(registrations) do
+ local method = reg.method
+ if not self.registrations[method] then
+ self.registrations[method] = {}
+ end
+ table.insert(self.registrations[method], reg)
+ end
+end
+
+--- @param registrations lsp.Registration[]
+function Client:_register(registrations)
+ self:_register_dynamic(registrations)
+
+ local unsupported = {} --- @type string[]
+
+ for _, reg in ipairs(registrations) do
+ local method = reg.method
+ if method == ms.workspace_didChangeWatchedFiles then
+ vim.lsp._watchfiles.register(reg, self.id)
+ elseif not self:_supports_registration(method) then
+ unsupported[#unsupported + 1] = method
+ end
+ end
+
+ if #unsupported > 0 then
+ local warning_tpl = 'The language server %s triggers a registerCapability '
+ .. 'handler for %s despite dynamicRegistration set to false. '
+ .. 'Report upstream, this warning is harmless'
+ log.warn(string.format(warning_tpl, self.name, table.concat(unsupported, ', ')))
+ end
+end
+
+--- @private
+--- @param unregistrations lsp.Unregistration[]
+function Client:_unregister_dynamic(unregistrations)
+ for _, unreg in ipairs(unregistrations) do
+ local sreg = self.registrations[unreg.method]
+ -- Unegister dynamic capability
+ for i, reg in ipairs(sreg or {}) do
+ if reg.id == unreg.id then
+ table.remove(sreg, i)
+ break
+ end
+ end
+ end
+end
+
+--- @param unregistrations lsp.Unregistration[]
+function Client:_unregister(unregistrations)
+ self:_unregister_dynamic(unregistrations)
+ for _, unreg in ipairs(unregistrations) do
+ if unreg.method == ms.workspace_didChangeWatchedFiles then
+ vim.lsp._watchfiles.unregister(unreg, self.id)
+ end
+ end
+end
+
+--- @private
+function Client:_get_language_id(bufnr)
+ return self.get_language_id(bufnr, vim.bo[bufnr].filetype)
+end
+
+--- @param method string
+--- @param bufnr? integer
+--- @return lsp.Registration?
+function Client:_get_registration(method, bufnr)
+ bufnr = bufnr or vim.api.nvim_get_current_buf()
+ for _, reg in ipairs(self.registrations[method] or {}) do
+ if not reg.registerOptions or not reg.registerOptions.documentSelector then
+ return reg
+ end
+ local documentSelector = reg.registerOptions.documentSelector
+ local language = self:_get_language_id(bufnr)
+ local uri = vim.uri_from_bufnr(bufnr)
+ local fname = vim.uri_to_fname(uri)
+ for _, filter in ipairs(documentSelector) do
+ if
+ not (filter.language and language ~= filter.language)
+ and not (filter.scheme and not vim.startswith(uri, filter.scheme .. ':'))
+ and not (filter.pattern and not vim.glob.to_lpeg(filter.pattern):match(fname))
+ then
+ return reg
+ end
+ end
+ end
+end
+
--- @private
--- Checks whether a client is stopped.
---
@@ -865,10 +979,9 @@ end
--- or via workspace/executeCommand (if supported by the server)
---
--- @param command lsp.Command
---- @param context? {bufnr: integer}
+--- @param context? {bufnr?: integer}
--- @param handler? lsp.Handler only called if a server command
---- @param on_unsupported? function handler invoked when the command is not supported by the client.
-function Client:_exec_cmd(command, context, handler, on_unsupported)
+function Client:exec_cmd(command, context, handler)
context = vim.deepcopy(context or {}, true) --[[@as lsp.HandlerContext]]
context.bufnr = context.bufnr or api.nvim_get_current_buf()
context.client_id = self.id
@@ -881,25 +994,23 @@ function Client:_exec_cmd(command, context, handler, on_unsupported)
local command_provider = self.server_capabilities.executeCommandProvider
local commands = type(command_provider) == 'table' and command_provider.commands or {}
+
if not vim.list_contains(commands, cmdname) then
- if on_unsupported then
- on_unsupported()
- else
- vim.notify_once(
- string.format(
- 'Language server `%s` does not support command `%s`. This command may require a client extension.',
- self.name,
- cmdname
- ),
- vim.log.levels.WARN
- )
- end
+ vim.notify_once(
+ string.format(
+ 'Language server `%s` does not support command `%s`. This command may require a client extension.',
+ self.name,
+ cmdname
+ ),
+ vim.log.levels.WARN
+ )
return
end
-- Not using command directly to exclude extra properties,
-- see https://github.com/python-lsp/python-lsp-server/issues/146
+ --- @type lsp.ExecuteCommandParams
local params = {
- command = command.command,
+ command = cmdname,
arguments = command.arguments,
}
self.request(ms.workspace_executeCommand, params, handler, context.bufnr)
@@ -917,12 +1028,11 @@ function Client:_text_document_did_open_handler(bufnr)
return
end
- local filetype = vim.bo[bufnr].filetype
self.notify(ms.textDocument_didOpen, {
textDocument = {
version = lsp.util.buf_versions[bufnr],
uri = vim.uri_from_bufnr(bufnr),
- languageId = self.get_language_id(bufnr, filetype),
+ languageId = self:_get_language_id(bufnr),
text = lsp._buf_get_full_text(bufnr),
},
})
@@ -987,12 +1097,37 @@ function Client:_supports_method(method, opts)
if vim.tbl_get(self.server_capabilities, unpack(required_capability)) then
return true
end
- if self.dynamic_capabilities:supports_registration(method) then
- return self.dynamic_capabilities:supports(method, opts)
+
+ local rmethod = lsp._resolve_to_request[method]
+ if rmethod then
+ if self:_supports_registration(rmethod) then
+ local reg = self:_get_registration(rmethod, opts and opts.bufnr)
+ return vim.tbl_get(reg or {}, 'registerOptions', 'resolveProvider') or false
+ end
+ else
+ if self:_supports_registration(method) then
+ return self:_get_registration(method, opts and opts.bufnr) ~= nil
+ end
end
return false
end
+--- Get options for a method that is registered dynamically.
+--- @param method string
+--- @param bufnr? integer
+--- @return lsp.LSPAny?
+function Client:_get_registration_options(method, bufnr)
+ if not self:_supports_registration(method) then
+ return
+ end
+
+ local reg = self:_get_registration(method, bufnr)
+
+ if reg then
+ return reg.registerOptions
+ end
+end
+
--- @private
--- Handles a notification sent by an LSP server by invoking the
--- corresponding handler.
@@ -1070,7 +1205,7 @@ function Client:_add_workspace_folder(dir)
end
end
- local wf = assert(get_workspace_folders(nil, dir))
+ local wf = assert(get_workspace_folders(dir))
self:_notify(ms.workspace_didChangeWorkspaceFolders, {
event = { added = wf, removed = {} },
@@ -1085,7 +1220,7 @@ end
--- Remove a directory to the workspace folders.
--- @param dir string?
function Client:_remove_workspace_folder(dir)
- local wf = assert(get_workspace_folders(nil, dir))
+ local wf = assert(get_workspace_folders(dir))
self:_notify(ms.workspace_didChangeWorkspaceFolders, {
event = { added = {}, removed = wf },