diff options
| author | Josh Rahm <joshuarahm@gmail.com> | 2024-03-09 14:57:57 -0700 |
|---|---|---|
| committer | Josh Rahm <joshuarahm@gmail.com> | 2024-03-09 14:57:57 -0700 |
| commit | c324271b99eee4c621463f368914d57cd729bd9c (patch) | |
| tree | 5d979d333a2d5f9c080991d5482fd5916f8579c6 /runtime/lua/vim/lsp.lua | |
| parent | 931bffbda3668ddc609fc1da8f9eb576b170aa52 (diff) | |
| parent | ade1b12f49c3b3914c74847d791eb90ea90b56b7 (diff) | |
| download | rneovim-c324271b99eee4c621463f368914d57cd729bd9c.tar.gz rneovim-c324271b99eee4c621463f368914d57cd729bd9c.tar.bz2 rneovim-c324271b99eee4c621463f368914d57cd729bd9c.zip | |
Merge remote-tracking branch 'upstream/master' into userreg
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 initializa |