From 4ecc71f6fc7377403ed91ae5bc32992a5d08f678 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Wed, 7 Jun 2023 13:39:41 +0100 Subject: [PATCH] fix(lsp): reduce diagnostics and add more types (#23948) --- runtime/doc/lsp.txt | 13 +-- runtime/lua/vim/lsp.lua | 144 +++++++++++++++++++------------ runtime/lua/vim/lsp/protocol.lua | 1 + 3 files changed, 100 insertions(+), 58 deletions(-) diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt index 581cfd5348..5b7c013c57 100644 --- a/runtime/doc/lsp.txt +++ b/runtime/doc/lsp.txt @@ -858,7 +858,7 @@ get_active_clients({filter}) *vim.lsp.get_active_clients()* • name (string): Only return clients with the given name Return: ~ - (table) List of |vim.lsp.client| objects + lsp.Client []: List of |vim.lsp.client| objects *vim.lsp.get_buffers_by_client_id()* get_buffers_by_client_id({client_id}) @@ -961,7 +961,7 @@ start({config}, {opts}) *vim.lsp.start()* Parameters: ~ • {config} (table) Same configuration as documented in |vim.lsp.start_client()| - • {opts} nil|table Optional keyword arguments: + • {opts} (nil|lsp.StartOpts) Optional keyword arguments: • reuse_client (fun(client: client, config: table): boolean) Predicate used to decide if a client should be re-used. Used on all running clients. The default implementation @@ -970,7 +970,7 @@ start({config}, {opts}) *vim.lsp.start()* re-using a client (0 for current). Return: ~ - (number|nil) client_id + (integer|nil) client_id start_client({config}) *vim.lsp.start_client()* Starts and initializes a client with the given configuration. @@ -978,7 +978,7 @@ start_client({config}) *vim.lsp.start_client()* Field `cmd` in {config} is required. Parameters: ~ - • {config} (table) Configuration for the server: + • {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 @@ -1108,7 +1108,7 @@ stop_client({client_id}, {force}) *vim.lsp.stop_client()* thereof • {force} (boolean|nil) shutdown forcefully -tagfunc({...}) *vim.lsp.tagfunc()* +tagfunc({pattern}, {flags}) *vim.lsp.tagfunc()* Provides an interface between the built-in client and 'tagfunc'. When used with normal mode commands (e.g. |CTRL-]|) this will invoke the @@ -2168,6 +2168,9 @@ make_client_capabilities() Gets a new ClientCapabilities object describing the LSP client capabilities. + Return: ~ + lsp.ClientCapabilities + *vim.lsp.protocol.resolve_capabilities()* resolve_capabilities({server_capabilities}) Creates a normalized object describing LSP server capabilities. diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 5f7a95ae14..532504a7db 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -147,9 +147,9 @@ local function next_client_id() 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 +local all_buffer_active_clients = {} --- @type table> +local uninitialized_clients = {} --- @type table ---@private ---@param bufnr? integer @@ -166,7 +166,7 @@ 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 for client_id in pairs(client_ids) do if vim.list_contains(restrict_client_ids, client_id) then filtered_client_ids[client_id] = true @@ -257,9 +257,10 @@ end ---@private --- Validates a client configuration as given to |vim.lsp.start_client()|. --- ----@param config (table) ----@return table config Cleaned config, containing the command, its ----arguments, and a valid encoding. +---@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' }, @@ -290,22 +291,19 @@ 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 @@ -328,10 +326,11 @@ end --- 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 ----@return function fn 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 @@ -371,7 +370,7 @@ 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|uv.uv_timer_t uv_timer + --- @field timer nil|uv_timer_t uv_timer --- @field last_flush nil|number uv.hrtime of the last flush/didChange-notification --- @field needs_flush boolean true if buffer updates haven't been sent to clients/servers yet --- @field refs integer how many clients are using this group @@ -610,7 +609,7 @@ do ---@private function changetracking.send_changes(bufnr, firstline, lastline, new_lastline) - local groups = {} + local groups = {} ---@type table for _, client in pairs(lsp.get_active_clients({ bufnr = bufnr })) do local group = get_group(client) groups[group_key(group)] = group @@ -812,6 +811,10 @@ 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. @@ -849,7 +852,7 @@ end --- `ftplugin/.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 +861,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 @@ -930,6 +932,29 @@ function lsp._set_defaults(client, bufnr) end end +--- @class lsp.ClientConfig +--- @field cmd (string[]|fun(dispatchers: table):table) +--- @field cmd_cwd string +--- @field cmd_env (table) +--- @field detached boolean +--- @field workspace_folders (table) +--- @field capabilities lsp.ClientCapabilities +--- @field handlers table +--- @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 @@ -940,7 +965,7 @@ end --- --- Field `cmd` in {config} is required. --- ----@param config (table) Configuration for the 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. @@ -970,7 +995,7 @@ 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 @@ -1051,9 +1076,7 @@ end --- 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 {} @@ -1090,7 +1113,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. @@ -1104,13 +1129,19 @@ 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 @@ -1122,8 +1153,9 @@ function lsp.start_client(config) ---@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 }) + 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)) if config.on_error then local status, usererr = pcall(config.on_error, code, err) @@ -1232,11 +1264,13 @@ function lsp.start_client(config) handlers = handlers, commands = config.commands or {}, + --- @type table requests = {}, -- for $/progress report messages = { name = name, messages = {}, progress = {}, status = {} }, dynamic_capabilities = require('vim.lsp._dynamic').new(client_id), } + --- @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. @@ -1251,9 +1285,9 @@ function lsp.start_client(config) } 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 = { @@ -1278,7 +1312,7 @@ 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 = { @@ -1405,8 +1439,9 @@ function lsp.start_client(config) -- Ensure pending didChange notifications are sent so that the server doesn't operate on a stale state changetracking.flush(client, 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, @@ -1879,13 +1914,13 @@ end --- - 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 +---@return lsp.Client[]: List of |vim.lsp.client| objects function lsp.get_active_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 @@ -2143,20 +2178,20 @@ end --- - findstart=0: column where the completion starts, or -2 or -3 --- - findstart=1: list of matches (actually just calls |complete()|) function lsp.omnifunc(findstart, base) - local _ = log.debug() and log.debug('omnifunc.findstart', { findstart = findstart, base = base }) + if log.debug() then + log.debug('omnifunc.findstart', { findstart = findstart, base = base }) + end 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 + return findstart == 1 and -1 or {} end -- Then, perform standard completion request - local _ = log.info() and log.info('base ', base) + if log.info() then + log.info('base ', base) + end local pos = api.nvim_win_get_cursor(0) local line = api.nvim_get_current_line() @@ -2270,8 +2305,8 @@ end ---@param flags string See |tag-function| --- ---@return table[] tags A list of matching tags -function lsp.tagfunc(...) - return require('vim.lsp.tagfunc')(...) +function lsp.tagfunc(pattern, flags) + return require('vim.lsp.tagfunc')(pattern, flags) end ---Checks whether a client is stopped. @@ -2359,10 +2394,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 +--- @param user_config table function lsp._with_extend(name, options, user_config) user_config = user_config or {} - local resulting_config = {} + local resulting_config = {} --- @type table for k, v in pairs(user_config) do if options[k] == nil then error( diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index 7558b5c6ba..172d43e483 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -632,6 +632,7 @@ export interface WorkspaceClientCapabilities { --- Gets a new ClientCapabilities object describing the LSP client --- capabilities. +--- @return lsp.ClientCapabilities function protocol.make_client_capabilities() return { general = {