diff options
| author | Josh Rahm <joshuarahm@gmail.com> | 2023-11-30 20:35:25 +0000 |
|---|---|---|
| committer | Josh Rahm <joshuarahm@gmail.com> | 2023-11-30 20:35:25 +0000 |
| commit | 1b7b916b7631ddf73c38e3a0070d64e4636cb2f3 (patch) | |
| tree | cd08258054db80bb9a11b1061bb091c70b76926a /runtime/lua/vim/lsp.lua | |
| parent | eaa89c11d0f8aefbb512de769c6c82f61a8baca3 (diff) | |
| parent | 4a8bf24ac690004aedf5540fa440e788459e5e34 (diff) | |
| download | rneovim-aucmd_textputpost.tar.gz rneovim-aucmd_textputpost.tar.bz2 rneovim-aucmd_textputpost.zip | |
Merge remote-tracking branch 'upstream/master' into aucmd_textputpostaucmd_textputpost
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) |