diff options
Diffstat (limited to 'runtime/lua/vim/lsp.lua')
-rw-r--r-- | runtime/lua/vim/lsp.lua | 1769 |
1 files changed, 257 insertions, 1512 deletions
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 261a3aa5de..d5c376ba44 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -1,39 +1,37 @@ ----@diagnostic disable: invisible -local default_handlers = require('vim.lsp.handlers') -local log = require('vim.lsp.log') -local lsp_rpc = require('vim.lsp.rpc') -local protocol = require('vim.lsp.protocol') -local ms = protocol.Methods -local util = require('vim.lsp.util') -local sync = require('vim.lsp.sync') -local semantic_tokens = require('vim.lsp.semantic_tokens') - local api = vim.api -local nvim_err_writeln, nvim_buf_get_lines, nvim_command, nvim_exec_autocmds = - api.nvim_err_writeln, api.nvim_buf_get_lines, api.nvim_command, api.nvim_exec_autocmds -local uv = vim.uv local tbl_isempty, tbl_extend = vim.tbl_isempty, vim.tbl_extend local validate = vim.validate local if_nil = vim.F.if_nil -local lsp = { - protocol = protocol, - - handlers = default_handlers, - - buf = require('vim.lsp.buf'), - diagnostic = require('vim.lsp.diagnostic'), - codelens = require('vim.lsp.codelens'), - inlay_hint = require('vim.lsp.inlay_hint'), - semantic_tokens = semantic_tokens, - util = util, +local lsp = vim._defer_require('vim.lsp', { + _changetracking = ..., --- @module 'vim.lsp._changetracking' + _completion = ..., --- @module 'vim.lsp._completion' + _dynamic = ..., --- @module 'vim.lsp._dynamic' + _snippet_grammar = ..., --- @module 'vim.lsp._snippet_grammar' + _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' + inlay_hint = ..., --- @module 'vim.lsp.inlay_hint' + log = ..., --- @module 'vim.lsp.log' + protocol = ..., --- @module 'vim.lsp.protocol' + rpc = ..., --- @module 'vim.lsp.rpc' + semantic_tokens = ..., --- @module 'vim.lsp.semantic_tokens' + util = ..., --- @module 'vim.lsp.util' +}) - -- Allow raw RPC access. - rpc = lsp_rpc, +local log = lsp.log +local protocol = lsp.protocol +local ms = protocol.Methods +local util = lsp.util +local changetracking = lsp._changetracking - -- Export these directly from rpc. - rpc_response_error = lsp_rpc.rpc_response_error, -} +-- Export these directly from rpc. +---@nodoc +lsp.rpc_response_error = lsp.rpc.rpc_response_error -- maps request name to the required server_capability in the client. lsp._request_name_to_capability = { @@ -69,14 +67,6 @@ lsp._request_name_to_capability = { -- TODO improve handling of scratch buffers with LSP attached. ---- Concatenates and writes a list of strings to the Vim error buffer. ---- ----@param ... string List to write to the buffer -local function err_message(...) - nvim_err_writeln(table.concat(vim.tbl_flatten({ ... }))) - nvim_command('redraw') -end - --- Returns the buffer number for the given {bufnr}. --- ---@param bufnr (integer|nil) Buffer number to resolve. Defaults to current buffer @@ -102,57 +92,28 @@ function lsp._unsupported_method(method) return msg end ---- 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 wait_result_reason = { [-1] = 'timeout', [-2] = 'interrupted', [-3] = 'error' } -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', -} - local format_line_ending = { ['unix'] = '\n', ['dos'] = '\r\n', ['mac'] = '\r', } +---@private ---@param bufnr (number) ---@return string -local function buf_get_line_ending(bufnr) +function lsp._buf_get_line_ending(bufnr) return format_line_ending[vim.bo[bufnr].fileformat] or '\n' end -local client_index = 0 ---- Returns a new, unused client id. ---- ----@return integer client_id -local function next_client_id() - client_index = client_index + 1 - return client_index -end -- Tracks all clients created via lsp.start_client -local active_clients = {} --- @type table<integer,lsp.Client> +local active_clients = {} --- @type table<integer,vim.lsp.Client> local all_buffer_active_clients = {} --- @type table<integer,table<integer,true>> -local uninitialized_clients = {} --- @type table<integer,lsp.Client> +local uninitialized_clients = {} --- @type table<integer,vim.lsp.Client> ---@param bufnr? integer ----@param fn fun(client: lsp.Client, client_id: integer, bufnr: integer) +---@param fn fun(client: vim.lsp.Client, client_id: integer, bufnr: integer) local function for_each_buffer_client(bufnr, fn, restrict_client_ids) validate({ fn = { fn, 'f' }, @@ -182,136 +143,36 @@ local function for_each_buffer_client(bufnr, fn, restrict_client_ids) end end +local client_errors_base = table.maxn(lsp.rpc.client_errors) +local client_errors_offset = 0 + +local function client_error(name) + client_errors_offset = client_errors_offset + 1 + local index = client_errors_base + client_errors_offset + return { [name] = index, [index] = name } +end + --- Error codes to be used with `on_error` from |vim.lsp.start_client|. --- Can be used to look up the string from a the number or the number --- from the string. --- @nodoc lsp.client_errors = tbl_extend( 'error', - lsp_rpc.client_errors, - vim.tbl_add_reverse_lookup({ - BEFORE_INIT_CALLBACK_ERROR = table.maxn(lsp_rpc.client_errors) + 1, - ON_INIT_CALLBACK_ERROR = table.maxn(lsp_rpc.client_errors) + 2, - ON_ATTACH_ERROR = table.maxn(lsp_rpc.client_errors) + 3, - }) + lsp.rpc.client_errors, + client_error('BEFORE_INIT_CALLBACK_ERROR'), + client_error('ON_INIT_CALLBACK_ERROR'), + client_error('ON_ATTACH_ERROR'), + client_error('ON_EXIT_CALLBACK_ERROR') ) ---- 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' }, - }) - 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 - ----@internal ---- Parses a command invocation into the command itself and its args. If there ---- are no arguments, an empty table is returned as the second argument. ---- ----@param input string[] ----@return string command, string[] args #the command and arguments -function lsp._cmd_parts(input) - validate({ - cmd = { - input, - function() - return vim.tbl_islist(input) - end, - 'list', - }, - }) - - local cmd = input[1] - local cmd_args = {} - -- Don't mutate our input. - for i, v in ipairs(input) do - validate({ ['cmd argument'] = { v, 's' } }) - if i > 1 then - table.insert(cmd_args, v) - end - end - return cmd, cmd_args -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 - ---- Validates a client configuration as given to |vim.lsp.start_client()|. ---- ----@param config (lsp.ClientConfig) ----@return (string|fun(dispatchers:table):table) Command ----@return string[] Arguments ----@return string Encoding. -local function validate_client_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', true }, - on_init = { config.on_init, 'f', true }, - settings = { config.settings, 't', true }, - commands = { config.commands, 't', true }, - before_init = { config.before_init, 'f', 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' - ) - - local cmd, cmd_args --- @type (string|fun(dispatchers:table):table), string[] - local config_cmd = config.cmd - if type(config_cmd) == 'function' then - cmd = config_cmd - else - cmd, cmd_args = lsp._cmd_parts(config_cmd) - end - local offset_encoding = valid_encodings.UTF16 - if config.offset_encoding then - offset_encoding = validate_encoding(config.offset_encoding) - end - - return cmd, cmd_args, offset_encoding -end - +---@private --- Returns full text of buffer {bufnr} as a string. --- ---@param bufnr (number) Buffer handle, or 0 for current. ---@return string # Buffer text as string. -local function buf_get_full_text(bufnr) - local line_ending = buf_get_line_ending(bufnr) - local text = table.concat(nvim_buf_get_lines(bufnr, 0, -1, true), line_ending) +function lsp._buf_get_full_text(bufnr) + local line_ending = lsp._buf_get_line_ending(bufnr) + local text = table.concat(api.nvim_buf_get_lines(bufnr, 0, -1, true), line_ending) if vim.bo[bufnr].eol then text = text .. line_ending end @@ -327,484 +188,26 @@ end ---@param fn (T) Function to run ---@return T local function once(fn) - local value --- @type any + local value --- @type function local ran = false return function(...) if not ran then - value = fn(...) + value = fn(...) --- @type function ran = true end return value end end -local changetracking = {} -do - ---@private - --- - --- LSP has 3 different sync modes: - --- - None (Servers will read the files themselves when needed) - --- - Full (Client sends the full buffer content on updates) - --- - Incremental (Client sends only the changed parts) - --- - --- Changes are tracked per buffer. - --- A buffer can have multiple clients attached and each client needs to send the changes - --- To minimize the amount of changesets to compute, computation is grouped: - --- - --- None: One group for all clients - --- Full: One group for all clients - --- Incremental: One group per `offset_encoding` - --- - --- Sending changes can be debounced per buffer. To simplify the implementation the - --- smallest debounce interval is used and we don't group clients by different intervals. - --- - --- @class CTGroup - --- @field sync_kind integer TextDocumentSyncKind, considers config.flags.allow_incremental_sync - --- @field offset_encoding "utf-8"|"utf-16"|"utf-32" - --- - --- @class CTBufferState - --- @field name string name of the buffer - --- @field lines string[] snapshot of buffer lines from last didChange - --- @field lines_tmp string[] - --- @field pending_changes table[] List of debounced changes in incremental sync mode - --- @field timer nil|uv.uv_timer_t uv_timer - --- @field last_flush nil|number uv.hrtime of the last flush/didChange-notification - --- @field needs_flush boolean true if buffer updates haven't been sent to clients/servers yet - --- @field refs integer how many clients are using this group - --- - --- @class CTGroupState - --- @field buffers table<integer, CTBufferState> - --- @field debounce integer debounce duration in ms - --- @field clients table<integer, table> clients using this state. {client_id, client} - - ---@param group CTGroup - ---@return string - local function group_key(group) - if group.sync_kind == protocol.TextDocumentSyncKind.Incremental then - return tostring(group.sync_kind) .. '\0' .. group.offset_encoding - end - return tostring(group.sync_kind) - end - - ---@private - ---@type table<CTGroup, CTGroupState> - local state_by_group = setmetatable({}, { - __index = function(tbl, k) - return rawget(tbl, group_key(k)) - end, - __newindex = function(tbl, k, v) - rawset(tbl, group_key(k), v) - end, - }) - - ---@return CTGroup - local function get_group(client) - local allow_inc_sync = if_nil(client.config.flags.allow_incremental_sync, true) - local change_capability = vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'change') - local sync_kind = change_capability or protocol.TextDocumentSyncKind.None - if not allow_inc_sync and change_capability == protocol.TextDocumentSyncKind.Incremental then - sync_kind = protocol.TextDocumentSyncKind.Full - end - return { - sync_kind = sync_kind, - offset_encoding = client.offset_encoding, - } - end - - ---@param state CTBufferState - local function incremental_changes(state, encoding, bufnr, firstline, lastline, new_lastline) - local prev_lines = state.lines - local curr_lines = state.lines_tmp - - local changed_lines = nvim_buf_get_lines(bufnr, firstline, new_lastline, true) - for i = 1, firstline do - curr_lines[i] = prev_lines[i] - end - for i = firstline + 1, new_lastline do - curr_lines[i] = changed_lines[i - firstline] - end - for i = lastline + 1, #prev_lines do - curr_lines[i - lastline + new_lastline] = prev_lines[i] - end - if tbl_isempty(curr_lines) then - -- Can happen when deleting the entire contents of a buffer, see https://github.com/neovim/neovim/issues/16259. - curr_lines[1] = '' - end - - local line_ending = buf_get_line_ending(bufnr) - local incremental_change = sync.compute_diff( - state.lines, - curr_lines, - firstline, - lastline, - new_lastline, - encoding, - line_ending - ) - - -- Double-buffering of lines tables is used to reduce the load on the garbage collector. - -- At this point the prev_lines table is useless, but its internal storage has already been allocated, - -- so let's keep it around for the next didChange event, in which it will become the next - -- curr_lines table. Note that setting elements to nil doesn't actually deallocate slots in the - -- internal storage - it merely marks them as free, for the GC to deallocate them. - for i in ipairs(prev_lines) do - prev_lines[i] = nil - end - state.lines = curr_lines - state.lines_tmp = prev_lines - - return incremental_change - end - - ---@private - function changetracking.init(client, bufnr) - assert(client.offset_encoding, 'lsp client must have an offset_encoding') - local group = get_group(client) - local state = state_by_group[group] - if state then - state.debounce = math.min(state.debounce, client.config.flags.debounce_text_changes or 150) - state.clients[client.id] = client - else - state = { - buffers = {}, - debounce = client.config.flags.debounce_text_changes or 150, - clients = { - [client.id] = client, - }, - } - state_by_group[group] = state - end - local buf_state = state.buffers[bufnr] - if buf_state then - buf_state.refs = buf_state.refs + 1 - else - buf_state = { - name = api.nvim_buf_get_name(bufnr), - lines = {}, - lines_tmp = {}, - pending_changes = {}, - needs_flush = false, - refs = 1, - } - state.buffers[bufnr] = buf_state - if group.sync_kind == protocol.TextDocumentSyncKind.Incremental then - buf_state.lines = nvim_buf_get_lines(bufnr, 0, -1, true) - end - end - end - - ---@private - function changetracking._get_and_set_name(client, bufnr, name) - local state = state_by_group[get_group(client)] or {} - local buf_state = (state.buffers or {})[bufnr] - local old_name = buf_state.name - buf_state.name = name - return old_name - end - - ---@private - function changetracking.reset_buf(client, bufnr) - changetracking.flush(client, bufnr) - local state = state_by_group[get_group(client)] - if not state then - return - end - assert(state.buffers, 'CTGroupState must have buffers') - local buf_state = state.buffers[bufnr] - buf_state.refs = buf_state.refs - 1 - assert(buf_state.refs >= 0, 'refcount on buffer state must not get negative') - if buf_state.refs == 0 then - state.buffers[bufnr] = nil - changetracking._reset_timer(buf_state) - end - end - - ---@private - function changetracking.reset(client) - local state = state_by_group[get_group(client)] - if not state then - return - end - state.clients[client.id] = nil - if vim.tbl_count(state.clients) == 0 then - for _, buf_state in pairs(state.buffers) do - changetracking._reset_timer(buf_state) - end - state.buffers = {} - end - end - - -- Adjust debounce time by taking time of last didChange notification into - -- consideration. If the last didChange happened more than `debounce` time ago, - -- debounce can be skipped and otherwise maybe reduced. - -- - -- This turns the debounce into a kind of client rate limiting - -- - ---@param debounce integer - ---@param buf_state CTBufferState - ---@return number - local function next_debounce(debounce, buf_state) - if debounce == 0 then - return 0 - end - local ns_to_ms = 0.000001 - if not buf_state.last_flush then - return debounce - end - local now = uv.hrtime() - local ms_since_last_flush = (now - buf_state.last_flush) * ns_to_ms - return math.max(debounce - ms_since_last_flush, 0) - end - - ---@param bufnr integer - ---@param sync_kind integer protocol.TextDocumentSyncKind - ---@param state CTGroupState - ---@param buf_state CTBufferState - local function send_changes(bufnr, sync_kind, state, buf_state) - if not buf_state.needs_flush then - return - end - buf_state.last_flush = uv.hrtime() - buf_state.needs_flush = false - - if not api.nvim_buf_is_valid(bufnr) then - buf_state.pending_changes = {} - return - end - - local changes - if sync_kind == protocol.TextDocumentSyncKind.None then - return - elseif sync_kind == protocol.TextDocumentSyncKind.Incremental then - changes = buf_state.pending_changes - buf_state.pending_changes = {} - else - changes = { - { text = buf_get_full_text(bufnr) }, - } - end - local uri = vim.uri_from_bufnr(bufnr) - for _, client in pairs(state.clients) do - if not client.is_stopped() and lsp.buf_is_attached(bufnr, client.id) then - client.notify(ms.textDocument_didChange, { - textDocument = { - uri = uri, - version = util.buf_versions[bufnr], - }, - contentChanges = changes, - }) - end - end - end - - ---@private - function changetracking.send_changes(bufnr, firstline, lastline, new_lastline) - local groups = {} ---@type table<string,CTGroup> - for _, client in pairs(lsp.get_clients({ bufnr = bufnr })) do - local group = get_group(client) - groups[group_key(group)] = group - end - for _, group in pairs(groups) do - local state = state_by_group[group] - if not state then - error( - string.format( - 'changetracking.init must have been called for all LSP clients. group=%s states=%s', - vim.inspect(group), - vim.inspect(vim.tbl_keys(state_by_group)) - ) - ) - end - local buf_state = state.buffers[bufnr] - buf_state.needs_flush = true - changetracking._reset_timer(buf_state) - local debounce = next_debounce(state.debounce, buf_state) - if group.sync_kind == protocol.TextDocumentSyncKind.Incremental then - -- This must be done immediately and cannot be delayed - -- The contents would further change and startline/endline may no longer fit - local changes = incremental_changes( - buf_state, - group.offset_encoding, - bufnr, - firstline, - lastline, - new_lastline - ) - table.insert(buf_state.pending_changes, changes) - end - if debounce == 0 then - send_changes(bufnr, group.sync_kind, state, buf_state) - else - local timer = assert(uv.new_timer(), 'Must be able to create timer') - buf_state.timer = timer - timer:start( - debounce, - 0, - vim.schedule_wrap(function() - changetracking._reset_timer(buf_state) - send_changes(bufnr, group.sync_kind, state, buf_state) - end) - ) - end - end - end - - ---@private - function changetracking._reset_timer(buf_state) - local timer = buf_state.timer - if timer then - buf_state.timer = nil - if not timer:is_closing() then - timer:stop() - timer:close() - end - end - end - - --- Flushes any outstanding change notification. - ---@private - function changetracking.flush(client, bufnr) - local group = get_group(client) - local state = state_by_group[group] - if not state then - return - end - if bufnr then - local buf_state = state.buffers[bufnr] or {} - changetracking._reset_timer(buf_state) - send_changes(bufnr, group.sync_kind, state, buf_state) - else - for buf, buf_state in pairs(state.buffers) do - changetracking._reset_timer(buf_state) - send_changes(buf, group.sync_kind, state, buf_state) - end - end - end -end - ---- Default handler for the 'textDocument/didOpen' LSP notification. ---- ----@param bufnr integer Number of the buffer, or 0 for current ----@param client table 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 = 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 ---- |vim.lsp.get_client_by_id()| or |vim.lsp.get_clients()|. ---- ---- - Methods: ---- ---- - request(method, params, [handler], bufnr) ---- 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. ---- ---- - request_sync(method, params, timeout_ms, bufnr) ---- 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`. ---- ---- - notify(method, params) ---- 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). ---- ---- - cancel_request(id) ---- Cancels a request with a given request id. ---- Returns: same as `notify()`. ---- ---- - stop([force]) ---- 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. ---- ---- - is_stopped() ---- Checks whether a client is stopped. ---- Returns: true if the client is fully stopped. ---- ---- - on_attach(client, bufnr) ---- Runs the on_attach function from the client's config if it was defined. ---- Useful for buffer-local setup. +--- @class vim.lsp.start.Opts +--- @inlinedoc --- ---- - Members ---- - {id} (number): The id allocated to the client. +--- Predicate used to decide if a client should be re-used. Used on all +--- running clients. The default implementation re-uses a client if name and +--- root_dir matches. +--- @field reuse_client fun(client: vim.lsp.Client, config: table): boolean --- ---- - {name} (string): 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. ---- ---- - {rpc} (table): RPC client object, for low level interaction with the ---- client. See |vim.lsp.rpc.start()|. ---- ---- - {offset_encoding} (string): 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. ---- ---- - {handlers} (table): The handlers used by the client as described in |lsp-handler|. ---- ---- - {requests} (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. ---- ---- - {config} (table): copy of the table that was passed by the user ---- to |vim.lsp.start_client()|. ---- ---- - {server_capabilities} (table): Response from the server sent on ---- `initialize` describing the server's capabilities. ---- ---- - {progress} A ring buffer (|vim.ringbuf()|) containing progress messages ---- sent by the server. -function lsp.client() - error() -end - ---- @class lsp.StartOpts ---- @field reuse_client fun(client: lsp.Client, config: table): boolean +--- Buffer handle to attach to if starting or re-using a client (0 for current). --- @field bufnr integer --- Create a new LSP client and start a language server or reuses an already @@ -824,8 +227,7 @@ end --- See |vim.lsp.start_client()| for all available options. The most important are: --- --- - `name` arbitrary name for the LSP client. Should be unique per language server. ---- - `cmd` command (in list form) used to start the language server. Must be absolute, or found on ---- `$PATH`. Shell constructs like `~` are not expanded. +--- - `cmd` command string[] or function, described at |vim.lsp.start_client()|. --- - `root_dir` path to the project root. By default this is used to decide if an existing client --- should be re-used. The example above uses |vim.fs.find()| and |vim.fs.dirname()| to detect the --- root by traversing the file system upwards starting from the current directory until either @@ -844,30 +246,18 @@ end --- Either use |:au|, |nvim_create_autocmd()| or put the call in a --- `ftplugin/<filetype_name>.lua` (See |ftplugin-name|) --- ----@param config table Same configuration as documented in |vim.lsp.start_client()| ----@param opts (nil|lsp.StartOpts) Optional keyword arguments: ---- - reuse_client (fun(client: client, config: table): boolean) ---- Predicate used to decide if a client should be re-used. ---- Used on all running clients. ---- The default implementation re-uses a client if name ---- and root_dir matches. ---- - bufnr (number) ---- Buffer handle to attach to if starting or re-using a ---- client (0 for current). ----@return integer|nil client_id +--- @param config vim.lsp.ClientConfig Configuration for the server. +--- @param opts vim.lsp.start.Opts? Optional keyword arguments +--- @return integer? client_id function lsp.start(config, opts) opts = opts or {} local reuse_client = opts.reuse_client or function(client, conf) - return client.config.root_dir == conf.root_dir and client.name == conf.name + return client.root_dir == conf.root_dir and client.name == conf.name end - if not config.name and type(config.cmd) == 'table' then - config.name = config.cmd[1] and vim.fs.basename(config.cmd[1]) or nil - end - local bufnr = opts.bufnr - if bufnr == nil or bufnr == 0 then - bufnr = api.nvim_get_current_buf() - end + + local bufnr = resolve_bufnr(opts.bufnr) + for _, clients in ipairs({ uninitialized_clients, lsp.get_clients() }) do for _, client in pairs(clients) do if reuse_client(client, config) then @@ -876,10 +266,13 @@ function lsp.start(config, opts) end end end + local client_id = lsp.start_client(config) - if client_id == nil then - return nil -- lsp.start_client will have printed an error + + if not client_id then + return -- lsp.start_client will have printed an error end + lsp.buf_attach_client(bufnr, client_id) return client_id end @@ -890,9 +283,11 @@ end ---@return string function lsp.status() local percentage = nil - local messages = {} + 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 @@ -913,12 +308,15 @@ function lsp.status() end -- Determines whether the given option can be set by `set_defaults`. +---@param bufnr integer +---@param option string +---@return boolean local function is_empty_or_default(bufnr, option) if vim.bo[bufnr][option] == '' then return true end - local info = vim.api.nvim_get_option_info2(option, { buf = bufnr }) + local info = api.nvim_get_option_info2(option, { buf = bufnr }) local scriptinfo = vim.tbl_filter(function(e) return e.sid == info.last_set_sid end, vim.fn.getscriptinfo()) @@ -931,7 +329,8 @@ local function is_empty_or_default(bufnr, option) end ---@private ----@param client lsp.Client +---@param client vim.lsp.Client +---@param bufnr integer function lsp._set_defaults(client, bufnr) if client.supports_method(ms.textDocument_definition) and is_empty_or_default(bufnr, 'tagfunc') @@ -964,818 +363,151 @@ function lsp._set_defaults(client, bufnr) end end ---- @class lsp.ClientConfig ---- @field cmd (string[]|fun(dispatchers: table):table) ---- @field cmd_cwd string ---- @field cmd_env (table) ---- @field detached boolean ---- @field workspace_folders (table) ---- @field capabilities lsp.ClientCapabilities ---- @field handlers table<string,function> ---- @field settings table ---- @field commands table ---- @field init_options table ---- @field name string ---- @field get_language_id fun(bufnr: integer, filetype: string): string ---- @field offset_encoding string ---- @field on_error fun(code: integer) ---- @field before_init function ---- @field on_init function ---- @field on_exit fun(code: integer, signal: integer, client_id: integer) ---- @field on_attach fun(client: lsp.Client, bufnr: integer) ---- @field trace 'off'|'messages'|'verbose'|nil ---- @field flags table ---- @field root_dir string - --- 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 --- not handling method names correctly. If you change the documentation on --- either, please make sure to update the other as well. --- ---- Starts and initializes a client with the given configuration. ---- ---- Field `cmd` in {config} is required. ---- ----@param config (lsp.ClientConfig) Configuration for the server: ---- - cmd: (string[]|fun(dispatchers: table):table) command a list of ---- strings treated like |jobstart()|. The command must launch the language server ---- process. `cmd` can also be a function that creates an RPC client. ---- The function receives a dispatchers table and must return a table with the ---- functions `request`, `notify`, `is_closing` and `terminate` ---- See |vim.lsp.rpc.request()| and |vim.lsp.rpc.notify()| ---- For TCP there is a built-in rpc client factory: |vim.lsp.rpc.connect()| ---- ---- - cmd_cwd: (string, default=|getcwd()|) Directory to launch ---- the `cmd` process. Not related to `root_dir`. ---- ---- - cmd_env: (table) Environment flags to pass to the LSP on ---- spawn. Must be specified using a table. ---- Non-string values are coerced to string. ---- Example: ---- <pre> ---- { PORT = 8080; HOST = "0.0.0.0"; } ---- </pre> ---- ---- - detached: (boolean, default true) 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. ---- ---- - workspace_folders: (table) 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. ---- ---- - capabilities: 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. ---- ---- - handlers: Map of language server method names to |lsp-handler| ---- ---- - settings: Map with language server specific settings. These are ---- returned to the language server if requested via `workspace/configuration`. ---- Keys are case-sensitive. ---- ---- - commands: 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. ---- ---- - init_options Values to pass in the initialization request ---- as `initializationOptions`. See `initialize` in the LSP spec. ---- ---- - name: (string, default=client-id) Name in log messages. ---- ---- - get_language_id: function(bufnr, filetype) -> language ID as string. ---- Defaults to the filetype. ---- ---- - offset_encoding: (default="utf-16") One of "utf-8", "utf-16", ---- or "utf-32" which is the encoding that the LSP server expects. Client does ---- not verify this is correct. ---- ---- - on_error: Callback with parameters (code, ...), 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. ---- ---- - before_init: Callback with parameters (initialize_params, config) ---- 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. ---- ---- - on_init: Callback (client, initialize_result) 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. Most language servers expect to be sent client specified settings after ---- initialization. Nvim does not make this assumption. A ---- `workspace/didChangeConfiguration` notification should be sent ---- to the server during on_init. ---- ---- - on_exit Callback (code, signal, client_id) invoked on client ---- exit. ---- - code: exit code of the process ---- - signal: number describing the signal used to terminate (if any) ---- - client_id: client handle ---- ---- - on_attach: Callback (client, bufnr) invoked when client ---- attaches to a buffer. ---- ---- - trace: ("off" | "messages" | "verbose" | nil) passed directly to the language ---- server in the initialize request. Invalid/empty values will default to "off" ---- ---- - flags: A table with flags for the client. The current (experimental) flags are: ---- - allow_incremental_sync (bool, default true): Allow using incremental sync for buffer edits ---- - debounce_text_changes (number, default 150): Debounce didChange ---- notifications to the server by the given number in milliseconds. No debounce ---- occurs if nil ---- - exit_timeout (number|boolean, default false): 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. ---- ---- - root_dir: (string) Directory where the LSP ---- server will base its workspaceFolders, rootUri, and rootPath ---- on initialization. ---- ----@return integer|nil client_id. |vim.lsp.get_client_by_id()| Note: client may not be ---- fully initialized. Use `on_init` to do any actions once ---- the client has been initialized. -function lsp.start_client(config) - local cmd, cmd_args, offset_encoding = validate_client_config(config) - - config.flags = config.flags or {} - config.settings = config.settings or {} - - -- 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. - config.get_language_id = config.get_language_id or function(_, filetype) - return filetype - end - - local client_id = next_client_id() - - local handlers = config.handlers or {} - local name = config.name or tostring(client_id) - local log_prefix = string.format('LSP[%s]', name) - - local dispatch = {} - - --- 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 The handler for the given method, if defined, or the default from |vim.lsp.handlers| - local function resolve_handler(method) - return handlers[method] or default_handlers[method] - 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 dispatch.notification(method, params) - if log.trace() then - log.trace('notification', method, params) - end - local handler = resolve_handler(method) - if handler then - -- Method name is provided here for convenience. - handler(nil, params, { method = method, client_id = client_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 - function dispatch.server_request(method, params) - if log.trace() then - log.trace('server_request', method, params) - end - local handler = resolve_handler(method) - if handler then - if log.trace() then - log.trace('server_request: found handler for', method) - end - return handler(nil, params, { method = method, client_id = client_id }) - end - if log.warn() then - log.warn('server_request: no handler found for', method) - end - return nil, lsp.rpc_response_error(protocol.ErrorCodes.MethodNotFound) +--- 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 - - --- Logs the given error to the LSP log and to the error buffer. - --- @param code integer Error code - --- @param err any Error arguments - local function write_error(code, err) - if log.error() then - log.error(log_prefix, 'on_error', { code = lsp.client_errors[code], err = err }) - end - err_message(log_prefix, ': Error ', lsp.client_errors[code], ': ', vim.inspect(err)) - end - - ---@private - --- Invoked when the client operation throws an error. - --- - ---@param code (integer) Error code - ---@param err (...) 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 dispatch.on_error(code, err) - write_error(code, err) - if config.on_error then - local status, usererr = pcall(config.on_error, code, err) - if not status then - local _ = log.error() and log.error(log_prefix, 'user on_error failed', { err = usererr }) - err_message(log_prefix, ' user on_error failed: ', tostring(usererr)) - end - end + if vim.bo[bufnr].omnifunc == 'v:lua.vim.lsp.omnifunc' then + vim.bo[bufnr].omnifunc = nil 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) + if vim.bo[bufnr].formatexpr == 'v:lua.vim.lsp.formatexpr()' then + vim.bo[bufnr].formatexpr = nil 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 dispatch.on_exit(code, signal) - if config.on_exit then - pcall(config.on_exit, code, signal, client_id) + 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 - local client = active_clients[client_id] and active_clients[client_id] - or uninitialized_clients[client_id] - - for bufnr, client_ids in pairs(all_buffer_active_clients) do - if client_ids[client_id] then - vim.schedule(function() - if client and client.attached_buffers[bufnr] then - nvim_exec_autocmds('LspDetach', { - buffer = bufnr, - modeline = false, - data = { client_id = client_id }, - }) - end - - local namespace = vim.lsp.diagnostic.get_namespace(client_id) - vim.diagnostic.reset(namespace, bufnr) - - client_ids[client_id] = nil - if vim.tbl_isempty(client_ids) then - reset_defaults(bufnr) - end - end) - end +--- @param client vim.lsp.Client +local function on_client_init(client) + local id = client.id + uninitialized_clients[id] = nil + -- Only assign after initialized. + active_clients[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[id] then + client.on_attach(bufnr) end - - -- Schedule the deletion of the client object so that it exists in the execution of LspDetach - -- autocommands - vim.schedule(function() - active_clients[client_id] = nil - uninitialized_clients[client_id] = nil - - -- Client can be absent if executable starts, but initialize fails - -- init/attach won't have happened - if client then - changetracking.reset(client) - end - if code ~= 0 or (signal ~= 0 and signal ~= 15) then - local msg = string.format( - 'Client %s quit with exit code %s and signal %s. Check log for errors: %s', - name, - code, - signal, - lsp.get_log_path() - ) - vim.notify(msg, vim.log.levels.WARN) - end - end) - end - - -- Start the RPC client. - local rpc - if type(cmd) == 'function' then - rpc = cmd(dispatch) - else - rpc = lsp_rpc.start(cmd, cmd_args, dispatch, { - cwd = config.cmd_cwd, - env = config.cmd_env, - detached = config.detached, - }) end +end - -- Return nil if client fails to start - if not rpc then - return - end - - ---@class lsp.Client - local client = { - id = client_id, - name = name, - rpc = rpc, - offset_encoding = offset_encoding, - config = config, - attached_buffers = {}, - - handlers = handlers, - commands = config.commands or {}, - - --- @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 = require('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() - - -- 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 table[]? - 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 +--- @param code integer +--- @param signal integer +--- @param client_id integer +local function on_client_exit(code, signal, client_id) + local client = active_clients[client_id] or uninitialized_clients[client_id] - 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?: number} - 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) + for bufnr, client_ids in pairs(all_buffer_active_clients) do + if client_ids[client_id] then + vim.schedule(function() + if client and client.attached_buffers[bufnr] then + api.nvim_exec_autocmds('LspDetach', { + buffer = bufnr, + modeline = false, + data = { client_id = client_id }, + }) end - return false - end - end - local _ = log.trace() and log.trace(log_prefix, 'initialize_params', initialize_params) - 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 = 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 - local _ = log.info() - and log.info( - log_prefix, - 'server_capabilities', - { server_capabilities = client.server_capabilities } - ) + local namespace = vim.lsp.diagnostic.get_namespace(client_id) + vim.diagnostic.reset(namespace, bufnr) - -- 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) + client_ids[client_id] = nil + if vim.tbl_isempty(client_ids) then + reset_defaults(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 + local name = client.name or 'unknown' - ---@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 {}) - 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 + -- Schedule the deletion of the client object so that it exists in the execution of LspDetach + -- autocommands + vim.schedule(function() + active_clients[client_id] = nil + uninitialized_clients[client_id] = nil - 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 + -- Client can be absent if executable starts, but initialize fails + -- init/attach won't have happened + if client then + changetracking.reset(client) + end + if code ~= 0 or (signal ~= 0 and signal ~= 15) then + local msg = string.format( + 'Client %s quit with exit code %s and signal %s. Check log for errors: %s', + name, + code, + signal, + lsp.get_log_path() ) - return + vim.notify(msg, vim.log.levels.WARN) 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) + end) +end - nvim_exec_autocmds('LspAttach', { - buffer = bufnr, - modeline = false, - data = { client_id = client.id }, - }) +--- Starts and initializes a client with the given configuration. +--- @param config vim.lsp.ClientConfig Configuration for the server. +--- @return integer|nil client_id |vim.lsp.get_client_by_id()| Note: client may not be +--- fully initialized. Use `on_init` to do any actions once +--- the client has been initialized. +function lsp.start_client(config) + local client = require('vim.lsp.client').create(config) - 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) - end - end + if not client then + return + 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 - semantic_tokens.start(bufnr, client.id) - end - end) + --- @diagnostic disable-next-line: invisible + table.insert(client._on_init_cbs, on_client_init) + --- @diagnostic disable-next-line: invisible + table.insert(client._on_exit_cbs, on_client_exit) - client.attached_buffers[bufnr] = true - end + -- Store the uninitialized_clients for cleanup in case we exit before initialize finishes. + uninitialized_clients[client.id] = client - initialize() + client:initialize() - return client_id + return client.id end ----@private ----@fn text_document_did_change_handler(_, bufnr, changedtick, firstline, lastline, new_lastline, old_byte_size, old_utf32_size, old_utf16_size) --- Notify all attached clients that a buffer has changed. -local text_document_did_change_handler -do - text_document_did_change_handler = function( - _, - bufnr, - changedtick, - firstline, - lastline, - new_lastline - ) - -- Detach (nvim_buf_attach) via returning True to on_lines if no clients are attached - if tbl_isempty(all_buffer_active_clients[bufnr] or {}) then - return true - end - util.buf_versions[bufnr] = changedtick - changetracking.send_changes(bufnr, firstline, lastline, new_lastline) +---@param _ integer +---@param bufnr integer +---@param changedtick integer +---@param firstline integer +---@param lastline integer +---@param new_lastline integer +---@return true? +local function text_document_did_change_handler( + _, + bufnr, + changedtick, + firstline, + lastline, + new_lastline +) + -- Detach (nvim_buf_attach) via returning True to on_lines if no clients are attached + if tbl_isempty(all_buffer_active_clients[bufnr] or {}) then + return true end + util.buf_versions[bufnr] = changedtick + changetracking.send_changes(bufnr, firstline, lastline, new_lastline) end ---Buffer lifecycle handler for textDocument/didSave +--- @param bufnr integer local function text_document_did_save_handler(bufnr) bufnr = resolve_bufnr(bufnr) local uri = vim.uri_from_bufnr(bufnr) - local text = once(buf_get_full_text) + local text = once(lsp._buf_get_full_text) for _, client in ipairs(lsp.get_clients({ bufnr = bufnr })) do local name = api.nvim_buf_get_name(bufnr) local old_name = changetracking._get_and_set_name(client, bufnr, name) @@ -1789,15 +521,15 @@ local function text_document_did_save_handler(bufnr) textDocument = { version = 0, uri = uri, - languageId = client.config.get_language_id(bufnr, vim.bo[bufnr].filetype), - text = buf_get_full_text(bufnr), + languageId = client.get_language_id(bufnr, vim.bo[bufnr].filetype), + text = lsp._buf_get_full_text(bufnr), }, }) util.buf_versions[bufnr] = 0 end local save_capability = vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'save') if save_capability then - local included_text + local included_text --- @type string? if type(save_capability) == 'table' and save_capability.includeText then included_text = text(bufnr) end @@ -1826,8 +558,7 @@ function lsp.buf_attach_client(bufnr, client_id) }) bufnr = resolve_bufnr(bufnr) if not api.nvim_buf_is_loaded(bufnr) then - local _ = log.warn() - and log.warn(string.format('buf_attach_client called on unloaded buffer (id: %d): ', bufnr)) + log.warn(string.format('buf_attach_client called on unloaded buffer (id: %d): ', bufnr)) return false end local buffer_client_ids = all_buffer_active_clients[bufnr] @@ -1884,7 +615,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() @@ -1916,7 +647,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 @@ -1946,7 +677,7 @@ function lsp.buf_detach_client(bufnr, client_id) return end - nvim_exec_autocmds('LspDetach', { + api.nvim_exec_autocmds('LspDetach', { buffer = bufnr, modeline = false, data = { client_id = client_id }, @@ -1985,7 +716,7 @@ end --- ---@param client_id integer client id --- ----@return (nil|lsp.Client) client rpc object +---@return (nil|vim.lsp.Client) client rpc object function lsp.get_client_by_id(client_id) return active_clients[client_id] or uninitialized_clients[client_id] end @@ -2001,7 +732,7 @@ end --- Stops a client(s). --- ---- You can also use the `stop()` function on a |vim.lsp.client| object. +--- You can also use the `stop()` function on a |vim.lsp.Client| object. --- To stop all clients: --- --- ```lua @@ -2011,7 +742,7 @@ end --- By default asks the server to shutdown, unless stop was requested --- already for this client, then force-shutdown is attempted. --- ----@param client_id integer|table id or |vim.lsp.client| object, or list thereof +---@param client_id integer|vim.lsp.Client id or |vim.lsp.Client| object, or list thereof ---@param force boolean|nil shutdown forcefully function lsp.stop_client(client_id, force) local ids = type(client_id) == 'table' and client_id or { client_id } @@ -2026,28 +757,32 @@ function lsp.stop_client(client_id, force) end end ----@class vim.lsp.get_clients.filter ----@field id integer|nil Match clients by id ----@field bufnr integer|nil match clients attached to the given buffer ----@field name string|nil match clients by name ----@field method string|nil match client by supported method name +--- Key-value pairs used to filter the returned clients. +--- @class vim.lsp.get_clients.Filter +--- @inlinedoc +--- +--- Only return clients with the given id +--- @field id? integer +--- +--- Only return clients attached to this buffer +--- @field bufnr? integer +--- +--- Only return clients with the given name +--- @field name? string +--- +--- Only return clients supporting the given method +--- @field method? string --- Get active clients. --- ----@param filter vim.lsp.get_clients.filter|nil (table|nil) A table with ---- key-value pairs used to filter the returned clients. ---- The available keys are: ---- - id (number): Only return clients with the given id ---- - bufnr (number): Only return clients attached to this buffer ---- - name (string): Only return clients with the given name ---- - method (string): Only return clients supporting the given method ----@return lsp.Client[]: List of |vim.lsp.client| objects +---@param filter? vim.lsp.get_clients.Filter +---@return vim.lsp.Client[]: List of |vim.lsp.Client| objects function lsp.get_clients(filter) validate({ filter = { filter, 't', true } }) filter = filter or {} - local clients = {} --- @type lsp.Client[] + local clients = {} --- @type vim.lsp.Client[] local t = filter.bufnr and (all_buffer_active_clients[resolve_bufnr(filter.bufnr)] or {}) or active_clients @@ -2068,7 +803,7 @@ end ---@private ---@deprecated function lsp.get_active_clients(filter) - -- TODO: add vim.deprecate call after 0.10 is out for removal in 0.12 + vim.deprecate('vim.lsp.get_active_clients()', 'vim.lsp.get_clients()', '0.12') return lsp.get_clients(filter) end @@ -2087,12 +822,12 @@ api.nvim_create_autocmd('VimLeavePre', { client.stop() end - local timeouts = {} + local timeouts = {} --- @type table<integer,integer> local max_timeout = 0 local send_kill = false for client_id, client in pairs(active_clients) do - local timeout = if_nil(client.config.flags.exit_timeout, false) + local timeout = if_nil(client.flags.exit_timeout, false) if timeout then send_kill = true timeouts[client_id] = timeout @@ -2134,7 +869,7 @@ api.nvim_create_autocmd('VimLeavePre', { ---@param bufnr (integer) Buffer handle, or 0 for current. ---@param method (string) LSP method name ---@param params table|nil Parameters to send to the server ----@param handler? lsp-handler See |lsp-handler| +---@param handler? lsp.Handler See |lsp-handler| --- If nil, follows resolution strategy defined in |lsp-handler-configuration| --- ---@return table<integer, integer> client_request_ids Map of client-id:request-id pairs @@ -2152,7 +887,7 @@ function lsp.buf_request(bufnr, method, params, handler) bufnr = resolve_bufnr(bufnr) local method_supported = false local clients = lsp.get_clients({ bufnr = bufnr }) - local client_request_ids = {} + local client_request_ids = {} --- @type table<integer,integer> for _, client in ipairs(clients) do if client.supports_method(method, { bufnr = bufnr }) then method_supported = true @@ -2169,7 +904,7 @@ function lsp.buf_request(bufnr, method, params, handler) -- if has client but no clients support the given method, notify the user if next(clients) and not method_supported then vim.notify(lsp._unsupported_method(method), vim.log.levels.ERROR) - nvim_command('redraw') + vim.cmd.redraw() return {}, function() end end @@ -2194,7 +929,7 @@ end --- a `client_id:result` map. ---@return function cancel Function that cancels all requests. function lsp.buf_request_all(bufnr, method, params, handler) - local results = {} + local results = {} --- @type table<integer,{error:string, result:any}> local result_count = 0 local expected_result_count = 0 @@ -2225,16 +960,15 @@ end --- --- Calls |vim.lsp.buf_request_all()| but blocks Nvim while awaiting the result. --- Parameters are the same as |vim.lsp.buf_request_all()| but the result is ---- different. Waits a maximum of {timeout_ms} (default 1000) ms. ---- ----@param bufnr (integer) Buffer handle, or 0 for current. ----@param method (string) LSP method name ----@param params (table|nil) Parameters to send to the server ----@param timeout_ms (integer|nil) Maximum time in milliseconds to wait for a ---- result. Defaults to 1000 ---- ----@return table<integer, {err: lsp.ResponseError, result: any}>|nil (table) result Map of client_id:request_result. ----@return string|nil err On timeout, cancel, or error, `err` is a string describing the failure reason, and `result` is nil. +--- different. Waits a maximum of {timeout_ms}. +--- +---@param bufnr integer Buffer handle, or 0 for current. +---@param method string LSP method name +---@param params table? Parameters to send to the server +---@param timeout_ms integer? Maximum time in milliseconds to wait for a result. +--- (default: `1000`) +---@return table<integer, {err: lsp.ResponseError, result: any}>? result Map of client_id:request_result. +---@return string? err On timeout, cancel, or error, `err` is a string describing the failure reason, and `result` is nil. function lsp.buf_request_sync(bufnr, method, params, timeout_ms) local request_results @@ -2287,21 +1021,24 @@ end --- - findstart=0: column where the completion starts, or -2 or -3 --- - findstart=1: list of matches (actually just calls |complete()|) function lsp.omnifunc(findstart, base) - if log.debug() then - log.debug('omnifunc.findstart', { findstart = findstart, base = base }) - end - return require('vim.lsp._completion').omnifunc(findstart, base) + log.debug('omnifunc.findstart', { findstart = findstart, base = base }) + return vim.lsp._completion.omnifunc(findstart, base) end +--- @class vim.lsp.formatexpr.Opts +--- @inlinedoc +--- +--- The timeout period for the formatting request. +--- (default: 500ms). +--- @field timeout_ms integer + --- Provides an interface between the built-in client and a `formatexpr` function. --- --- Currently only supports a single client. This can be set via --- `setlocal formatexpr=v:lua.vim.lsp.formatexpr()` but will typically or in `on_attach` ---- via ``vim.bo[bufnr].formatexpr = 'v:lua.vim.lsp.formatexpr(#{timeout_ms:250})'``. +--- via `vim.bo[bufnr].formatexpr = 'v:lua.vim.lsp.formatexpr(#{timeout_ms:250})'`. --- ----@param opts table options for customizing the formatting expression which takes the ---- following optional keys: ---- * timeout_ms (default 500ms). The timeout period for the formatting request. +---@param opts? vim.lsp.formatexpr.Opts function lsp.formatexpr(opts) opts = opts or {} local timeout_ms = opts.timeout_ms or 500 @@ -2324,6 +1061,7 @@ function lsp.formatexpr(opts) local params = util.make_formatting_params() local end_line = vim.fn.getline(end_lnum) --[[@as string]] local end_col = util._str_utfindex_enc(end_line, nil, client.offset_encoding) + --- @cast params +lsp.DocumentRangeFormattingParams params.range = { start = { line = start_lnum - 1, @@ -2337,7 +1075,7 @@ function lsp.formatexpr(opts) local response = client.request_sync(ms.textDocument_rangeFormatting, params, timeout_ms, bufnr) if response and response.result then - lsp.util.apply_text_edits(response.result, 0, client.offset_encoding) + lsp.util.apply_text_edits(response.result, bufnr, client.offset_encoding) return 0 end end @@ -2359,7 +1097,7 @@ end --- ---@return table[] tags A list of matching tags function lsp.tagfunc(pattern, flags) - return require('vim.lsp.tagfunc')(pattern, flags) + return vim.lsp._tagfunc(pattern, flags) end ---Checks whether a client is stopped. @@ -2372,13 +1110,14 @@ function lsp.client_is_stopped(client_id) end --- Gets a map of client_id:client pairs for the given buffer, where each value ---- is a |vim.lsp.client| object. +--- is a |vim.lsp.Client| object. --- ---@param bufnr (integer|nil): Buffer handle, or 0 for current ---@return table result is table of (client_id, client) pairs ---@deprecated Use |vim.lsp.get_clients()| instead. function lsp.buf_get_clients(bufnr) - local result = {} + vim.deprecate('vim.lsp.buf_get_clients()', 'vim.lsp.get_clients()', '0.12') + local result = {} --- @type table<integer,vim.lsp.Client> for _, client in ipairs(lsp.get_clients({ bufnr = resolve_bufnr(bufnr) })) do result[client.id] = client end @@ -2428,11 +1167,16 @@ end --- buffer number as arguments. ---@deprecated use lsp.get_clients({ bufnr = bufnr }) with regular loop function lsp.for_each_buffer_client(bufnr, fn) + vim.deprecate( + 'vim.lsp.for_each_buffer_client()', + 'lsp.get_clients({ bufnr = bufnr }) with regular loop', + '0.12' + ) return for_each_buffer_client(bufnr, fn) end --- Function to manage overriding defaults for LSP handlers. ----@param handler (function) See |lsp-handler| +---@param handler (lsp.Handler) See |lsp-handler| ---@param override_config (table) Table containing the keys to override behavior of the {handler} function lsp.with(handler, override_config) return function(err, result, ctx, config) @@ -2497,6 +1241,7 @@ end --- arguments?: any[] --- --- The second argument is the `ctx` of |lsp-handler| +--- @type table<string,function> lsp.commands = setmetatable({}, { __newindex = function(tbl, key, value) assert(type(key) == 'string', 'The key for commands in `vim.lsp.commands` must be a string') |