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 --- (default: `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 --- --- 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 --- --- 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 --- --- 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 --- --- Callback invoked when client attaches to a buffer. --- @field on_attach? elem_or_list --- --- 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 --- @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 --- --- 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 --- --- 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 --- --- Buffers that should be attached to upon initialize() --- @field package _buffers_to_attach table --- --- @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 --- --- 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 = root_dir, }, } end end --- @generic T --- @param x elem_or_list? --- @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 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 config_cmd = config.cmd if type(config_cmd) == 'function' then self.rpc = config_cmd(dispatchers) else self.rpc = lsp.rpc.start(config_cmd, dispatchers, { cwd = config.cmd_cwd, env = config.cmd_env, detached = config.detached, }) end 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, workDoneToken = '1', } 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 -- If server is being restarted, make sure to re-attach to any previously attached buffers. -- Save which buffers before on_init in case new buffers are attached. local reattach_bufs = vim.deepcopy(self.attached_buffers) self:_run_callbacks(self._on_init_cbs, lsp.client_errors.ON_INIT_CALLBACK_ERROR, self, result) for buf in pairs(reattach_bufs) do self:_on_attach(buf) end 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 LSP request params. --- @param handler? lsp.Handler Response |lsp-handler| for this method. --- @param bufnr? integer Buffer handle (0 for current). --- @return boolean status, integer? 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 = api.nvim_buf_is_valid(bufnr) and bufnr or nil, 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.iter({ ... }):flatten():totable()) 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 --- @package --- 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 = api.nvim_buf_is_valid(request.bufnr) and request.bufnr or nil, 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 --- @package --- Add a directory to the workspace folders. --- @param dir string? function Client:_add_workspace_folder(dir) for _, folder in pairs(self.workspace_folders or {}) do if folder.name == dir then print(dir, 'is already part of this workspace') return end end local wf = assert(get_workspace_folders(nil, dir)) self:_notify(ms.workspace_didChangeWorkspaceFolders, { event = { added = wf, removed = {} }, }) if not self.workspace_folders then self.workspace_folders = {} end vim.list_extend(self.workspace_folders, wf) end --- @package --- Remove a directory to the workspace folders. --- @param dir string? function Client:_remove_workspace_folder(dir) local wf = assert(get_workspace_folders(nil, dir)) self:_notify(ms.workspace_didChangeWorkspaceFolders, { event = { added = {}, removed = wf }, }) for idx, folder in pairs(self.workspace_folders) do if folder.name == dir then table.remove(self.workspace_folders, idx) break end end end return Client