diff options
author | Josh Rahm <joshuarahm@gmail.com> | 2023-11-29 21:52:58 +0000 |
---|---|---|
committer | Josh Rahm <joshuarahm@gmail.com> | 2023-11-29 21:52:58 +0000 |
commit | 931bffbda3668ddc609fc1da8f9eb576b170aa52 (patch) | |
tree | d8c1843a95da5ea0bb4acc09f7e37843d9995c86 /runtime/lua/vim/lsp.lua | |
parent | 142d9041391780ac15b89886a54015fdc5c73995 (diff) | |
parent | 4a8bf24ac690004aedf5540fa440e788459e5e34 (diff) | |
download | rneovim-userreg.tar.gz rneovim-userreg.tar.bz2 rneovim-userreg.zip |
Merge remote-tracking branch 'upstream/master' into userreguserreg
Diffstat (limited to 'runtime/lua/vim/lsp.lua')
-rw-r--r-- | runtime/lua/vim/lsp.lua | 1063 |
1 files changed, 597 insertions, 466 deletions
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index c5392ac154..261a3aa5de 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -1,19 +1,17 @@ +---@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_buf_get_option, nvim_exec_autocmds = - api.nvim_err_writeln, - api.nvim_buf_get_lines, - api.nvim_command, - api.nvim_buf_get_option, - api.nvim_exec_autocmds -local uv = vim.loop +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 @@ -26,6 +24,7 @@ local lsp = { 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, @@ -38,47 +37,50 @@ local lsp = { -- maps request name to the required server_capability in the client. lsp._request_name_to_capability = { - ['textDocument/hover'] = { 'hoverProvider' }, - ['textDocument/signatureHelp'] = { 'signatureHelpProvider' }, - ['textDocument/definition'] = { 'definitionProvider' }, - ['textDocument/implementation'] = { 'implementationProvider' }, - ['textDocument/declaration'] = { 'declarationProvider' }, - ['textDocument/typeDefinition'] = { 'typeDefinitionProvider' }, - ['textDocument/documentSymbol'] = { 'documentSymbolProvider' }, - ['textDocument/prepareCallHierarchy'] = { 'callHierarchyProvider' }, - ['textDocument/rename'] = { 'renameProvider' }, - ['textDocument/prepareRename'] = { 'renameProvider', 'prepareProvider' }, - ['textDocument/codeAction'] = { 'codeActionProvider' }, - ['textDocument/codeLens'] = { 'codeLensProvider' }, - ['codeLens/resolve'] = { 'codeLensProvider', 'resolveProvider' }, - ['workspace/executeCommand'] = { 'executeCommandProvider' }, - ['workspace/symbol'] = { 'workspaceSymbolProvider' }, - ['textDocument/references'] = { 'referencesProvider' }, - ['textDocument/rangeFormatting'] = { 'documentRangeFormattingProvider' }, - ['textDocument/formatting'] = { 'documentFormattingProvider' }, - ['textDocument/completion'] = { 'completionProvider' }, - ['textDocument/documentHighlight'] = { 'documentHighlightProvider' }, - ['textDocument/semanticTokens/full'] = { 'semanticTokensProvider' }, - ['textDocument/semanticTokens/full/delta'] = { 'semanticTokensProvider' }, + [ms.textDocument_hover] = { 'hoverProvider' }, + [ms.textDocument_signatureHelp] = { 'signatureHelpProvider' }, + [ms.textDocument_definition] = { 'definitionProvider' }, + [ms.textDocument_implementation] = { 'implementationProvider' }, + [ms.textDocument_declaration] = { 'declarationProvider' }, + [ms.textDocument_typeDefinition] = { 'typeDefinitionProvider' }, + [ms.textDocument_documentSymbol] = { 'documentSymbolProvider' }, + [ms.textDocument_prepareCallHierarchy] = { 'callHierarchyProvider' }, + [ms.callHierarchy_incomingCalls] = { 'callHierarchyProvider' }, + [ms.callHierarchy_outgoingCalls] = { 'callHierarchyProvider' }, + [ms.textDocument_rename] = { 'renameProvider' }, + [ms.textDocument_prepareRename] = { 'renameProvider', 'prepareProvider' }, + [ms.textDocument_codeAction] = { 'codeActionProvider' }, + [ms.textDocument_codeLens] = { 'codeLensProvider' }, + [ms.codeLens_resolve] = { 'codeLensProvider', 'resolveProvider' }, + [ms.codeAction_resolve] = { 'codeActionProvider', 'resolveProvider' }, + [ms.workspace_executeCommand] = { 'executeCommandProvider' }, + [ms.workspace_symbol] = { 'workspaceSymbolProvider' }, + [ms.textDocument_references] = { 'referencesProvider' }, + [ms.textDocument_rangeFormatting] = { 'documentRangeFormattingProvider' }, + [ms.textDocument_formatting] = { 'documentFormattingProvider' }, + [ms.textDocument_completion] = { 'completionProvider' }, + [ms.textDocument_documentHighlight] = { 'documentHighlightProvider' }, + [ms.textDocument_semanticTokens_full] = { 'semanticTokensProvider' }, + [ms.textDocument_semanticTokens_full_delta] = { 'semanticTokensProvider' }, + [ms.textDocument_inlayHint] = { 'inlayHintProvider' }, + [ms.textDocument_diagnostic] = { 'diagnosticProvider' }, + [ms.inlayHint_resolve] = { 'inlayHintProvider', 'resolveProvider' }, } -- TODO improve handling of scratch buffers with LSP attached. ----@private --- Concatenates and writes a list of strings to the Vim error buffer. --- ----@param {...} table[] List to write to the 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 ----@private --- Returns the buffer number for the given {bufnr}. --- ----@param bufnr (number|nil) Buffer number to resolve. Defaults to the current ----buffer if not given. ----@returns bufnr (number) Number of requested buffer +---@param bufnr (integer|nil) Buffer number to resolve. Defaults to current buffer +---@return integer bufnr local function resolve_bufnr(bufnr) validate({ bufnr = { bufnr, 'n', true } }) if bufnr == nil or bufnr == 0 then @@ -100,11 +102,10 @@ function lsp._unsupported_method(method) return msg end ----@private --- Checks whether a given path is a directory. --- ---@param filename (string) path to check ----@returns true if {filename} exists and is a directory, false otherwise +---@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) @@ -131,28 +132,27 @@ local format_line_ending = { ['mac'] = '\r', } ----@private ---@param bufnr (number) ----@returns (string) +---@return string local function buf_get_line_ending(bufnr) - return format_line_ending[nvim_buf_get_option(bufnr, 'fileformat')] or '\n' + return format_line_ending[vim.bo[bufnr].fileformat] or '\n' end local client_index = 0 ----@private --- Returns a new, unused client id. --- ----@returns (number) 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 = {} -local all_buffer_active_clients = {} -local uninitialized_clients = {} +local active_clients = {} --- @type table<integer,lsp.Client> +local all_buffer_active_clients = {} --- @type table<integer,table<integer,true>> +local uninitialized_clients = {} --- @type table<integer,lsp.Client> ----@private +---@param bufnr? integer +---@param fn fun(client: lsp.Client, client_id: integer, bufnr: integer) local function for_each_buffer_client(bufnr, fn, restrict_client_ids) validate({ fn = { fn, 'f' }, @@ -165,9 +165,9 @@ local function for_each_buffer_client(bufnr, fn, restrict_client_ids) end if restrict_client_ids and #restrict_client_ids > 0 then - local filtered_client_ids = {} + local filtered_client_ids = {} --- @type table<integer,true> for client_id in pairs(client_ids) do - if vim.tbl_contains(restrict_client_ids, client_id) then + if vim.list_contains(restrict_client_ids, client_id) then filtered_client_ids[client_id] = true end end @@ -182,22 +182,24 @@ local function for_each_buffer_client(bufnr, fn, restrict_client_ids) end 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. +--- 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({ - ON_INIT_CALLBACK_ERROR = table.maxn(lsp_rpc.client_errors) + 1, + 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, }) ) ----@private --- Normalizes {encoding} to valid LSP encoding names. --- ---@param encoding (string) Encoding to normalize ----@returns (string) normalized encoding name +---@return string # normalized encoding name local function validate_encoding(encoding) validate({ encoding = { encoding, 's' }, @@ -215,9 +217,8 @@ end --- 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 (List) ----@returns (string) the command ----@returns (list of strings) its arguments +---@param input string[] +---@return string command, string[] args #the command and arguments function lsp._cmd_parts(input) validate({ cmd = { @@ -241,12 +242,11 @@ function lsp._cmd_parts(input) return cmd, cmd_args end ----@private --- Augments a validator function with support for optional (nil) values. --- ----@param fn (fun(v)) The original validator function; should return a +---@param fn (fun(v): boolean) The original validator function; should return a ---bool. ----@returns (fun(v)) The augmented function. Also returns true if {v} is +---@return fun(v): boolean # The augmented function. Also returns true if {v} is ---`nil`. local function optional_validator(fn) return function(v) @@ -254,14 +254,12 @@ local function optional_validator(fn) end end ----@private --- Validates a client configuration as given to |vim.lsp.start_client()|. --- ----@param config (table) ----@returns (table) "Cleaned" config, containing only the command, its ----arguments, and a valid encoding. ---- ----@see |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' }, @@ -292,48 +290,44 @@ local function validate_client_config(config) 'flags.debounce_text_changes must be a number with the debounce time in milliseconds' ) - local cmd, cmd_args - if type(config.cmd) == 'function' then - cmd = config.cmd + 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) + 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, - cmd_args = cmd_args, - offset_encoding = offset_encoding, - } + 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. ----@returns Buffer text as string. +---@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) - if nvim_buf_get_option(bufnr, 'eol') then + if vim.bo[bufnr].eol then text = text .. line_ending end return text end ----@private --- Memoizes a function. On first run, the function return value is saved and --- immediately returned on subsequent runs. If the function returns a multival, --- only the first returned value will be memoized and returned. The function will only be run once, --- even if it has side effects. --- ----@param fn (function) Function to run ----@returns (function) Memoized function +---@generic T: function +---@param fn (T) Function to run +---@return T local function once(fn) - local value + local value --- @type any local ran = false return function(...) if not ran then @@ -365,7 +359,7 @@ do --- smallest debounce interval is used and we don't group clients by different intervals. --- --- @class CTGroup - --- @field sync_kind number TextDocumentSyncKind, considers config.flags.allow_incremental_sync + --- @field sync_kind integer TextDocumentSyncKind, considers config.flags.allow_incremental_sync --- @field offset_encoding "utf-8"|"utf-16"|"utf-32" --- --- @class CTBufferState @@ -373,17 +367,16 @@ do --- @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|userdata uv_timer + --- @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 number how many clients are using this group + --- @field refs integer how many clients are using this group --- --- @class CTGroupState - --- @field buffers table<number, CTBufferState> - --- @field debounce number debounce duration in ms - --- @field clients table<number, table> clients using this state. {client_id, client} + --- @field buffers table<integer, CTBufferState> + --- @field debounce integer debounce duration in ms + --- @field clients table<integer, table> clients using this state. {client_id, client} - ---@private ---@param group CTGroup ---@return string local function group_key(group) @@ -404,12 +397,10 @@ do end, }) - ---@private ---@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 or {}, 'textDocumentSync', 'change') + 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 @@ -420,7 +411,6 @@ do } end - ---@private ---@param state CTBufferState local function incremental_changes(state, encoding, bufnr, firstline, lastline, new_lastline) local prev_lines = state.lines @@ -544,15 +534,13 @@ do end end - ---@private - -- -- 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 number + ---@param debounce integer ---@param buf_state CTBufferState ---@return number local function next_debounce(debounce, buf_state) @@ -568,9 +556,8 @@ do return math.max(debounce - ms_since_last_flush, 0) end - ---@private - ---@param bufnr number - ---@param sync_kind number protocol.TextDocumentSyncKind + ---@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) @@ -599,7 +586,7 @@ do 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('textDocument/didChange', { + client.notify(ms.textDocument_didChange, { textDocument = { uri = uri, version = util.buf_versions[bufnr], @@ -612,8 +599,8 @@ do ---@private function changetracking.send_changes(bufnr, firstline, lastline, new_lastline) - local groups = {} - for _, client in pairs(lsp.get_active_clients({ bufnr = bufnr })) do + 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 @@ -648,7 +635,7 @@ do if debounce == 0 then send_changes(bufnr, group.sync_kind, state, buf_state) else - local timer = uv.new_timer() + local timer = assert(uv.new_timer(), 'Must be able to create timer') buf_state.timer = timer timer:start( debounce, @@ -695,10 +682,9 @@ do end end ----@private --- Default handler for the 'textDocument/didOpen' LSP notification. --- ----@param bufnr number Number of the buffer, or 0 for current +---@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) @@ -708,7 +694,7 @@ local function text_document_did_open_handler(bufnr, client) if not api.nvim_buf_is_loaded(bufnr) then return end - local filetype = nvim_buf_get_option(bufnr, 'filetype') + local filetype = vim.bo[bufnr].filetype local params = { textDocument = { @@ -718,7 +704,7 @@ local function text_document_did_open_handler(bufnr, client) text = buf_get_full_text(bufnr), }, } - client.notify('textDocument/didOpen', params) + client.notify(ms.textDocument_didOpen, params) util.buf_versions[bufnr] = params.textDocument.version -- Next chance we get, we should re-do the diagnostics @@ -735,7 +721,7 @@ 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_active_clients()|. +--- |vim.lsp.get_client_by_id()| or |vim.lsp.get_clients()|. --- --- - Methods: --- @@ -801,29 +787,39 @@ end --- 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. +--- 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 +--- @field bufnr integer + --- Create a new LSP client and start a language server or reuses an already --- running client if one is found matching `name` and `root_dir`. --- Attaches the current buffer to the client. --- --- Example: ---- <pre>lua +--- +--- ```lua --- vim.lsp.start({ --- name = 'my-server-name', --- cmd = {'name-of-language-server-executable'}, --- root_dir = vim.fs.dirname(vim.fs.find({'pyproject.toml', 'setup.py'}, { upward = true })[1]), --- }) ---- </pre> +--- ``` --- --- See |vim.lsp.start_client()| for all available options. The most important are: --- @@ -849,7 +845,7 @@ end --- `ftplugin/<filetype_name>.lua` (See |ftplugin-name|) --- ---@param config table Same configuration as documented in |vim.lsp.start_client()| ----@param opts nil|table Optional keyword arguments: +---@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. @@ -858,14 +854,13 @@ end --- - bufnr (number) --- Buffer handle to attach to if starting or re-using a --- client (0 for current). ----@return number|nil client_id +---@return integer|nil 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 end - config.name = config.name 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 @@ -873,7 +868,7 @@ function lsp.start(config, opts) if bufnr == nil or bufnr == 0 then bufnr = api.nvim_get_current_buf() end - for _, clients in ipairs({ uninitialized_clients, lsp.get_active_clients() }) do + for _, clients in ipairs({ uninitialized_clients, lsp.get_clients() }) do for _, client in pairs(clients) do if reuse_client(client, config) then lsp.buf_attach_client(bufnr, client.id) @@ -889,6 +884,109 @@ function lsp.start(config, opts) return client_id end +--- Consumes the latest progress messages from all clients and formats them as a string. +--- Empty if there are no clients or if no new messages +--- +---@return string +function lsp.status() + local percentage = nil + local messages = {} + for _, client in ipairs(vim.lsp.get_clients()) do + for progress in client.progress do + local value = progress.value + if type(value) == 'table' and value.kind then + local message = value.message and (value.title .. ': ' .. value.message) or value.title + messages[#messages + 1] = message + if value.percentage then + percentage = math.max(percentage or 0, value.percentage) + end + end + -- else: Doesn't look like work done progress and can be in any format + -- Just ignore it as there is no sensible way to display it + end + end + local message = table.concat(messages, ', ') + if percentage then + return string.format('%3d%%: %s', percentage, message) + end + return message +end + +-- Determines whether the given option can be set by `set_defaults`. +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 scriptinfo = vim.tbl_filter(function(e) + return e.sid == info.last_set_sid + end, vim.fn.getscriptinfo()) + + if #scriptinfo ~= 1 then + return false + end + + return vim.startswith(scriptinfo[1].name, vim.fn.expand('$VIMRUNTIME')) +end + +---@private +---@param client lsp.Client +function lsp._set_defaults(client, bufnr) + if + client.supports_method(ms.textDocument_definition) and is_empty_or_default(bufnr, 'tagfunc') + then + vim.bo[bufnr].tagfunc = 'v:lua.vim.lsp.tagfunc' + end + if + client.supports_method(ms.textDocument_completion) and is_empty_or_default(bufnr, 'omnifunc') + then + vim.bo[bufnr].omnifunc = 'v:lua.vim.lsp.omnifunc' + end + if + client.supports_method(ms.textDocument_rangeFormatting) + and is_empty_or_default(bufnr, 'formatprg') + and is_empty_or_default(bufnr, 'formatexpr') + then + vim.bo[bufnr].formatexpr = 'v:lua.vim.lsp.formatexpr()' + end + api.nvim_buf_call(bufnr, function() + if + client.supports_method(ms.textDocument_hover) + and is_empty_or_default(bufnr, 'keywordprg') + and vim.fn.maparg('K', 'n', false, false) == '' + then + vim.keymap.set('n', 'K', vim.lsp.buf.hover, { buffer = bufnr }) + end + end) + if client.supports_method(ms.textDocument_diagnostic) then + lsp.diagnostic._enable(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 @@ -899,9 +997,9 @@ end --- --- Field `cmd` in {config} is required. --- ----@param config (table) Configuration for the server: ---- - cmd: (table|string|fun(dispatchers: table):table) command string or ---- list treated like |jobstart()|. The command must launch the language server +---@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` @@ -912,11 +1010,11 @@ end --- the `cmd` process. Not related to `root_dir`. --- --- - cmd_env: (table) Environment flags to pass to the LSP on ---- spawn. Can be specified using keys like a map or as a list with `k=v` ---- pairs or both. Non-string values are coerced to string. +--- spawn. Must be specified using a table. +--- Non-string values are coerced to string. --- Example: --- <pre> ---- { "PRODUCTION=true"; "TEST=123"; PORT = 8080; HOST = "0.0.0.0"; } +--- { PORT = 8080; HOST = "0.0.0.0"; } --- </pre> --- --- - detached: (boolean, default true) Daemonize the server process so that it runs in a @@ -929,11 +1027,10 @@ end --- the LSP spec. --- --- - capabilities: Map overriding the default capabilities defined by ---- |vim.lsp.protocol.make_client_capabilities()|, passed to the language +--- \|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.type_idx]=vim.types.dictionary}`, else it will be encoded as an +--- - 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| @@ -977,7 +1074,7 @@ end --- `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. Neovim does not make this assumption. A +--- initialization. Nvim does not make this assumption. A --- `workspace/didChangeConfiguration` notification should be sent --- to the server during on_init. --- @@ -1006,13 +1103,11 @@ end --- server will base its workspaceFolders, rootUri, and rootPath --- on initialization. --- ----@returns Client id. |vim.lsp.get_client_by_id()| Note: client may not be +---@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 cleaned_config = validate_client_config(config) - local cmd, cmd_args, offset_encoding = - cleaned_config.cmd, cleaned_config.cmd_args, cleaned_config.offset_encoding + local cmd, cmd_args, offset_encoding = validate_client_config(config) config.flags = config.flags or {} config.settings = config.settings or {} @@ -1032,12 +1127,11 @@ function lsp.start_client(config) local dispatch = {} - ---@private --- Returns the handler associated with an LSP method. --- Returns the default handler if the user hasn't set a custom one. --- ---@param method (string) LSP method name - ---@returns (fn) The handler for the given method, if defined, or the default from |vim.lsp.handlers| + ---@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 @@ -1049,7 +1143,9 @@ function lsp.start_client(config) ---@param method (string) LSP method name ---@param params (table) The parameters for that method. function dispatch.notification(method, params) - local _ = log.trace() and log.trace('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. @@ -1063,27 +1159,41 @@ function lsp.start_client(config) ---@param method (string) LSP method name ---@param params (table) The parameters for that method function dispatch.server_request(method, params) - local _ = log.trace() and log.trace('server_request', method, params) + if log.trace() then + log.trace('server_request', method, params) + end local handler = resolve_handler(method) if handler then - local _ = log.trace() and log.trace('server_request: found handler for', method) + if log.trace() then + log.trace('server_request: found handler for', method) + end return handler(nil, params, { method = method, client_id = client_id }) end - local _ = log.warn() and log.warn('server_request: no handler found for', method) + if log.warn() then + log.warn('server_request: no handler found for', method) + end return nil, lsp.rpc_response_error(protocol.ErrorCodes.MethodNotFound) 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 (number) Error code + ---@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 + ---@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) - local _ = log.error() - and log.error(log_prefix, 'on_error', { code = lsp.client_errors[code], err = err }) - err_message(log_prefix, ': Error ', lsp.client_errors[code], ': ', vim.inspect(err)) + write_error(code, err) if config.on_error then local status, usererr = pcall(config.on_error, code, err) if not status then @@ -1093,28 +1203,9 @@ function lsp.start_client(config) end end - ---@private - local function set_defaults(client, bufnr) - local capabilities = client.server_capabilities - if capabilities.definitionProvider and vim.bo[bufnr].tagfunc == '' then - vim.bo[bufnr].tagfunc = 'v:lua.vim.lsp.tagfunc' - end - if capabilities.completionProvider and vim.bo[bufnr].omnifunc == '' then - vim.bo[bufnr].omnifunc = 'v:lua.vim.lsp.omnifunc' - end - if - capabilities.documentRangeFormattingProvider - and vim.bo[bufnr].formatprg == '' - and vim.bo[bufnr].formatexpr == '' - then - vim.bo[bufnr].formatexpr = 'v:lua.vim.lsp.formatexpr()' - end - end - - ---@private --- Reset defaults set by `set_defaults`. --- Must only be called if the last client attached to a buffer exits. - local function unset_defaults(bufnr) + local function reset_defaults(bufnr) if vim.bo[bufnr].tagfunc == 'v:lua.vim.lsp.tagfunc' then vim.bo[bufnr].tagfunc = nil end @@ -1124,33 +1215,44 @@ function lsp.start_client(config) if vim.bo[bufnr].formatexpr == 'v:lua.vim.lsp.formatexpr()' then vim.bo[bufnr].formatexpr = nil end + api.nvim_buf_call(bufnr, function() + local keymap = vim.fn.maparg('K', 'n', false, true) + if keymap and keymap.callback == vim.lsp.buf.hover then + vim.keymap.del('n', 'K', { buffer = bufnr }) + end + end) end ---@private --- Invoked on client exit. --- - ---@param code (number) exit code of the process - ---@param signal (number) the signal used to terminate (if any) + ---@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) 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() - nvim_exec_autocmds('LspDetach', { - buffer = bufnr, - modeline = false, - data = { client_id = client_id }, - }) + 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 - unset_defaults(bufnr) + reset_defaults(bufnr) end end) end @@ -1159,8 +1261,6 @@ function lsp.start_client(config) -- Schedule the deletion of the client object so that it exists in the execution of LspDetach -- autocommands vim.schedule(function() - local client = active_clients[client_id] and active_clients[client_id] - or uninitialized_clients[client_id] active_clients[client_id] = nil uninitialized_clients[client_id] = nil @@ -1170,8 +1270,13 @@ function lsp.start_client(config) 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', client_id, code, signal) + 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) @@ -1194,6 +1299,7 @@ function lsp.start_client(config) return end + ---@class lsp.Client local client = { id = client_id, name = name, @@ -1205,26 +1311,44 @@ function lsp.start_client(config) handlers = handlers, commands = config.commands or {}, + --- @type table<integer,{ type: string, bufnr: integer, method: string}> requests = {}, - -- for $/progress report + + --- 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 - ---@private local function initialize() local valid_traces = { off = 'off', messages = 'messages', verbose = 'verbose', } - local version = vim.version() - local workspace_folders - local root_uri - local root_path + 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 = { @@ -1249,12 +1373,12 @@ function lsp.start_client(config) -- 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.getpid(), + processId = uv.os_getpid(), -- Information about the client -- since 3.15.0 clientInfo = { name = 'Neovim', - version = string.format('%s.%s.%s', version.major, version.minor, version.patch), + version = tostring(vim.version()), }, -- The rootPath of the workspace. Is null if no folder is open. -- @@ -1271,15 +1395,37 @@ function lsp.start_client(config) -- User provided initialization options. initializationOptions = config.init_options, -- The capabilities provided by the client (editor or tool) - capabilities = config.capabilities or protocol.make_client_capabilities(), + 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 - -- TODO(ashkan) handle errors here. - pcall(config.before_init, initialize_params, config) + 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) + 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)) @@ -1295,46 +1441,18 @@ function lsp.start_client(config) assert(result.capabilities, "initialize result doesn't contain capabilities") client.server_capabilities = protocol.resolve_capabilities(client.server_capabilities) - -- Deprecation wrapper: this will be removed in 0.8 - local mt = {} - mt.__index = function(table, key) - if key == 'resolved_capabilities' then - vim.notify_once( - '[LSP] Accessing client.resolved_capabilities is deprecated, ' - .. 'update your plugins or configuration to access client.server_capabilities instead.' - .. 'The new key/value pairs in server_capabilities directly match those ' - .. 'defined in the language server protocol', - vim.log.levels.WARN - ) - rawset(table, key, protocol._resolve_capabilities_compat(client.server_capabilities)) - return rawget(table, key) - else - return rawget(table, key) - end - end - setmetatable(client, mt) - - client.supports_method = function(method) - 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 - return false - end + if client.server_capabilities.positionEncoding then + client.offset_encoding = client.server_capabilities.positionEncoding end if next(config.settings) then - client.notify('workspace/didChangeConfiguration', { settings = config.settings }) + 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 - pcall(handlers.on_error, lsp.client_errors.ON_INIT_CALLBACK_ERROR, err) + write_error(lsp.client_errors.ON_INIT_CALLBACK_ERROR, err) end end local _ = log.info() @@ -1357,23 +1475,23 @@ function lsp.start_client(config) end) end - ---@private + ---@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) LSP request params. - ---@param handler (function|nil) Response |lsp-handler| for this method. - ---@param bufnr (number) Buffer handle (0 for current). - ---@returns ({status}, [request_id]): {status} is a bool indicating + ---@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()| + ---@see |vim.lsp.buf_request_all()| function client.request(method, params, handler, bufnr) if not handler then handler = assert( @@ -1383,23 +1501,39 @@ function lsp.start_client(config) 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) - local _ = log.debug() - and log.debug(log_prefix, 'client.request', client_id, method, params, handler, 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) - handler( - err, - result, - { method = method, client_id = client_id, bufnr = bufnr, params = params } - ) + 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 - nvim_exec_autocmds('User', { pattern = 'LspRequest', modeline = false }) end) if success and request_id then - client.requests[request_id] = { type = 'pending', bufnr = bufnr, method = method } - nvim_exec_autocmds('User', { pattern = 'LspRequest', modeline = false }) + 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 @@ -1412,13 +1546,14 @@ function lsp.start_client(config) --- ---@param method (string) LSP method name. ---@param params (table) LSP request params. - ---@param timeout_ms (number|nil) Maximum time in milliseconds to wait for + ---@param timeout_ms (integer|nil) Maximum time in milliseconds to wait for --- a result. Defaults to 1000 - ---@param bufnr (number) Buffer handle (0 for current). - ---@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`. + ---@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 @@ -1444,41 +1579,62 @@ function lsp.start_client(config) return request_result end - ---@private + ---@nodoc --- Sends a notification to an LSP server. --- ---@param method string LSP method name. ---@param params table|nil LSP request params. - ---@returns {status} (bool) true if the notification was successful. + ---@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 ~= 'textDocument/didChange' then + if method ~= ms.textDocument_didChange then changetracking.flush(client) end - return rpc.notify(method, params) + + 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 - ---@private + ---@nodoc --- Cancels a request with a given request id. --- - ---@param id (number) id of request to cancel - ---@returns true if any client returns true; false otherwise + ---@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('User', { pattern = 'LspRequest', modeline = false }) + nvim_exec_autocmds('LspRequest', { + buffer = request.bufnr, + modeline = false, + data = { client_id = client_id, request_id = id, request = request }, + }) end - return rpc.notify('$/cancelRequest', { id = id }) + 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 - ---@private + + ---@nodoc --- Stops a client, optionally with force. --- ---By default, it will just ask the - server to shutdown without force. If @@ -1495,9 +1651,9 @@ function lsp.start_client(config) return end -- Sending a signal after a process has exited is acceptable. - rpc.request('shutdown', nil, function(err, _) + rpc.request(ms.shutdown, nil, function(err, _) if err == nil then - rpc.notify('exit') + rpc.notify(ms.exit) else -- If there was an error in the shutdown request, then term to be safe. rpc.terminate() @@ -1509,19 +1665,59 @@ function lsp.start_client(config) ---@private --- Checks whether a client is stopped. --- - ---@returns (bool) true if client is stopped or in the process of being + ---@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 + + local command_provider = client.server_capabilities.executeCommandProvider + local commands = type(command_provider) == 'table' and command_provider.commands or {} + if not vim.list_contains(commands, cmdname) then + vim.notify_once( + string.format( + 'Language server `%s` does not support command `%s`. This command may require a client extension.', + client.name, + cmdname + ), + vim.log.levels.WARN + ) + return + end + -- Not using command directly to exclude extra properties, + -- see https://github.com/python-lsp/python-lsp-server/issues/146 + local params = { + command = command.command, + arguments = command.arguments, + } + client.request(ms.workspace_executeCommand, params, handler, context.bufnr) + end + + ---@private --- Runs the on_attach function from the client's config if it was defined. - ---@param bufnr (number) Buffer number + ---@param bufnr integer Buffer number function client._on_attach(bufnr) text_document_did_open_handler(bufnr, client) - set_defaults(client, bufnr) + lsp._set_defaults(client, bufnr) nvim_exec_autocmds('LspAttach', { buffer = bufnr, @@ -1530,8 +1726,10 @@ function lsp.start_client(config) }) if config.on_attach then - -- TODO(ashkan) handle errors. - pcall(config.on_attach, client, bufnr) + local status, err = pcall(config.on_attach, client, bufnr) + if not status then + write_error(lsp.client_errors.ON_ATTACH_ERROR, err) + end end -- schedule the initialization of semantic tokens to give the above @@ -1573,17 +1771,21 @@ do end end ----@private ---Buffer lifecycle handler for textDocument/didSave 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) - for_each_buffer_client(bufnr, function(client) + 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) if old_name and name ~= old_name then - client.notify('textDocument/didOpen', { + client.notify(ms.textDocument_didClose, { + textDocument = { + uri = vim.uri_from_fname(old_name), + }, + }) + client.notify(ms.textDocument_didOpen, { textDocument = { version = 0, uri = uri, @@ -1599,14 +1801,14 @@ local function text_document_did_save_handler(bufnr) if type(save_capability) == 'table' and save_capability.includeText then included_text = text(bufnr) end - client.notify('textDocument/didSave', { + client.notify(ms.textDocument_didSave, { textDocument = { uri = uri, }, text = included_text, }) end - end) + end end --- Implements the `textDocument/did…` notifications required to track a buffer @@ -1614,8 +1816,9 @@ end --- --- Without calling this, the server won't be notified of changes to a buffer. --- ----@param bufnr (number) Buffer handle, or 0 for current ----@param client_id (number) Client id +---@param bufnr (integer) Buffer handle, or 0 for current +---@param client_id (integer) Client id +---@return boolean success `true` if client was attached successfully; `false` otherwise function lsp.buf_attach_client(bufnr, client_id) validate({ bufnr = { bufnr, 'n', true }, @@ -1641,7 +1844,7 @@ function lsp.buf_attach_client(bufnr, client_id) buffer = bufnr, desc = 'vim.lsp: textDocument/willSave', callback = function(ctx) - for_each_buffer_client(ctx.buf, function(client) + for _, client in ipairs(lsp.get_clients({ bufnr = ctx.buf })) do local params = { textDocument = { uri = uri, @@ -1649,18 +1852,18 @@ function lsp.buf_attach_client(bufnr, client_id) reason = protocol.TextDocumentSaveReason.Manual, } if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'willSave') then - client.notify('textDocument/willSave', params) + client.notify(ms.textDocument_willSave, params) end if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'willSaveWaitUntil') then local result, err = - client.request_sync('textDocument/willSaveWaitUntil', params, 1000, ctx.buf) + client.request_sync(ms.textDocument_willSaveWaitUntil, params, 1000, ctx.buf) if result and result.result then util.apply_text_edits(result.result, ctx.buf, client.offset_encoding) elseif err then log.error(vim.inspect(err)) end end - end) + end end, }) api.nvim_create_autocmd('BufWritePost', { @@ -1676,23 +1879,23 @@ function lsp.buf_attach_client(bufnr, client_id) on_lines = text_document_did_change_handler, on_reload = function() local params = { textDocument = { uri = uri } } - for_each_buffer_client(bufnr, function(client, _) + for _, client in ipairs(lsp.get_clients({ bufnr = bufnr })) do changetracking.reset_buf(client, bufnr) if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'openClose') then - client.notify('textDocument/didClose', params) + client.notify(ms.textDocument_didClose, params) end text_document_did_open_handler(bufnr, client) - end) + end end, on_detach = function() local params = { textDocument = { uri = uri } } - for_each_buffer_client(bufnr, function(client, _) + for _, client in ipairs(lsp.get_clients({ bufnr = bufnr })) do changetracking.reset_buf(client, bufnr) if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'openClose') then - client.notify('textDocument/didClose', params) + client.notify(ms.textDocument_didClose, params) end client.attached_buffers[bufnr] = nil - end) + end util.buf_versions[bufnr] = nil all_buffer_active_clients[bufnr] = nil end, @@ -1704,7 +1907,7 @@ function lsp.buf_attach_client(bufnr, client_id) end if buffer_client_ids[client_id] then - return + return true end -- This is our first time attaching this client to this buffer. buffer_client_ids[client_id] = true @@ -1722,8 +1925,8 @@ end --- Note: While the server is notified that the text document (buffer) --- was closed, it is still able to send notifications should it ignore this notification. --- ----@param bufnr number Buffer handle, or 0 for current ----@param client_id number Client id +---@param bufnr integer Buffer handle, or 0 for current +---@param client_id integer Client id function lsp.buf_detach_client(bufnr, client_id) validate({ bufnr = { bufnr, 'n', true }, @@ -1736,8 +1939,8 @@ function lsp.buf_detach_client(bufnr, client_id) vim.notify( string.format( 'Buffer (id: %d) is not attached to client (id: %d). Cannot detach.', - client_id, - bufnr + bufnr, + client_id ) ) return @@ -1754,7 +1957,7 @@ function lsp.buf_detach_client(bufnr, client_id) if vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'openClose') then local uri = vim.uri_from_bufnr(bufnr) local params = { textDocument = { uri = uri } } - client.notify('textDocument/didClose', params) + client.notify(ms.textDocument_didClose, params) end client.attached_buffers[bufnr] = nil @@ -1765,16 +1968,14 @@ function lsp.buf_detach_client(bufnr, client_id) all_buffer_active_clients[bufnr] = nil end - local namespace = vim.lsp.diagnostic.get_namespace(client_id) + local namespace = lsp.diagnostic.get_namespace(client_id) vim.diagnostic.reset(namespace, bufnr) - - vim.notify(string.format('Detached buffer (id: %d) from client (id: %d)', bufnr, client_id)) end --- Checks if a buffer is attached for a particular client. --- ----@param bufnr (number) Buffer handle, or 0 for current ----@param client_id (number) the client id +---@param bufnr (integer) Buffer handle, or 0 for current +---@param client_id (integer) the client id function lsp.buf_is_attached(bufnr, client_id) return (all_buffer_active_clients[resolve_bufnr(bufnr)] or {})[client_id] == true end @@ -1782,17 +1983,17 @@ end --- Gets a client by id, or nil if the id is invalid. --- The returned client may not yet be fully initialized. --- ----@param client_id number client id +---@param client_id integer client id --- ----@returns |vim.lsp.client| object, or nil +---@return (nil|lsp.Client) client rpc object function lsp.get_client_by_id(client_id) return active_clients[client_id] or uninitialized_clients[client_id] end --- Returns list of buffers attached to client_id. --- ----@param client_id number client id ----@returns list of buffer ids +---@param client_id integer client id +---@return integer[] buffers list of buffer ids function lsp.get_buffers_by_client_id(client_id) local client = lsp.get_client_by_id(client_id) return client and vim.tbl_keys(client.attached_buffers) or {} @@ -1802,14 +2003,15 @@ end --- --- You can also use the `stop()` function on a |vim.lsp.client| object. --- To stop all clients: ---- <pre>lua ---- vim.lsp.stop_client(vim.lsp.get_active_clients()) ---- </pre> +--- +--- ```lua +--- vim.lsp.stop_client(vim.lsp.get_clients()) +--- ``` --- --- By default asks the server to shutdown, unless stop was requested --- already for this client, then force-shutdown is attempted. --- ----@param client_id number|table id or |vim.lsp.client| object, or list thereof +---@param client_id integer|table 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 } @@ -1824,26 +2026,28 @@ function lsp.stop_client(client_id, force) end end ----@class vim.lsp.get_active_clients.filter ----@field id number|nil Match clients by id ----@field bufnr number|nil match clients attached to the given buffer +---@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 --- Get active clients. --- ----@param filter vim.lsp.get_active_clients.filter|nil (table|nil) A table with +---@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 ----@returns (table) List of |vim.lsp.client| objects -function lsp.get_active_clients(filter) +--- - method (string): Only return clients supporting the given method +---@return lsp.Client[]: List of |vim.lsp.client| objects +function lsp.get_clients(filter) validate({ filter = { filter, 't', true } }) filter = filter or {} - local clients = {} + local clients = {} --- @type lsp.Client[] local t = filter.bufnr and (all_buffer_active_clients[resolve_bufnr(filter.bufnr)] or {}) or active_clients @@ -1853,6 +2057,7 @@ function lsp.get_active_clients(filter) client and (filter.id == nil or client.id == filter.id) and (filter.name == nil or client.name == filter.name) + and (filter.method == nil or client.supports_method(filter.method, { bufnr = filter.bufnr })) then clients[#clients + 1] = client end @@ -1860,6 +2065,13 @@ function lsp.get_active_clients(filter) return clients end +---@private +---@deprecated +function lsp.get_active_clients(filter) + -- TODO: add vim.deprecate call after 0.10 is out for removal in 0.12 + return lsp.get_clients(filter) +end + api.nvim_create_autocmd('VimLeavePre', { desc = 'vim.lsp: exit handler', callback = function() @@ -1890,7 +2102,6 @@ api.nvim_create_autocmd('VimLeavePre', { local poll_time = 50 - ---@private local function check_clients_closed() for client_id, timeout in pairs(timeouts) do timeouts[client_id] = timeout - poll_time @@ -1920,16 +2131,17 @@ api.nvim_create_autocmd('VimLeavePre', { --- Sends an async request for all active clients attached to the --- buffer. --- ----@param bufnr (number) Buffer handle, or 0 for current. +---@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 function|nil See |lsp-handler| +---@param handler? lsp-handler See |lsp-handler| --- If nil, follows resolution strategy defined in |lsp-handler-configuration| --- ----@returns 2-tuple: ---- - Map of client-id:request-id pairs for all successful requests. ---- - Function which can be used to cancel all the requests. You could instead ---- iterate all clients and call their `cancel_request()` methods. +---@return table<integer, integer> client_request_ids Map of client-id:request-id pairs +---for all successful requests. +---@return function _cancel_all_requests Function which can be used to +---cancel all the requests. You could instead +---iterate all clients and call their `cancel_request()` methods. function lsp.buf_request(bufnr, method, params, handler) validate({ bufnr = { bufnr, 'n', true }, @@ -1937,34 +2149,30 @@ function lsp.buf_request(bufnr, method, params, handler) handler = { handler, 'f', true }, }) - local supported_clients = {} + bufnr = resolve_bufnr(bufnr) local method_supported = false - for_each_buffer_client(bufnr, function(client, client_id) - if client.supports_method(method) then + local clients = lsp.get_clients({ bufnr = bufnr }) + local client_request_ids = {} + for _, client in ipairs(clients) do + if client.supports_method(method, { bufnr = bufnr }) then method_supported = true - table.insert(supported_clients, client_id) + + local request_success, request_id = client.request(method, params, handler, bufnr) + -- This could only fail if the client shut down in the time since we looked + -- it up and we did the request, which should be rare. + if request_success then + client_request_ids[client.id] = request_id + end end - end) + end -- if has client but no clients support the given method, notify the user - if - not tbl_isempty(all_buffer_active_clients[resolve_bufnr(bufnr)] or {}) and not method_supported - then + if next(clients) and not method_supported then vim.notify(lsp._unsupported_method(method), vim.log.levels.ERROR) nvim_command('redraw') return {}, function() end end - local client_request_ids = {} - for_each_buffer_client(bufnr, function(client, client_id, resolved_bufnr) - local request_success, request_id = client.request(method, params, handler, resolved_bufnr) - -- This could only fail if the client shut down in the time since we looked - -- it up and we did the request, which should be rare. - if request_success then - client_request_ids[client_id] = request_id - end - end, supported_clients) - local function _cancel_all_requests() for client_id, request_id in pairs(client_request_ids) do local client = active_clients[client_id] @@ -1975,39 +2183,36 @@ function lsp.buf_request(bufnr, method, params, handler) return client_request_ids, _cancel_all_requests end ----Sends an async request for all active clients attached to the buffer. ----Executes the callback on the combined result. ----Parameters are the same as |vim.lsp.buf_request()| but the return result and callback are ----different. +--- Sends an async request for all active clients attached to the buffer and executes the `handler` +--- callback with the combined result. --- ----@param bufnr (number) Buffer handle, or 0 for current. +---@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 callback (function) The callback to call when all requests are finished. --- Unlike `buf_request`, this will collect all the responses from each server instead of handling them. --- A map of client_id:request_result will be provided to the callback --- ----@returns (function) A function that will cancel all requests which is the same as the one returned from `buf_request`. -function lsp.buf_request_all(bufnr, method, params, callback) - local request_results = {} +---@param handler fun(results: table<integer, {error: lsp.ResponseError, result: any}>) (function) +--- Handler called after all requests are completed. Server results are passed as +--- 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 result_count = 0 local expected_result_count = 0 local set_expected_result_count = once(function() - for_each_buffer_client(bufnr, function(client) - if client.supports_method(method) then + for _, client in ipairs(lsp.get_clients({ bufnr = bufnr })) do + if client.supports_method(method, { bufnr = bufnr }) then expected_result_count = expected_result_count + 1 end - end) + end end) local function _sync_handler(err, result, ctx) - request_results[ctx.client_id] = { error = err, result = result } + results[ctx.client_id] = { error = err, result = result } result_count = result_count + 1 set_expected_result_count() if result_count >= expected_result_count then - callback(request_results) + handler(results) end end @@ -2019,18 +2224,17 @@ end --- Sends a request to all server and waits for the response of all of them. --- --- Calls |vim.lsp.buf_request_all()| but blocks Nvim while awaiting the result. ---- Parameters are the same as |vim.lsp.buf_request()| but the return result is ---- different. Wait maximum of {timeout_ms} (default 1000) ms. +--- 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 (number) Buffer handle, or 0 for current. +---@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 (number|nil) Maximum time in milliseconds to wait for a +---@param timeout_ms (integer|nil) Maximum time in milliseconds to wait for a --- result. Defaults to 1000 --- ----@returns Map of client_id:request_result. On timeout, cancel or error, ---- returns `(nil, err)` where `err` is a string describing the failure ---- reason. +---@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. function lsp.buf_request_sync(bufnr, method, params, timeout_ms) local request_results @@ -2051,41 +2255,23 @@ function lsp.buf_request_sync(bufnr, method, params, timeout_ms) end --- Send a notification to a server ----@param bufnr (number|nil) The number of the buffer +---@param bufnr (integer|nil) The number of the buffer ---@param method (string) Name of the request method ---@param params (any) Arguments to send to the server --- ----@returns true if any client returns true; false otherwise +---@return boolean success true if any client returns true; false otherwise function lsp.buf_notify(bufnr, method, params) validate({ bufnr = { bufnr, 'n', true }, method = { method, 's' }, }) local resp = false - for_each_buffer_client(bufnr, function(client, _client_id, _resolved_bufnr) + for _, client in ipairs(lsp.get_clients({ bufnr = bufnr })) do if client.rpc.notify(method, params) then resp = true end - end) - return resp -end - ----@private -local function adjust_start_col(lnum, line, items, encoding) - local min_start_char = nil - for _, item in pairs(items) do - if item.filterText == nil and item.textEdit and item.textEdit.range.start.line == lnum - 1 then - if min_start_char and min_start_char ~= item.textEdit.range.start.character then - return nil - end - min_start_char = item.textEdit.range.start.character - end - end - if min_start_char then - return util._str_byteindex_enc(line, min_start_char, encoding) - else - return nil end + return resp end --- Implements 'omnifunc' compatible LSP completion. @@ -2094,80 +2280,24 @@ end ---@see |complete-items| ---@see |CompleteDone| --- ----@param findstart number 0 or 1, decides behavior ----@param base number findstart=0, text to match against +---@param findstart integer 0 or 1, decides behavior +---@param base integer findstart=0, text to match against --- ----@returns (number) Decided by {findstart}: +---@return integer|table Decided by {findstart}: --- - 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) - local _ = log.debug() and log.debug('omnifunc.findstart', { findstart = findstart, base = base }) - - local bufnr = resolve_bufnr() - local has_buffer_clients = not tbl_isempty(all_buffer_active_clients[bufnr] or {}) - if not has_buffer_clients then - if findstart == 1 then - return -1 - else - return {} - end + if log.debug() then + log.debug('omnifunc.findstart', { findstart = findstart, base = base }) end - - -- Then, perform standard completion request - local _ = log.info() and log.info('base ', base) - - local pos = api.nvim_win_get_cursor(0) - local line = api.nvim_get_current_line() - local line_to_cursor = line:sub(1, pos[2]) - local _ = log.trace() and log.trace('omnifunc.line', pos, line) - - -- Get the start position of the current keyword - local textMatch = vim.fn.match(line_to_cursor, '\\k*$') - - local params = util.make_position_params() - - local items = {} - lsp.buf_request(bufnr, 'textDocument/completion', params, function(err, result, ctx) - if err or not result or vim.fn.mode() ~= 'i' then - return - end - - -- Completion response items may be relative to a position different than `textMatch`. - -- Concrete example, with sumneko/lua-language-server: - -- - -- require('plenary.asy| - -- ▲ ▲ ▲ - -- │ │ └── cursor_pos: 20 - -- │ └────── textMatch: 17 - -- └────────────── textEdit.range.start.character: 9 - -- .newText = 'plenary.async' - -- ^^^ - -- prefix (We'd remove everything not starting with `asy`, - -- so we'd eliminate the `plenary.async` result - -- - -- `adjust_start_col` is used to prefer the language server boundary. - -- - local client = lsp.get_client_by_id(ctx.client_id) - local encoding = client and client.offset_encoding or 'utf-16' - local candidates = util.extract_completion_items(result) - local startbyte = adjust_start_col(pos[1], line, candidates, encoding) or textMatch - local prefix = line:sub(startbyte + 1, pos[2]) - local matches = util.text_document_completion_list_to_complete_items(result, prefix) - -- TODO(ashkan): is this the best way to do this? - vim.list_extend(items, matches) - vim.fn.complete(startbyte + 1, items) - end) - - -- Return -2 to signal that we should continue completion so that we can - -- async complete. - return -2 + return require('vim.lsp._completion').omnifunc(findstart, base) end --- 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.api.nvim_buf_set_option(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: @@ -2176,7 +2306,7 @@ function lsp.formatexpr(opts) opts = opts or {} local timeout_ms = opts.timeout_ms or 500 - if vim.tbl_contains({ 'i', 'R', 'ic', 'ix' }, vim.fn.mode()) then + if vim.list_contains({ 'i', 'R', 'ic', 'ix' }, vim.fn.mode()) then -- `formatexpr` is also called when exceeding `textwidth` in insert mode -- fall back to internal formatting return 1 @@ -2189,10 +2319,10 @@ function lsp.formatexpr(opts) return 0 end local bufnr = api.nvim_get_current_buf() - for _, client in pairs(lsp.get_active_clients({ bufnr = bufnr })) do - if client.supports_method('textDocument/rangeFormatting') then + for _, client in pairs(lsp.get_clients({ bufnr = bufnr })) do + if client.supports_method(ms.textDocument_rangeFormatting) then local params = util.make_formatting_params() - local end_line = vim.fn.getline(end_lnum) + local end_line = vim.fn.getline(end_lnum) --[[@as string]] local end_col = util._str_utfindex_enc(end_line, nil, client.offset_encoding) params.range = { start = { @@ -2205,9 +2335,9 @@ function lsp.formatexpr(opts) }, } local response = - client.request_sync('textDocument/rangeFormatting', params, timeout_ms, bufnr) - if response.result then - vim.lsp.util.apply_text_edits(response.result, 0, client.offset_encoding) + 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) return 0 end end @@ -2227,39 +2357,41 @@ end ---@param pattern string Pattern used to find a workspace symbol ---@param flags string See |tag-function| --- ----@returns A list of matching tags -function lsp.tagfunc(...) - return require('vim.lsp.tagfunc')(...) +---@return table[] tags A list of matching tags +function lsp.tagfunc(pattern, flags) + return require('vim.lsp.tagfunc')(pattern, flags) end ---Checks whether a client is stopped. --- ----@param client_id (number) ----@returns true if client is stopped, false otherwise. +---@param client_id (integer) +---@return boolean stopped true if client is stopped, false otherwise. function lsp.client_is_stopped(client_id) - return active_clients[client_id] == nil + assert(client_id, 'missing client_id param') + return active_clients[client_id] == nil and not uninitialized_clients[client_id] end --- Gets a map of client_id:client pairs for the given buffer, where each value --- is a |vim.lsp.client| object. --- ----@param bufnr (number|nil): Buffer handle, or 0 for current ----@returns (table) Table of (client_id, client) pairs ----@deprecated Use |vim.lsp.get_active_clients()| instead. +---@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 = {} - for _, client in ipairs(lsp.get_active_clients({ bufnr = resolve_bufnr(bufnr) })) do + for _, client in ipairs(lsp.get_clients({ bufnr = resolve_bufnr(bufnr) })) do result[client.id] = client end return result end --- Log level dictionary with reverse lookup as well. --- --- Can be used to lookup the number from the name or the --- name from the number. --- Levels by name: "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "OFF" --- Level numbers begin with "TRACE" at 0 +--- Log level dictionary with reverse lookup as well. +--- +--- Can be used to lookup the number from the name or the +--- name from the number. +--- Levels by name: "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "OFF" +--- Level numbers begin with "TRACE" at 0 +--- @nodoc lsp.log_levels = log.levels --- Sets the global log level for LSP logging. @@ -2272,7 +2404,7 @@ lsp.log_levels = log.levels --- ---@see |vim.lsp.log_levels| --- ----@param level (number|string) the case insensitive level name or number +---@param level (integer|string) the case insensitive level name or number function lsp.set_log_level(level) if type(level) == 'string' or type(level) == 'number' then log.set_level(level) @@ -2282,22 +2414,19 @@ function lsp.set_log_level(level) end --- Gets the path of the logfile used by the LSP client. ----@returns (String) Path to logfile. +---@return string path to log file function lsp.get_log_path() return log.get_filename() end +---@private --- Invokes a function for each LSP client attached to a buffer. --- ----@param bufnr number Buffer number +---@param bufnr integer Buffer number ---@param fn function Function to run on each client attached to buffer --- {bufnr}. The function takes the client, client ID, and ---- buffer number as arguments. Example: ---- <pre>lua ---- vim.lsp.for_each_buffer_client(0, function(client, client_id, bufnr) ---- print(vim.inspect(client)) ---- end) ---- </pre> +--- buffer number as arguments. +---@deprecated use lsp.get_clients({ bufnr = bufnr }) with regular loop function lsp.for_each_buffer_client(bufnr, fn) return for_each_buffer_client(bufnr, fn) end @@ -2316,10 +2445,13 @@ end --- are valid keys and make sense to include for this handler. --- --- Will error on invalid keys (i.e. keys that do not exist in the options) +--- @param name string +--- @param options table<string,any> +--- @param user_config table<string,any> function lsp._with_extend(name, options, user_config) user_config = user_config or {} - local resulting_config = {} + local resulting_config = {} --- @type table<string,any> for k, v in pairs(user_config) do if options[k] == nil then error( @@ -2374,4 +2506,3 @@ lsp.commands = setmetatable({}, { }) return lsp --- vim:sw=2 ts=2 et |