diff options
author | Josh Rahm <joshuarahm@gmail.com> | 2024-11-25 19:15:05 +0000 |
---|---|---|
committer | Josh Rahm <joshuarahm@gmail.com> | 2024-11-25 19:27:38 +0000 |
commit | c5d770d311841ea5230426cc4c868e8db27300a8 (patch) | |
tree | dd21f70127b4b8b5f109baefc8ecc5016f507c91 /runtime/lua/vim/lsp/client.lua | |
parent | 9be89f131f87608f224f0ee06d199fcd09d32176 (diff) | |
parent | 081beb3659bd6d8efc3e977a160b1e72becbd8a2 (diff) | |
download | rneovim-c5d770d311841ea5230426cc4c868e8db27300a8.tar.gz rneovim-c5d770d311841ea5230426cc4c868e8db27300a8.tar.bz2 rneovim-c5d770d311841ea5230426cc4c868e8db27300a8.zip |
Merge remote-tracking branch 'upstream/master' into mix_20240309
Diffstat (limited to 'runtime/lua/vim/lsp/client.lua')
-rw-r--r-- | runtime/lua/vim/lsp/client.lua | 255 |
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 }, |