diff options
Diffstat (limited to 'runtime/lua/vim/lsp/client.lua')
-rw-r--r-- | runtime/lua/vim/lsp/client.lua | 1056 |
1 files changed, 1056 insertions, 0 deletions
diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua new file mode 100644 index 0000000000..ff0db166d5 --- /dev/null +++ b/runtime/lua/vim/lsp/client.lua @@ -0,0 +1,1056 @@ +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 +local validate = vim.validate + +--- @alias vim.lsp.client.on_init_cb fun(client: vim.lsp.Client, initialize_result: lsp.InitializeResult) +--- @alias vim.lsp.client.on_attach_cb fun(client: vim.lsp.Client, bufnr: integer) +--- @alias vim.lsp.client.on_exit_cb fun(code: integer, signal: integer, client_id: integer) +--- @alias vim.lsp.client.before_init_cb fun(params: lsp.InitializeParams, config: vim.lsp.ClientConfig) + +--- @class vim.lsp.Client.Flags +--- @inlinedoc +--- +--- Allow using incremental sync for buffer edits +--- (defailt: `true`) +--- @field allow_incremental_sync? boolean +--- +--- Debounce `didChange` notifications to the server by the given number in milliseconds. +--- No debounce occurs if `nil`. +--- (default: `150`) +--- @field debounce_text_changes integer +--- +--- Milliseconds to wait for server to exit cleanly after sending the +--- "shutdown" request before sending kill -15. If set to false, nvim exits +--- immediately after sending the "shutdown" request to the server. +--- (default: `false`) +--- @field exit_timeout integer|false + +--- @class vim.lsp.ClientConfig +--- command string[] that launches the language +--- server (treated as in |jobstart()|, must be absolute or on `$PATH`, shell constructs like +--- "~" are not expanded), or function that creates an RPC client. Function receives +--- a `dispatchers` table and returns a table with member functions `request`, `notify`, +--- `is_closing` and `terminate`. +--- See |vim.lsp.rpc.request()|, |vim.lsp.rpc.notify()|. +--- For TCP there is a builtin RPC client factory: |vim.lsp.rpc.connect()| +--- @field cmd string[]|fun(dispatchers: vim.lsp.rpc.Dispatchers): vim.lsp.rpc.PublicClient? +--- +--- Directory to launch the `cmd` process. Not related to `root_dir`. +--- (default: cwd) +--- @field cmd_cwd? string +--- +--- Environment flags to pass to the LSP on spawn. +--- Must be specified using a table. +--- Non-string values are coerced to string. +--- Example: +--- ```lua +--- { PORT = 8080; HOST = "0.0.0.0"; } +--- ``` +--- @field cmd_env? table +--- +--- Daemonize the server process so that it runs in a separate process group from Nvim. +--- Nvim will shutdown the process on exit, but if Nvim fails to exit cleanly this could leave +--- behind orphaned server processes. +--- (default: true) +--- @field detached? boolean +--- +--- List of workspace folders passed to the language server. +--- For backwards compatibility rootUri and rootPath will be derived from the first workspace +--- folder in this list. See `workspaceFolders` in the LSP spec. +--- @field workspace_folders? lsp.WorkspaceFolder[] +--- +--- Map overriding the default capabilities defined by |vim.lsp.protocol.make_client_capabilities()|, +--- passed to the language server on initialization. Hint: use make_client_capabilities() and modify +--- its result. +--- - Note: To send an empty dictionary use |vim.empty_dict()|, else it will be encoded as an +--- array. +--- @field capabilities? lsp.ClientCapabilities +--- +--- Map of language server method names to |lsp-handler| +--- @field handlers? table<string,function> +--- +--- Map with language server specific settings. +--- See the {settings} in |vim.lsp.Client|. +--- @field settings? table +--- +--- Table that maps string of clientside commands to user-defined functions. +--- Commands passed to start_client take precedence over the global command registry. Each key +--- must be a unique command name, and the value is a function which is called if any LSP action +--- (code action, code lenses, ...) triggers the command. +--- @field commands? table<string,fun(command: lsp.Command, ctx: table)> +--- +--- Values to pass in the initialization request as `initializationOptions`. See `initialize` in +--- the LSP spec. +--- @field init_options? table +--- +--- Name in log messages. +--- (default: client-id) +--- @field name? string +--- +--- Language ID as string. Defaults to the 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. +--- @field offset_encoding? 'utf-8'|'utf-16'|'utf-32' +--- +--- Callback invoked when the client operation throws an error. `code` is a number describing the error. +--- Other arguments may be passed depending on the error kind. See `vim.lsp.rpc.client_errors` +--- for possible errors. Use `vim.lsp.rpc.client_errors[code]` to get human-friendly name. +--- @field on_error? fun(code: integer, err: string) +--- +--- Callback invoked before the LSP "initialize" phase, where `params` contains the parameters +--- being sent to the server and `config` is the config that was passed to |vim.lsp.start_client()|. +--- You can use this to modify parameters before they are sent. +--- @field before_init? fun(params: lsp.InitializeParams, config: vim.lsp.ClientConfig) +--- +--- Callback invoked after LSP "initialize", where `result` is a table of `capabilities` +--- and anything else the server may send. For example, clangd sends +--- `initialize_result.offsetEncoding` if `capabilities.offsetEncoding` was sent to it. +--- You can only modify the `client.offset_encoding` here before any notifications are sent. +--- @field on_init? elem_or_list<fun(client: vim.lsp.Client, initialize_result: lsp.InitializeResult)> +--- +--- Callback invoked on client exit. +--- - code: exit code of the process +--- - signal: number describing the signal used to terminate (if any) +--- - client_id: client handle +--- @field on_exit? elem_or_list<fun(code: integer, signal: integer, client_id: integer)> +--- +--- Callback invoked when client attaches to a buffer. +--- @field on_attach? elem_or_list<fun(client: vim.lsp.Client, bufnr: integer)> +--- +--- Passed directly to the language server in the initialize request. Invalid/empty values will +--- (default: "off") +--- @field trace? 'off'|'messages'|'verbose' +--- +--- A table with flags for the client. The current (experimental) flags are: +--- @field flags? vim.lsp.Client.Flags +--- +--- Directory where the LSP server will base its workspaceFolders, rootUri, and rootPath on initialization. +--- @field root_dir? string + +--- @class vim.lsp.Client.Progress: vim.Ringbuf<{token: integer|string, value: any}> +--- @field pending table<lsp.ProgressToken,lsp.LSPAny> + +--- @class vim.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 vim.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 vim.lsp.Client.Progress +--- +--- @field initialized true? +--- +--- 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. +--- @field workspace_folders lsp.WorkspaceFolder[]? +--- @field root_dir string +--- +--- @field attached_buffers table<integer,true> +--- @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? +--- +--- The initial trace setting. If omitted trace is disabled ("off"). +--- trace = "off" | "messages" | "verbose"; +--- @field private _trace 'off'|'messages'|'verbose' +--- +--- Table of command name to function which is called if any LSP action +--- (code action, code lenses, ...) triggers the command. +--- Client commands take precedence over the global command registry. +--- @field commands table<string,fun(command: lsp.Command, ctx: table)> +--- +--- Map with language server specific settings. These are returned to the +--- language server if requested via `workspace/configuration`. Keys are +--- case-sensitive. +--- @field settings table +--- +--- A table with flags for the client. The current (experimental) flags are: +--- @field flags vim.lsp.Client.Flags +--- +--- @field get_language_id fun(bufnr: integer, filetype: string): string +--- +--- The capabilities provided by the client (editor or tool) +--- @field capabilities lsp.ClientCapabilities +--- @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 and if there's no respective global +--- handler, 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) +--- +--- @field private _before_init_cb? vim.lsp.client.before_init_cb +--- @field private _on_attach_cbs vim.lsp.client.on_attach_cb[] +--- @field private _on_init_cbs vim.lsp.client.on_init_cb[] +--- @field private _on_exit_cbs vim.lsp.client.on_exit_cb[] +--- @field private _on_error_cb? fun(code: integer, err: string) +--- +--- 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 + +local client_index = 0 + +--- Checks whether a given path is a directory. +--- @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' } }) + local stat = uv.fs_stat(filename) + return stat and stat.type == 'directory' or false +end + +local valid_encodings = { + ['utf-8'] = 'utf-8', + ['utf-16'] = 'utf-16', + ['utf-32'] = 'utf-32', + ['utf8'] = 'utf-8', + ['utf16'] = 'utf-16', + ['utf32'] = 'utf-32', + UTF8 = 'utf-8', + UTF16 = 'utf-16', + UTF32 = 'utf-32', +} + +--- Normalizes {encoding} to valid LSP encoding names. +--- @param encoding string? Encoding to normalize +--- @return string # normalized encoding name +local function validate_encoding(encoding) + validate({ + encoding = { encoding, 's', true }, + }) + if not encoding then + return valid_encodings.UTF16 + end + return valid_encodings[encoding:lower()] + or error( + string.format( + "Invalid offset encoding %q. Must be one of: 'utf-8', 'utf-16', 'utf-32'", + encoding + ) + ) +end + +--- Augments a validator function with support for optional (nil) values. +--- @param fn (fun(v): boolean) The original validator function; should return a +--- bool. +--- @return fun(v): boolean # The augmented function. Also returns true if {v} is +--- `nil`. +local function optional_validator(fn) + return function(v) + return v == nil or fn(v) + end +end + +--- By default, get_language_id just returns the exact filetype it is passed. +--- It is possible to pass in something that will calculate a different filetype, +--- to be sent by the client. +--- @param _bufnr integer +--- @param filetype string +local function default_get_language_id(_bufnr, filetype) + return filetype +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 }, + }) + + assert( + ( + not config.flags + or not config.flags.debounce_text_changes + or type(config.flags.debounce_text_changes) == 'number' + ), + 'flags.debounce_text_changes must be a number with the debounce time in milliseconds' + ) +end + +--- @param trace string +--- @return 'off'|'messages'|'verbose' +local function get_trace(trace) + local valid_traces = { + off = 'off', + messages = 'messages', + verbose = 'verbose', + } + return trace and valid_traces[trace] or 'off' +end + +--- @param id integer +--- @param config vim.lsp.ClientConfig +--- @return string +local function get_name(id, config) + local name = config.name + if name then + return name + end + + if type(config.cmd) == 'table' and config.cmd[1] then + return assert(vim.fs.basename(config.cmd[1])) + end + + return tostring(id) +end + +--- @param workspace_folders lsp.WorkspaceFolder[]? +--- @param root_dir string? +--- @return lsp.WorkspaceFolder[]? +local function get_workspace_folders(workspace_folders, root_dir) + if workspace_folders then + return workspace_folders + end + if root_dir then + return { + { + uri = vim.uri_from_fname(root_dir), + name = string.format('%s', root_dir), + }, + } + end +end + +--- @generic T +--- @param x elem_or_list<T>? +--- @return T[] +local function ensure_list(x) + if type(x) == 'table' then + return x + end + return { x } +end + +--- @package +--- @param config vim.lsp.ClientConfig +--- @return vim.lsp.Client? +function Client.create(config) + validate_config(config) + + client_index = client_index + 1 + local id = client_index + local name = get_name(id, config) + + --- @class vim.lsp.Client + local self = { + id = id, + config = config, + handlers = config.handlers or {}, + offset_encoding = validate_encoding(config.offset_encoding), + name = name, + _log_prefix = string.format('LSP[%s]', name), + requests = {}, + attached_buffers = {}, + server_capabilities = {}, + dynamic_capabilities = lsp._dynamic.new(id), + 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), + root_dir = config.root_dir, + _before_init_cb = config.before_init, + _on_init_cbs = ensure_list(config.on_init), + _on_exit_cbs = ensure_list(config.on_exit), + _on_attach_cbs = ensure_list(config.on_attach), + _on_error_cb = config.on_error, + _root_dir = config.root_dir, + _trace = get_trace(config.trace), + + --- 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 vim.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 = {} + + --- @type vim.lsp.rpc.Dispatchers + local dispatchers = { + notification = method_wrapper(self, Client._notification), + server_request = method_wrapper(self, Client._server_request), + on_error = method_wrapper(self, Client._on_error), + on_exit = method_wrapper(self, Client._on_exit), + } + + -- Start the RPC client. + local rpc --- @type vim.lsp.rpc.PublicClient? + local config_cmd = config.cmd + if type(config_cmd) == 'function' then + rpc = config_cmd(dispatchers) + else + rpc = lsp.rpc.start(config_cmd, dispatchers, { + cwd = config.cmd_cwd, + env = config.cmd_env, + detached = config.detached, + }) + end + + -- Return nil if the rpc client fails to start + if not rpc then + return + end + + self.rpc = rpc + + setmetatable(self, Client) + + return self +end + +--- @private +--- @param cbs function[] +--- @param error_id integer +--- @param ... any +function Client:_run_callbacks(cbs, error_id, ...) + for _, cb in pairs(cbs) do + --- @type boolean, string? + local status, err = pcall(cb, ...) + if not status then + self:write_error(error_id, err) + end + end +end + +--- @package +function Client:initialize() + local config = self.config + + local root_uri --- @type string? + local root_path --- @type string? + if self.workspace_folders then + root_uri = self.workspace_folders[1].uri + root_path = vim.uri_to_fname(root_uri) + 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, + workspaceFolders = self.workspace_folders or vim.NIL, + -- User provided initialization options. + initializationOptions = config.init_options, + capabilities = self.capabilities, + trace = self._trace, + } + + self:_run_callbacks( + { self._before_init_cb }, + lsp.client_errors.BEFORE_INIT_CALLBACK_ERROR, + initialize_params, + config + ) + + log.trace(self._log_prefix, 'initialize_params', initialize_params) + + 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 + + -- 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(self.settings) then + self:_notify(ms.workspace_didChangeConfiguration, { settings = self.settings }) + end + + self:_run_callbacks(self._on_init_cbs, lsp.client_errors.ON_INIT_CALLBACK_ERROR, self, result) + + log.info( + self._log_prefix, + 'server_capabilities', + { server_capabilities = self.server_capabilities } + ) + 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) + 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) + log.debug(self._log_prefix, 'client.request', self.id, method, params, handler, bufnr) + 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' } + +--- Concatenates and writes a list of strings to the Vim error buffer. +--- +--- @param ... string List to write to the buffer +local function err_message(...) + local message = table.concat(vim.tbl_flatten({ ... })) + if vim.in_fast_event() then + vim.schedule(function() + api.nvim_err_writeln(message) + api.nvim_command('redraw') + end) + else + api.nvim_err_writeln(message) + api.nvim_command('redraw') + end +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) + 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 + +--- @private +--- 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 + vim.lsp._watchfiles.cancel(self.id) + 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 + +--- @package +--- 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.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 = lsp.diagnostic.get_namespace(self.id) + vim.diagnostic.show(namespace, bufnr) + end + end) +end + +--- @package +--- 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 }, + }) + + self:_run_callbacks(self._on_attach_cbs, lsp.client_errors.ON_ATTACH_ERROR, self, bufnr) + + -- 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) + local client_error = lsp.client_errors[code] --- @type string|integer + log.error(self._log_prefix, 'on_error', { code = client_error, err = err }) + err_message(self._log_prefix, ': Error ', client_error, ': ', vim.inspect(err)) +end + +--- @private +--- @param method string +--- @param opts? {bufnr: integer?} +function Client:_supports_method(method, opts) + 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 + end + if self.dynamic_capabilities:supports_registration(method) then + return self.dynamic_capabilities:supports(method, opts) + end + return false +end + +--- @private +--- Handles a notification sent by an LSP server by invoking the +--- corresponding handler. +--- +--- @param method string LSP method name +--- @param params table The parameters for that method. +function Client:_notification(method, params) + log.trace('notification', method, params) + local handler = self:_resolve_handler(method) + if handler then + -- Method name is provided here for convenience. + handler(nil, params, { method = method, client_id = self.id }) + end +end + +--- @private +--- Handles a request from an LSP server by invoking the corresponding handler. +--- +--- @param method (string) LSP method name +--- @param params (table) The parameters for that method +--- @return any result +--- @return lsp.ResponseError error code and message set in case an exception happens during the request. +function Client:_server_request(method, params) + log.trace('server_request', method, params) + local handler = self:_resolve_handler(method) + if handler then + log.trace('server_request: found handler for', method) + return handler(nil, params, { method = method, client_id = self.id }) + end + log.warn('server_request: no handler found for', method) + return nil, lsp.rpc_response_error(lsp.protocol.ErrorCodes.MethodNotFound) +end + +--- @private +--- Invoked when the client operation throws an error. +--- +--- @param code integer Error code +--- @param err any Other arguments may be passed depending on the error kind +--- @see vim.lsp.rpc.client_errors for possible errors. Use +--- `vim.lsp.rpc.client_errors[code]` to get a human-friendly name. +function Client:_on_error(code, err) + self:write_error(code, err) + if self._on_error_cb then + --- @type boolean, string + local status, usererr = pcall(self._on_error_cb, code, err) + if not status then + log.error(self._log_prefix, 'user on_error failed', { err = usererr }) + err_message(self._log_prefix, ' user on_error failed: ', tostring(usererr)) + end + end +end + +--- @private +--- Invoked on client exit. +--- +--- @param code integer) exit code of the process +--- @param signal integer the signal used to terminate (if any) +function Client:_on_exit(code, signal) + self:_run_callbacks( + self._on_exit_cbs, + lsp.client_errors.ON_EXIT_CALLBACK_ERROR, + code, + signal, + self.id + ) +end + +return Client |