diff options
-rw-r--r-- | runtime/lua/vim/lsp.lua | 543 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/_dynamic.lua | 5 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/buf.lua | 2 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/client.lua | 663 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/codelens.lua | 2 | ||||
-rw-r--r-- | runtime/lua/vim/shared.lua | 1 |
6 files changed, 709 insertions, 507 deletions
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index d8d47a8464..dc50ab0267 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -15,6 +15,7 @@ local lsp = vim._defer_require('vim.lsp', { _tagfunc = ..., --- @module 'vim.lsp._tagfunc' _watchfiles = ..., --- @module 'vim.lsp._watchfiles' buf = ..., --- @module 'vim.lsp.buf' + client = ..., --- @module 'vim.lsp.client' codelens = ..., --- @module 'vim.lsp.codelens' diagnostic = ..., --- @module 'vim.lsp.diagnostic' handlers = ..., --- @module 'vim.lsp.handlers' @@ -259,7 +260,7 @@ end --- Validates a client configuration as given to |vim.lsp.start_client()|. --- ---@param config (lsp.ClientConfig) ----@return (string|fun(dispatchers:vim.rpc.Dispatchers):vim.lsp.rpc.PublicClient?) Command +---@return (string|fun(dispatchers:vim.lsp.rpc.Dispatchers):vim.lsp.rpc.PublicClient?) Command ---@return string[] Arguments ---@return string Encoding. local function validate_client_config(config) @@ -292,7 +293,7 @@ local function validate_client_config(config) 'flags.debounce_text_changes must be a number with the debounce time in milliseconds' ) - local cmd, cmd_args --- @type (string|fun(dispatchers:vim.rpc.Dispatchers):vim.lsp.rpc.PublicClient), string[] + local cmd, cmd_args --- @type (string|fun(dispatchers:vim.lsp.rpc.Dispatchers):vim.lsp.rpc.PublicClient), string[] local config_cmd = config.cmd if type(config_cmd) == 'function' then cmd = config_cmd @@ -341,42 +342,6 @@ local function once(fn) end end ---- Default handler for the 'textDocument/didOpen' LSP notification. ---- ----@param bufnr integer Number of the buffer, or 0 for current ----@param client lsp.Client Client object -local function text_document_did_open_handler(bufnr, client) - changetracking.init(client, bufnr) - if not vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'openClose') then - return - end - if not api.nvim_buf_is_loaded(bufnr) then - return - end - local filetype = vim.bo[bufnr].filetype - - local params = { - textDocument = { - version = 0, - uri = vim.uri_from_bufnr(bufnr), - languageId = client.config.get_language_id(bufnr, filetype), - text = lsp._buf_get_full_text(bufnr), - }, - } - client.notify(ms.textDocument_didOpen, params) - util.buf_versions[bufnr] = params.textDocument.version - - -- Next chance we get, we should re-do the diagnostics - vim.schedule(function() - -- Protect against a race where the buffer disappears - -- between `did_open_handler` and the scheduled function firing. - if api.nvim_buf_is_valid(bufnr) then - local namespace = vim.lsp.diagnostic.get_namespace(client.id) - vim.diagnostic.show(namespace, bufnr) - end - end) -end - -- FIXME: DOC: Shouldn't need to use a dummy function -- --- LSP client object. You can get an active client object via @@ -556,7 +521,9 @@ function lsp.status() local percentage = nil local messages = {} --- @type string[] for _, client in ipairs(vim.lsp.get_clients()) do + --- @diagnostic disable-next-line:no-unknown for progress in client.progress do + --- @cast progress {token: lsp.ProgressToken, value: lsp.LSPAny} local value = progress.value if type(value) == 'table' and value.kind then local message = value.message and (value.title .. ': ' .. value.message) or value.title @@ -655,6 +622,26 @@ end --- @field flags table --- @field root_dir string +--- Reset defaults set by `set_defaults`. +--- Must only be called if the last client attached to a buffer exits. +local function reset_defaults(bufnr) + if vim.bo[bufnr].tagfunc == 'v:lua.vim.lsp.tagfunc' then + vim.bo[bufnr].tagfunc = nil + end + if vim.bo[bufnr].omnifunc == 'v:lua.vim.lsp.omnifunc' then + vim.bo[bufnr].omnifunc = nil + end + if vim.bo[bufnr].formatexpr == 'v:lua.vim.lsp.formatexpr()' then + vim.bo[bufnr].formatexpr = nil + end + api.nvim_buf_call(bufnr, function() + local keymap = vim.fn.maparg('K', 'n', false, true) + if keymap and keymap.callback == vim.lsp.buf.hover then + vim.keymap.del('n', 'K', { buffer = bufnr }) + end + 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 @@ -875,26 +862,6 @@ function lsp.start_client(config) end end - --- Reset defaults set by `set_defaults`. - --- Must only be called if the last client attached to a buffer exits. - local function reset_defaults(bufnr) - if vim.bo[bufnr].tagfunc == 'v:lua.vim.lsp.tagfunc' then - vim.bo[bufnr].tagfunc = nil - end - if vim.bo[bufnr].omnifunc == 'v:lua.vim.lsp.omnifunc' then - vim.bo[bufnr].omnifunc = nil - end - if vim.bo[bufnr].formatexpr == 'v:lua.vim.lsp.formatexpr()' then - vim.bo[bufnr].formatexpr = nil - end - api.nvim_buf_call(bufnr, function() - local keymap = vim.fn.maparg('K', 'n', false, true) - if keymap and keymap.callback == vim.lsp.buf.hover then - vim.keymap.del('n', 'K', { buffer = bufnr }) - end - end) - end - ---@private --- Invoked on client exit. --- @@ -971,456 +938,26 @@ function lsp.start_client(config) return end - ---@class lsp.Client - local client = { - id = client_id, - name = name, - rpc = rpc, - offset_encoding = offset_encoding, - config = config, - attached_buffers = {}, --- @type table<integer,true> - - handlers = handlers, - --- @type table<string,function> - commands = config.commands or {}, + config.capabilities = config.capabilities or protocol.make_client_capabilities() - --- @type table<integer,{ type: string, bufnr: integer, method: string}> - requests = {}, - - --- Contains $/progress report messages. - --- They have the format {token: integer|string, value: any} - --- For "work done progress", value will be one of: - --- - lsp.WorkDoneProgressBegin, - --- - lsp.WorkDoneProgressReport (extended with title from Begin) - --- - lsp.WorkDoneProgressEnd (extended with title from Begin) - progress = vim.ringbuf(50), - - --- @type lsp.ServerCapabilities - server_capabilities = {}, - - ---@deprecated use client.progress instead - messages = { name = name, messages = {}, progress = {}, status = {} }, - dynamic_capabilities = vim.lsp._dynamic.new(client_id), - } - - ---@type table<string|integer, string> title of unfinished progress sequences by token - client.progress.pending = {} - - --- @type lsp.ClientCapabilities - client.config.capabilities = config.capabilities or protocol.make_client_capabilities() + local client = require('vim.lsp.client').new(client_id, rpc, handlers, offset_encoding, config) -- Store the uninitialized_clients for cleanup in case we exit before initialize finishes. uninitialized_clients[client_id] = client - local function initialize() - local valid_traces = { - off = 'off', - messages = 'messages', - verbose = 'verbose', - } - - local workspace_folders --- @type lsp.WorkspaceFolder[]? - local root_uri --- @type string? - local root_path --- @type string? - if config.workspace_folders or config.root_dir then - if config.root_dir and not config.workspace_folders then - workspace_folders = { - { - uri = vim.uri_from_fname(config.root_dir), - name = string.format('%s', config.root_dir), - }, - } - else - workspace_folders = config.workspace_folders - end - root_uri = workspace_folders[1].uri - root_path = vim.uri_to_fname(root_uri) - else - workspace_folders = nil - root_uri = nil - root_path = nil - end - - local initialize_params = { - -- The process Id of the parent process that started the server. Is null if - -- the process has not been started by another process. If the parent - -- process is not alive then the server should exit (see exit notification) - -- its process. - processId = uv.os_getpid(), - -- Information about the client - -- since 3.15.0 - clientInfo = { - name = 'Neovim', - version = tostring(vim.version()), - }, - -- The rootPath of the workspace. Is null if no folder is open. - -- - -- @deprecated in favour of rootUri. - rootPath = root_path or vim.NIL, - -- The rootUri of the workspace. Is null if no folder is open. If both - -- `rootPath` and `rootUri` are set `rootUri` wins. - rootUri = root_uri or vim.NIL, - -- The workspace folders configured in the client when the server starts. - -- This property is only available if the client supports workspace folders. - -- It can be `null` if the client supports workspace folders but none are - -- configured. - workspaceFolders = workspace_folders or vim.NIL, - -- User provided initialization options. - initializationOptions = config.init_options, - -- The capabilities provided by the client (editor or tool) - capabilities = config.capabilities, - -- The initial trace setting. If omitted trace is disabled ("off"). - -- trace = "off" | "messages" | "verbose"; - trace = valid_traces[config.trace] or 'off', - } - if config.before_init then - local status, err = pcall(config.before_init, initialize_params, config) - if not status then - write_error(lsp.client_errors.BEFORE_INIT_CALLBACK_ERROR, err) - end - end - - --- @param method string - --- @param opts? {bufnr: integer?} - 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, 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 - - if log.trace() then - log.trace(log_prefix, 'initialize_params', initialize_params) - end - rpc.request('initialize', initialize_params, function(init_err, result) - assert(not init_err, tostring(init_err)) - assert(result, 'server sent empty result') - rpc.notify('initialized', vim.empty_dict()) - client.initialized = true - uninitialized_clients[client_id] = nil - client.workspace_folders = workspace_folders - - -- These are the cleaned up capabilities we use for dynamically deciding - -- when to send certain events to clients. - client.server_capabilities = - assert(result.capabilities, "initialize result doesn't contain capabilities") - client.server_capabilities = assert(protocol.resolve_capabilities(client.server_capabilities)) - - if client.server_capabilities.positionEncoding then - client.offset_encoding = client.server_capabilities.positionEncoding - end - - if next(config.settings) then - client.notify(ms.workspace_didChangeConfiguration, { settings = config.settings }) - end - - if config.on_init then - local status, err = pcall(config.on_init, client, result) - if not status then - write_error(lsp.client_errors.ON_INIT_CALLBACK_ERROR, err) - end - end - if log.info() then - log.info( - log_prefix, - 'server_capabilities', - { server_capabilities = client.server_capabilities } - ) - end - - -- Only assign after initialized. - active_clients[client_id] = client - -- If we had been registered before we start, then send didOpen This can - -- happen if we attach to buffers before initialize finishes or if - -- someone restarts a client. - for bufnr, client_ids in pairs(all_buffer_active_clients) do - if client_ids[client_id] then - client._on_attach(bufnr) - end - end - end) - end - - ---@nodoc - --- Sends a request to the server. - --- - --- This is a thin wrapper around {client.rpc.request} with some additional - --- checks for capabilities and handler availability. - --- - ---@param method string LSP method name. - ---@param params table|nil LSP request params. - ---@param handler lsp.Handler|nil Response |lsp-handler| for this method. - ---@param bufnr integer Buffer handle (0 for current). - ---@return boolean status, integer|nil request_id {status} is a bool indicating - ---whether the request was successful. If it is `false`, then it will - ---always be `false` (the client has shutdown). If it was - ---successful, then it will return {request_id} as the - ---second result. You can use this with `client.cancel_request(request_id)` - ---to cancel the-request. - ---@see |vim.lsp.buf_request_all()| - function client.request(method, params, handler, bufnr) - if not handler then - handler = assert( - resolve_handler(method), - string.format('not found: %q request handler for client %q.', method, client.name) - ) - end - -- Ensure pending didChange notifications are sent so that the server doesn't operate on a stale state - changetracking.flush(client, bufnr) - local version = util.buf_versions[bufnr] - bufnr = resolve_bufnr(bufnr) - if log.debug() then - log.debug(log_prefix, 'client.request', client_id, method, params, handler, bufnr) - end - local success, request_id = rpc.request(method, params, function(err, result) - local context = { - method = method, - client_id = client_id, - bufnr = bufnr, - params = params, - version = version, - } - handler(err, result, context) - end, function(request_id) - local request = client.requests[request_id] - request.type = 'complete' - nvim_exec_autocmds('LspRequest', { - buffer = api.nvim_buf_is_valid(bufnr) and bufnr or nil, - modeline = false, - data = { client_id = client_id, request_id = request_id, request = request }, - }) - client.requests[request_id] = nil - end) - - if success and request_id then - local request = { type = 'pending', bufnr = bufnr, method = method } - client.requests[request_id] = request - nvim_exec_autocmds('LspRequest', { - buffer = bufnr, - modeline = false, - data = { client_id = client_id, request_id = request_id, request = request }, - }) - end - - return success, request_id - end - - ---@private - --- Sends a request to the server and synchronously waits for the response. - --- - --- This is a wrapper around {client.request} - --- - ---@param method (string) LSP method name. - ---@param params (table) LSP request params. - ---@param timeout_ms (integer|nil) Maximum time in milliseconds to wait for - --- a result. Defaults to 1000 - ---@param bufnr (integer) Buffer handle (0 for current). - ---@return {err: lsp.ResponseError|nil, result:any}|nil, string|nil err # a dictionary, where - --- `err` and `result` come from the |lsp-handler|. - --- On timeout, cancel or error, returns `(nil, err)` where `err` is a - --- string describing the failure reason. If the request was unsuccessful - --- returns `nil`. - ---@see |vim.lsp.buf_request_sync()| - function client.request_sync(method, params, timeout_ms, bufnr) - local request_result = nil - local function _sync_handler(err, result) - request_result = { err = err, result = result } - end - - local success, request_id = client.request(method, params, _sync_handler, bufnr) - if not success then - return nil - end - - local wait_result, reason = vim.wait(timeout_ms or 1000, function() - return request_result ~= nil - end, 10) - - if not wait_result then - if request_id then - client.cancel_request(request_id) - end - return nil, wait_result_reason[reason] - end - return request_result - end - - ---@nodoc - --- Sends a notification to an LSP server. - --- - ---@param method string LSP method name. - ---@param params table|nil LSP request params. - ---@return boolean status true if the notification was successful. - ---If it is false, then it will always be false - ---(the client has shutdown). - function client.notify(method, params) - if method ~= ms.textDocument_didChange then - changetracking.flush(client) - end - - local client_active = rpc.notify(method, params) - - if client_active then - vim.schedule(function() - nvim_exec_autocmds('LspNotify', { - modeline = false, - data = { - client_id = client.id, - method = method, - params = params, - }, - }) - end) - end - - return client_active - end - - ---@nodoc - --- Cancels a request with a given request id. - --- - ---@param id (integer) id of request to cancel - ---@return boolean status true if notification was successful. false otherwise - ---@see |vim.lsp.client.notify()| - function client.cancel_request(id) - validate({ id = { id, 'n' } }) - local request = client.requests[id] - if request and request.type == 'pending' then - request.type = 'cancel' - nvim_exec_autocmds('LspRequest', { - buffer = request.bufnr, - modeline = false, - data = { client_id = client_id, request_id = id, request = request }, - }) - end - return rpc.notify(ms.dollar_cancelRequest, { id = id }) - end - - -- Track this so that we can escalate automatically if we've already tried a - -- graceful shutdown - local graceful_shutdown_failed = false - - ---@nodoc - --- Stops a client, optionally with force. - --- - ---By default, it will just ask the - server to shutdown without force. If - --- you request to stop a client which has previously been requested to - --- shutdown, it will automatically escalate and force shutdown. - --- - ---@param force boolean|nil - function client.stop(force) - if rpc.is_closing() then - return - end - if force or not client.initialized or graceful_shutdown_failed then - rpc.terminate() - return - end - -- Sending a signal after a process has exited is acceptable. - rpc.request(ms.shutdown, nil, function(err, _) - if err == nil then - rpc.notify(ms.exit) - else - -- If there was an error in the shutdown request, then term to be safe. - rpc.terminate() - graceful_shutdown_failed = true - end - end) - end - - ---@private - --- Checks whether a client is stopped. - --- - ---@return boolean # true if client is stopped or in the process of being - ---stopped; false otherwise - function client.is_stopped() - return rpc.is_closing() - end - - ---@private - --- Execute a lsp command, either via client command function (if available) - --- or via workspace/executeCommand (if supported by the server) - --- - ---@param command lsp.Command - ---@param context? {bufnr: integer} - ---@param handler? lsp.Handler only called if a server command - 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 = client.id - local cmdname = command.command - local fn = client.commands[cmdname] or lsp.commands[cmdname] - if fn then - fn(command, context) - return - end - - local command_provider = client.server_capabilities.executeCommandProvider - local commands = type(command_provider) == 'table' and command_provider.commands or {} - if not vim.list_contains(commands, cmdname) then - vim.notify_once( - string.format( - 'Language server `%s` does not support command `%s`. This command may require a client extension.', - client.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 - local params = { - command = command.command, - arguments = command.arguments, - } - client.request(ms.workspace_executeCommand, params, handler, context.bufnr) - end - - ---@private - --- Runs the on_attach function from the client's config if it was defined. - ---@param bufnr integer Buffer number - function client._on_attach(bufnr) - text_document_did_open_handler(bufnr, client) - - lsp._set_defaults(client, bufnr) - - nvim_exec_autocmds('LspAttach', { - buffer = bufnr, - modeline = false, - data = { client_id = client.id }, - }) - - if config.on_attach then - local status, err = pcall(config.on_attach, client, bufnr) - if not status then - write_error(lsp.client_errors.ON_ATTACH_ERROR, err) + client:initialize(function() + uninitialized_clients[client_id] = nil + -- Only assign after initialized. + active_clients[client_id] = client + -- If we had been registered before we start, then send didOpen This can + -- happen if we attach to buffers before initialize finishes or if + -- someone restarts a client. + for bufnr, client_ids in pairs(all_buffer_active_clients) do + if client_ids[client_id] then + client.on_attach(bufnr) end end - - -- schedule the initialization of semantic tokens to give the above - -- on_attach and LspAttach callbacks the ability to schedule wrap the - -- opt-out (deleting the semanticTokensProvider from capabilities) - vim.schedule(function() - if vim.tbl_get(client.server_capabilities, 'semanticTokensProvider', 'full') then - lsp.semantic_tokens.start(bufnr, client.id) - end - end) - - client.attached_buffers[bufnr] = true - end - - initialize() + end) return client_id end @@ -1564,7 +1101,7 @@ function lsp.buf_attach_client(bufnr, client_id) if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'openClose') then client.notify(ms.textDocument_didClose, params) end - text_document_did_open_handler(bufnr, client) + client:_text_document_did_open_handler(bufnr) end end, on_detach = function() @@ -1596,7 +1133,7 @@ function lsp.buf_attach_client(bufnr, client_id) -- Send didOpen for the client if it is initialized. If it isn't initialized -- then it will send didOpen on initialize. if client then - client._on_attach(bufnr) + client:_on_attach(bufnr) end return true end diff --git a/runtime/lua/vim/lsp/_dynamic.lua b/runtime/lua/vim/lsp/_dynamic.lua index 3c9dee2c69..8b8f3bdc38 100644 --- a/runtime/lua/vim/lsp/_dynamic.lua +++ b/runtime/lua/vim/lsp/_dynamic.lua @@ -6,6 +6,7 @@ local glob = vim.glob local M = {} --- @param client_id number +--- @return lsp.DynamicCapabilities function M.new(client_id) return setmetatable({ capabilities = {}, @@ -37,7 +38,7 @@ function M:register(registrations) end --- @param unregisterations lsp.Unregistration[] ---- @private +--- @package function M:unregister(unregisterations) for _, unreg in ipairs(unregisterations) do local method = unreg.method @@ -77,7 +78,7 @@ end --- @param method string --- @param opts? {bufnr: integer?} ---- @private +--- @package function M:supports(method, opts) return self:get(method, opts) ~= nil end diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index d67b2ac8ea..7fc5286a78 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -652,7 +652,7 @@ local function on_code_action_results(results, opts) end if action.command then local command = type(action.command) == 'table' and action.command or action - client._exec_cmd(command, ctx) + client:_exec_cmd(command, ctx) end end diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua new file mode 100644 index 0000000000..2daf564f63 --- /dev/null +++ b/runtime/lua/vim/lsp/client.lua @@ -0,0 +1,663 @@ +local uv = vim.uv +local api = vim.api +local lsp = vim.lsp +local log = lsp.log +local ms = lsp.protocol.Methods +local changetracking = lsp._changetracking + +--- @class lsp.Client.Progress: vim.Ringbuf<{token: integer|string, value: any}> +--- @field pending table<lsp.ProgressToken,lsp.LSPAny> + +--- @class lsp.Client +--- +--- The id allocated to the client. +--- @field id integer +--- +--- If a name is specified on creation, that will be used. Otherwise it is just +--- the client id. This is used for logs and messages. +--- @field name string +--- +--- RPC client object, for low level interaction with the client. +--- See |vim.lsp.rpc.start()|. +--- @field rpc vim.lsp.rpc.PublicClient +--- +--- The encoding used for communicating with the server. You can modify this in +--- the `config`'s `on_init` method before text is sent to the server. +--- @field offset_encoding string +--- +--- The handlers used by the client as described in |lsp-handler|. +--- @field handlers table<string,lsp.Handler> +--- +--- The current pending requests in flight to the server. Entries are key-value +--- pairs with the key being the request ID while the value is a table with +--- `type`, `bufnr`, and `method` key-value pairs. `type` is either "pending" +--- for an active request, or "cancel" for a cancel request. It will be +--- "complete" ephemerally while executing |LspRequest| autocmds when replies +--- are received from the server. +--- @field requests table<integer,{ type: string, bufnr: integer, method: string}> +--- +--- copy of the table that was passed by the user +--- to |vim.lsp.start_client()|. +--- @field config lsp.ClientConfig +--- +--- Response from the server sent on +--- initialize` describing the server's capabilities. +--- @field server_capabilities lsp.ServerCapabilities +--- +--- A ring buffer (|vim.ringbuf()|) containing progress messages +--- sent by the server. +--- @field progress lsp.Client.Progress +--- +--- @field initialized true? +--- @field workspace_folders lsp.WorkspaceFolder[]? +--- @field attached_buffers table<integer,true> +--- @field commands table<string,function> +--- @field private _log_prefix string +--- Track this so that we can escalate automatically if we've already tried a +--- graceful shutdown +--- @field private _graceful_shutdown_failed true? +--- +--- @field dynamic_capabilities lsp.DynamicCapabilities +--- +--- Sends a request to the server. +--- This is a thin wrapper around {client.rpc.request} with some additional +--- checking. +--- If {handler} is not specified, If one is not found there, then an error +--- will occur. Returns: {status}, {[client_id]}. {status} is a boolean +--- indicating if the notification was successful. If it is `false`, then it +--- will always be `false` (the client has shutdown). +--- If {status} is `true`, the function returns {request_id} as the second +--- result. You can use this with `client.cancel_request(request_id)` to cancel +--- the request. +--- @field request fun(method: string, params: table?, handler: lsp.Handler?, bufnr: integer): boolean, integer? +--- +--- Sends a request to the server and synchronously waits for the response. +--- This is a wrapper around {client.request} +--- Returns: { err=err, result=result }, a dictionary, where `err` and `result` +--- come from the |lsp-handler|. On timeout, cancel or error, returns `(nil, +--- err)` where `err` is a string describing the failure reason. If the request +--- was unsuccessful returns `nil`. +--- @field request_sync fun(method: string, params: table?, timeout_ms: integer?, bufnr: integer): {err: lsp.ResponseError|nil, result:any}|nil, string|nil err # a dictionary, where +--- +--- Sends a notification to an LSP server. +--- Returns: a boolean to indicate if the notification was successful. If +--- it is false, then it will always be false (the client has shutdown). +--- @field notify fun(method: string, params: table?): boolean +--- +--- Cancels a request with a given request id. +--- Returns: same as `notify()`. +--- @field cancel_request fun(id: integer): boolean +--- +--- Stops a client, optionally with force. +--- By default, it will just ask the server to shutdown without force. +--- If you request to stop a client which has previously been requested to +--- shutdown, it will automatically escalate and force shutdown. +--- @field stop fun(force?: boolean) +--- +--- Runs the on_attach function from the client's config if it was defined. +--- Useful for buffer-local setup. +--- @field on_attach fun(bufnr: integer) +--- +--- Checks if a client supports a given method. +--- Always returns true for unknown off-spec methods. +--- [opts] is a optional `{bufnr?: integer}` table. +--- Some language server capabilities can be file specific. +--- @field supports_method fun(method: string, opts?: {bufnr: integer?}): boolean +--- +--- Checks whether a client is stopped. +--- Returns: true if the client is fully stopped. +--- @field is_stopped fun(): boolean +local Client = {} +Client.__index = Client + +--- @param cls table +--- @param meth any +--- @return function +local function method_wrapper(cls, meth) + return function(...) + return meth(cls, ...) + end +end + +--- @package +--- @param id integer +--- @param rpc vim.lsp.rpc.PublicClient +--- @param handlers table<string,lsp.Handler> +--- @param offset_encoding string +--- @param config lsp.ClientConfig +--- @return lsp.Client +function Client.new(id, rpc, handlers, offset_encoding, config) + local name = config.name + + --- @class lsp.Client + local self = { + id = id, + config = config, + handlers = handlers, + rpc = rpc, + offset_encoding = offset_encoding, + name = name, + _log_prefix = string.format('LSP[%s]', name), + requests = {}, + commands = config.commands or {}, + attached_buffers = {}, + server_capabilities = {}, + dynamic_capabilities = vim.lsp._dynamic.new(id), + + --- Contains $/progress report messages. + --- They have the format {token: integer|string, value: any} + --- For "work done progress", value will be one of: + --- - lsp.WorkDoneProgressBegin, + --- - lsp.WorkDoneProgressReport (extended with title from Begin) + --- - lsp.WorkDoneProgressEnd (extended with title from Begin) + progress = vim.ringbuf(50) --[[@as lsp.Client.Progress]], + + --- @deprecated use client.progress instead + messages = { name = name, messages = {}, progress = {}, status = {} }, + } + + self.request = method_wrapper(self, Client._request) + self.request_sync = method_wrapper(self, Client._request_sync) + self.notify = method_wrapper(self, Client._notify) + self.cancel_request = method_wrapper(self, Client._cancel_request) + self.stop = method_wrapper(self, Client._stop) + self.is_stopped = method_wrapper(self, Client._is_stopped) + self.on_attach = method_wrapper(self, Client._on_attach) + self.supports_method = method_wrapper(self, Client._supports_method) + + ---@type table<string|integer, string> title of unfinished progress sequences by token + self.progress.pending = {} + + return setmetatable(self, Client) +end + +--- @private +--- @param cb fun() +function Client:initialize(cb) + local valid_traces = { + off = 'off', + messages = 'messages', + verbose = 'verbose', + } + + local config = self.config + + local workspace_folders --- @type lsp.WorkspaceFolder[]? + local root_uri --- @type string? + local root_path --- @type string? + if config.workspace_folders or config.root_dir then + if config.root_dir and not config.workspace_folders then + workspace_folders = { + { + uri = vim.uri_from_fname(config.root_dir), + name = string.format('%s', config.root_dir), + }, + } + else + workspace_folders = config.workspace_folders + end + root_uri = workspace_folders[1].uri + root_path = vim.uri_to_fname(root_uri) + else + workspace_folders = nil + root_uri = nil + root_path = nil + end + + local initialize_params = { + -- The process Id of the parent process that started the server. Is null if + -- the process has not been started by another process. If the parent + -- process is not alive then the server should exit (see exit notification) + -- its process. + processId = uv.os_getpid(), + -- Information about the client + -- since 3.15.0 + clientInfo = { + name = 'Neovim', + version = tostring(vim.version()), + }, + -- The rootPath of the workspace. Is null if no folder is open. + -- + -- @deprecated in favour of rootUri. + rootPath = root_path or vim.NIL, + -- The rootUri of the workspace. Is null if no folder is open. If both + -- `rootPath` and `rootUri` are set `rootUri` wins. + rootUri = root_uri or vim.NIL, + -- The workspace folders configured in the client when the server starts. + -- This property is only available if the client supports workspace folders. + -- It can be `null` if the client supports workspace folders but none are + -- configured. + workspaceFolders = workspace_folders or vim.NIL, + -- User provided initialization options. + initializationOptions = config.init_options, + -- The capabilities provided by the client (editor or tool) + capabilities = config.capabilities, + -- The initial trace setting. If omitted trace is disabled ("off"). + -- trace = "off" | "messages" | "verbose"; + trace = valid_traces[config.trace] or 'off', + } + if config.before_init then + --- @type boolean, string? + local status, err = pcall(config.before_init, initialize_params, config) + if not status then + self:write_error(lsp.client_errors.BEFORE_INIT_CALLBACK_ERROR, err) + end + end + + if log.trace() then + log.trace(self._log_prefix, 'initialize_params', initialize_params) + end + + local rpc = self.rpc + + rpc.request('initialize', initialize_params, function(init_err, result) + assert(not init_err, tostring(init_err)) + assert(result, 'server sent empty result') + rpc.notify('initialized', vim.empty_dict()) + self.initialized = true + self.workspace_folders = workspace_folders + + -- These are the cleaned up capabilities we use for dynamically deciding + -- when to send certain events to clients. + self.server_capabilities = + assert(result.capabilities, "initialize result doesn't contain capabilities") + self.server_capabilities = assert(lsp.protocol.resolve_capabilities(self.server_capabilities)) + + if self.server_capabilities.positionEncoding then + self.offset_encoding = self.server_capabilities.positionEncoding + end + + if next(config.settings) then + self:_notify(ms.workspace_didChangeConfiguration, { settings = config.settings }) + end + + if config.on_init then + --- @type boolean, string? + local status, err = pcall(config.on_init, self, result) + if not status then + self:write_error(lsp.client_errors.ON_INIT_CALLBACK_ERROR, err) + end + end + if log.info() then + log.info( + self._log_prefix, + 'server_capabilities', + { server_capabilities = self.server_capabilities } + ) + end + + cb() + end) +end + +--- @private +--- Returns the handler associated with an LSP method. +--- Returns the default handler if the user hasn't set a custom one. +--- +--- @param method (string) LSP method name +--- @return lsp.Handler|nil handler for the given method, if defined, or the default from |vim.lsp.handlers| +function Client:_resolve_handler(method) + return self.handlers[method] or lsp.handlers[method] +end + +--- Returns the buffer number for the given {bufnr}. +--- +--- @param bufnr (integer|nil) Buffer number to resolve. Defaults to current buffer +--- @return integer bufnr +local function resolve_bufnr(bufnr) + vim.validate({ bufnr = { bufnr, 'n', true } }) + if bufnr == nil or bufnr == 0 then + return api.nvim_get_current_buf() + end + return bufnr +end + +--- @private +--- Sends a request to the server. +--- +--- This is a thin wrapper around {client.rpc.request} with some additional +--- checks for capabilities and handler availability. +--- +--- @param method string LSP method name. +--- @param params table|nil LSP request params. +--- @param handler lsp.Handler|nil Response |lsp-handler| for this method. +--- @param bufnr integer Buffer handle (0 for current). +--- @return boolean status, integer|nil request_id {status} is a bool indicating +--- whether the request was successful. If it is `false`, then it will +--- always be `false` (the client has shutdown). If it was +--- successful, then it will return {request_id} as the +--- second result. You can use this with `client.cancel_request(request_id)` +--- to cancel the-request. +--- @see |vim.lsp.buf_request_all()| +function Client:_request(method, params, handler, bufnr) + if not handler then + handler = assert( + self:_resolve_handler(method), + string.format('not found: %q request handler for client %q.', method, self.name) + ) + end + -- Ensure pending didChange notifications are sent so that the server doesn't operate on a stale state + changetracking.flush(self, bufnr) + local version = lsp.util.buf_versions[bufnr] + bufnr = resolve_bufnr(bufnr) + if log.debug() then + log.debug(self._log_prefix, 'client.request', self.id, method, params, handler, bufnr) + end + local success, request_id = self.rpc.request(method, params, function(err, result) + local context = { + method = method, + client_id = self.id, + bufnr = bufnr, + params = params, + version = version, + } + handler(err, result, context) + end, function(request_id) + local request = self.requests[request_id] + request.type = 'complete' + api.nvim_exec_autocmds('LspRequest', { + buffer = api.nvim_buf_is_valid(bufnr) and bufnr or nil, + modeline = false, + data = { client_id = self.id, request_id = request_id, request = request }, + }) + self.requests[request_id] = nil + end) + + if success and request_id then + local request = { type = 'pending', bufnr = bufnr, method = method } + self.requests[request_id] = request + api.nvim_exec_autocmds('LspRequest', { + buffer = bufnr, + modeline = false, + data = { client_id = self.id, request_id = request_id, request = request }, + }) + end + + return success, request_id +end + +-- TODO(lewis6991): duplicated from lsp.lua +local wait_result_reason = { [-1] = 'timeout', [-2] = 'interrupted', [-3] = 'error' } + +-- TODO(lewis6991): duplicated from lsp.lua +--- Concatenates and writes a list of strings to the Vim error buffer. +--- +---@param ... string List to write to the buffer +local function err_message(...) + api.nvim_err_writeln(table.concat(vim.tbl_flatten({ ... }))) + api.nvim_command('redraw') +end + +--- @private +--- Sends a request to the server and synchronously waits for the response. +--- +--- This is a wrapper around {client.request} +--- +--- @param method (string) LSP method name. +--- @param params (table) LSP request params. +--- @param timeout_ms (integer|nil) Maximum time in milliseconds to wait for +--- a result. Defaults to 1000 +--- @param bufnr (integer) Buffer handle (0 for current). +--- @return {err: lsp.ResponseError|nil, result:any}|nil, string|nil err # a dictionary, where +--- `err` and `result` come from the |lsp-handler|. +--- On timeout, cancel or error, returns `(nil, err)` where `err` is a +--- string describing the failure reason. If the request was unsuccessful +--- returns `nil`. +--- @see |vim.lsp.buf_request_sync()| +function Client:_request_sync(method, params, timeout_ms, bufnr) + local request_result = nil + local function _sync_handler(err, result) + request_result = { err = err, result = result } + end + + local success, request_id = self:_request(method, params, _sync_handler, bufnr) + if not success then + return nil + end + + local wait_result, reason = vim.wait(timeout_ms or 1000, function() + return request_result ~= nil + end, 10) + + if not wait_result then + if request_id then + self:_cancel_request(request_id) + end + return nil, wait_result_reason[reason] + end + return request_result +end + +--- @private +--- Sends a notification to an LSP server. +--- +--- @param method string LSP method name. +--- @param params table|nil LSP request params. +--- @return boolean status true if the notification was successful. +--- If it is false, then it will always be false +--- (the client has shutdown). +function Client:_notify(method, params) + if method ~= ms.textDocument_didChange then + changetracking.flush(self) + end + + local client_active = self.rpc.notify(method, params) + + if client_active then + vim.schedule(function() + api.nvim_exec_autocmds('LspNotify', { + modeline = false, + data = { + client_id = self.id, + method = method, + params = params, + }, + }) + end) + end + + return client_active +end + +--- @private +--- Cancels a request with a given request id. +--- +--- @param id (integer) id of request to cancel +--- @return boolean status true if notification was successful. false otherwise +--- @see |vim.lsp.client.notify()| +function Client:_cancel_request(id) + vim.validate({ id = { id, 'n' } }) + local request = self.requests[id] + if request and request.type == 'pending' then + request.type = 'cancel' + api.nvim_exec_autocmds('LspRequest', { + buffer = request.bufnr, + modeline = false, + data = { client_id = self.id, request_id = id, request = request }, + }) + end + return self.rpc.notify(ms.dollar_cancelRequest, { id = id }) +end + +--- @nodoc +--- Stops a client, optionally with force. +--- +--- By default, it will just ask the - server to shutdown without force. If +--- you request to stop a client which has previously been requested to +--- shutdown, it will automatically escalate and force shutdown. +--- +--- @param force boolean|nil +function Client:_stop(force) + local rpc = self.rpc + + if rpc.is_closing() then + return + end + + if force or not self.initialized or self._graceful_shutdown_failed then + rpc.terminate() + return + end + + -- Sending a signal after a process has exited is acceptable. + rpc.request(ms.shutdown, nil, function(err, _) + if err == nil then + rpc.notify(ms.exit) + else + -- If there was an error in the shutdown request, then term to be safe. + rpc.terminate() + self._graceful_shutdown_failed = true + end + end) +end + +--- @private +--- Checks whether a client is stopped. +--- +--- @return boolean # true if client is stopped or in the process of being +--- stopped; false otherwise +function Client:_is_stopped() + return self.rpc.is_closing() +end + +--- @private +--- Execute a lsp command, either via client command function (if available) +--- or via workspace/executeCommand (if supported by the server) +--- +--- @param command lsp.Command +--- @param context? {bufnr: integer} +--- @param handler? lsp.Handler only called if a server command +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 + local cmdname = command.command + local fn = self.commands[cmdname] or lsp.commands[cmdname] + if fn then + fn(command, context) + return + end + + 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 + 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 + local params = { + command = command.command, + arguments = command.arguments, + } + self.request(ms.workspace_executeCommand, params, handler, context.bufnr) +end + +--- @package +--- Default handler for the 'textDocument/didOpen' LSP notification. +--- +--- @param bufnr integer Number of the buffer, or 0 for current +function Client:_text_document_did_open_handler(bufnr) + changetracking.init(self, bufnr) + if not vim.tbl_get(self.server_capabilities, 'textDocumentSync', 'openClose') then + return + end + if not api.nvim_buf_is_loaded(bufnr) then + return + end + local filetype = vim.bo[bufnr].filetype + + local params = { + textDocument = { + version = 0, + uri = vim.uri_from_bufnr(bufnr), + languageId = self.config.get_language_id(bufnr, filetype), + text = lsp._buf_get_full_text(bufnr), + }, + } + self.notify(ms.textDocument_didOpen, params) + lsp.util.buf_versions[bufnr] = params.textDocument.version + + -- Next chance we get, we should re-do the diagnostics + vim.schedule(function() + -- Protect against a race where the buffer disappears + -- between `did_open_handler` and the scheduled function firing. + if api.nvim_buf_is_valid(bufnr) then + local namespace = vim.lsp.diagnostic.get_namespace(self.id) + vim.diagnostic.show(namespace, bufnr) + end + end) +end + +--- @private +--- Runs the on_attach function from the client's config if it was defined. +--- @param bufnr integer Buffer number +function Client:_on_attach(bufnr) + self:_text_document_did_open_handler(bufnr) + + lsp._set_defaults(self, bufnr) + + api.nvim_exec_autocmds('LspAttach', { + buffer = bufnr, + modeline = false, + data = { client_id = self.id }, + }) + + if self.config.on_attach then + --- @type boolean, string? + local status, err = pcall(self.config.on_attach, self, bufnr) + if not status then + self:write_error(lsp.client_errors.ON_ATTACH_ERROR, err) + end + end + + -- schedule the initialization of semantic tokens to give the above + -- on_attach and LspAttach callbacks the ability to schedule wrap the + -- opt-out (deleting the semanticTokensProvider from capabilities) + vim.schedule(function() + if vim.tbl_get(self.server_capabilities, 'semanticTokensProvider', 'full') then + lsp.semantic_tokens.start(bufnr, self.id) + end + end) + + self.attached_buffers[bufnr] = true +end + +--- @private +--- Logs the given error to the LSP log and to the error buffer. +--- @param code integer Error code +--- @param err any Error arguments +function Client:write_error(code, err) + if log.error() then + log.error(self._log_prefix, 'on_error', { code = lsp.client_errors[code], err = err }) + end + err_message(self._log_prefix, ': Error ', lsp.client_errors[code], ': ', vim.inspect(err)) +end + +--- @param method string +--- @param opts? {bufnr: integer?} +function Client:_supports_method(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(self.server_capabilities, unpack(required_capability)) then + return true + else + if self.dynamic_capabilities:supports_registration(method) then + return self.dynamic_capabilities:supports(method, opts) + end + return false + end +end + +return Client diff --git a/runtime/lua/vim/lsp/codelens.lua b/runtime/lua/vim/lsp/codelens.lua index 199da288f4..a045a6bad4 100644 --- a/runtime/lua/vim/lsp/codelens.lua +++ b/runtime/lua/vim/lsp/codelens.lua @@ -48,7 +48,7 @@ local function execute_lens(lens, bufnr, client_id) local client = vim.lsp.get_client_by_id(client_id) assert(client, 'Client is required to execute lens, client_id=' .. client_id) - client._exec_cmd(lens.command, { bufnr = bufnr }, function(...) + client:_exec_cmd(lens.command, { bufnr = bufnr }, function(...) vim.lsp.handlers[ms.workspace_executeCommand](...) M.refresh() end) diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index fa7690e41e..dd0f7c2e1e 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -895,6 +895,7 @@ do ---@field private _idx_read integer ---@field private _idx_write integer ---@field private _size integer + ---@overload fun(self): table? local Ringbuf = {} --- Clear all items |