diff options
Diffstat (limited to 'runtime/lua/vim/lsp/client.lua')
-rw-r--r-- | runtime/lua/vim/lsp/client.lua | 307 |
1 files changed, 185 insertions, 122 deletions
diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua index a279be55e9..a460d95cc6 100644 --- a/runtime/lua/vim/lsp/client.lua +++ b/runtime/lua/vim/lsp/client.lua @@ -6,28 +6,33 @@ local ms = lsp.protocol.Methods local changetracking = lsp._changetracking local validate = vim.validate +--- @alias vim.lsp.client.on_init_cb fun(client: lsp.Client, initialize_result: lsp.InitializeResult) +--- @alias vim.lsp.client.on_attach_cb fun(client: lsp.Client, bufnr: integer) +--- @alias vim.lsp.client.on_exit_cb fun(code: integer, signal: integer, client_id: integer) +--- @alias vim.lsp.client.before_init_cb fun(params: lsp.InitializeParams, config: lsp.ClientConfig) + --- @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 cmd string[]|fun(dispatchers: vim.lsp.rpc.Dispatchers): vim.lsp.rpc.PublicClient? +--- @field cmd_cwd? string +--- @field cmd_env? table +--- @field detached? boolean +--- @field workspace_folders? lsp.WorkspaceFolder[] +--- @field capabilities? lsp.ClientCapabilities +--- @field handlers? table<string,function> +--- @field settings? table +--- @field commands? table<string,fun(command: lsp.Command, ctx: 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 fun(params: lsp.InitializeParams, config: lsp.ClientConfig) ---- @field on_init fun(client: lsp.Client, initialize_result: lsp.InitializeResult) ---- @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 +--- @field get_language_id? fun(bufnr: integer, filetype: string): string +--- @field offset_encoding? string +--- @field on_error? fun(code: integer, err: string) +--- @field before_init? vim.lsp.client.before_init_cb +--- @field on_init? elem_or_list<vim.lsp.client.on_init_cb> +--- @field on_exit? elem_or_list<vim.lsp.client.on_exit_cb> +--- @field on_attach? elem_or_list<vim.lsp.client.on_attach_cb> +--- @field trace? 'off'|'messages'|'verbose' +--- @field flags? table +--- @field root_dir? string --- @class lsp.Client.Progress: vim.Ringbuf<{token: integer|string, value: any}> --- @field pending table<lsp.ProgressToken,lsp.LSPAny> @@ -66,21 +71,43 @@ local validate = vim.validate --- --- Response from the server sent on --- initialize` describing the server's capabilities. ---- @field server_capabilities lsp.ServerCapabilities +--- @field server_capabilities lsp.ServerCapabilities? --- --- A ring buffer (|vim.ringbuf()|) containing progress messages --- sent by the server. --- @field progress lsp.Client.Progress --- --- @field initialized true? +--- +--- The workspace folders configured in the client when the server starts. +--- This property is only available if the client supports workspace folders. +--- It can be `null` if the client supports workspace folders but none are +--- configured. --- @field workspace_folders lsp.WorkspaceFolder[]? +--- @field root_dir string +--- --- @field attached_buffers table<integer,true> --- @field private _log_prefix string +--- --- Track this so that we can escalate automatically if we've already tried a --- graceful shutdown --- @field private _graceful_shutdown_failed true? ---- @field private commands table --- +--- The initial trace setting. If omitted trace is disabled ("off"). +--- trace = "off" | "messages" | "verbose"; +--- @field private _trace 'off'|'messages'|'verbose' +--- +--- Table of command name to function which is called if any LSP action +--- (code action, code lenses, ...) triggers the command. +--- Client commands take precedence over the global command registry. +--- @field commands table<string,fun(command: lsp.Command, ctx: table)> +--- +--- @field settings table +--- @field flags table +--- @field get_language_id fun(bufnr: integer, filetype: string): string +--- +--- The capabilities provided by the client (editor or tool) +--- @field capabilities lsp.ClientCapabilities --- @field dynamic_capabilities lsp.DynamicCapabilities --- --- Sends a request to the server. @@ -122,6 +149,12 @@ local validate = vim.validate --- Useful for buffer-local setup. --- @field on_attach fun(bufnr: integer) --- +--- @field private _before_init_cb? vim.lsp.client.before_init_cb +--- @field private _on_attach_cbs vim.lsp.client.on_attach_cb[] +--- @field private _on_init_cbs vim.lsp.client.on_init_cb[] +--- @field private _on_exit_cbs vim.lsp.client.on_exit_cb[] +--- @field private _on_error_cb? fun(code: integer, err: string) +--- --- Checks if a client supports a given method. --- Always returns true for unknown off-spec methods. --- [opts] is a optional `{bufnr?: integer}` table. @@ -196,9 +229,18 @@ local function optional_validator(fn) end end +--- By default, get_language_id just returns the exact filetype it is passed. +--- It is possible to pass in something that will calculate a different filetype, +--- to be sent by the client. +--- @param _bufnr integer +--- @param filetype string +local function default_get_language_id(_bufnr, filetype) + return filetype +end + --- Validates a client configuration as given to |vim.lsp.start_client()|. --- @param config lsp.ClientConfig -local function process_client_config(config) +local function validate_config(config) validate({ config = { config, 't' }, }) @@ -210,15 +252,17 @@ local function process_client_config(config) detached = { config.detached, 'b', true }, name = { config.name, 's', true }, on_error = { config.on_error, 'f', true }, - on_exit = { config.on_exit, 'f', true }, - on_init = { config.on_init, 'f', true }, + on_exit = { config.on_exit, { 'f', 't' }, true }, + on_init = { config.on_init, { 'f', 't' }, true }, + on_attach = { config.on_attach, { 'f', 't' }, true }, settings = { config.settings, 't', true }, commands = { config.commands, 't', true }, - before_init = { config.before_init, 'f', true }, + before_init = { config.before_init, { 'f', 't' }, true }, offset_encoding = { config.offset_encoding, 's', true }, flags = { config.flags, 't', true }, get_language_id = { config.get_language_id, 'f', true }, }) + assert( ( not config.flags @@ -227,51 +271,98 @@ local function process_client_config(config) ), 'flags.debounce_text_changes must be a number with the debounce time in milliseconds' ) +end + +--- @param trace string +--- @return 'off'|'messages'|'verbose' +local function get_trace(trace) + local valid_traces = { + off = 'off', + messages = 'messages', + verbose = 'verbose', + } + return trace and valid_traces[trace] or 'off' +end - if not config.name and type(config.cmd) == 'table' then - config.name = config.cmd[1] and vim.fs.basename(config.cmd[1]) or nil +--- @param id integer +--- @param config lsp.ClientConfig +--- @return string +local function get_name(id, config) + local name = config.name + if name then + return name end - config.offset_encoding = validate_encoding(config.offset_encoding) - config.flags = config.flags or {} - config.settings = config.settings or {} - config.handlers = config.handlers or {} + if type(config.cmd) == 'table' and config.cmd[1] then + return assert(vim.fs.basename(config.cmd[1])) + end - -- By default, get_language_id just returns the exact filetype it is passed. - -- It is possible to pass in something that will calculate a different filetype, - -- to be sent by the client. - config.get_language_id = config.get_language_id or function(_, filetype) - return filetype + return tostring(id) +end + +--- @param workspace_folders lsp.WorkspaceFolder[]? +--- @param root_dir string? +--- @return lsp.WorkspaceFolder[]? +local function get_workspace_folders(workspace_folders, root_dir) + if workspace_folders then + return workspace_folders end + if root_dir then + return { + { + uri = vim.uri_from_fname(root_dir), + name = string.format('%s', root_dir), + }, + } + end +end - config.capabilities = config.capabilities or lsp.protocol.make_client_capabilities() - config.commands = config.commands or {} +--- @generic T +--- @param x elem_or_list<T>? +--- @return T[] +local function ensure_list(x) + if type(x) == 'table' then + return x + end + return { x } end --- @package --- @param config lsp.ClientConfig --- @return lsp.Client? -function Client.start(config) - process_client_config(config) +function Client.create(config) + validate_config(config) client_index = client_index + 1 local id = client_index - - local name = config.name or tostring(id) + local name = get_name(id, config) --- @class lsp.Client local self = { id = id, config = config, - handlers = config.handlers, - offset_encoding = config.offset_encoding, + handlers = config.handlers or {}, + offset_encoding = validate_encoding(config.offset_encoding), name = name, _log_prefix = string.format('LSP[%s]', name), requests = {}, attached_buffers = {}, server_capabilities = {}, - dynamic_capabilities = vim.lsp._dynamic.new(id), - commands = config.commands, -- Remove in Nvim 0.11 + dynamic_capabilities = lsp._dynamic.new(id), + commands = config.commands or {}, + settings = config.settings or {}, + flags = config.flags or {}, + get_language_id = config.get_language_id or default_get_language_id, + capabilities = config.capabilities or lsp.protocol.make_client_capabilities(), + workspace_folders = get_workspace_folders(config.workspace_folders, config.root_dir), + root_dir = config.root_dir, + _before_init_cb = config.before_init, + _on_init_cbs = ensure_list(config.on_init), + _on_exit_cbs = ensure_list(config.on_exit), + _on_attach_cbs = ensure_list(config.on_attach), + _on_error_cb = config.on_error, + _root_dir = config.root_dir, + _trace = get_trace(config.trace), --- Contains $/progress report messages. --- They have the format {token: integer|string, value: any} @@ -327,41 +418,31 @@ function Client.start(config) setmetatable(self, Client) - self:initialize() - return self end ---- @private -function Client:initialize() - local valid_traces = { - off = 'off', - messages = 'messages', - verbose = 'verbose', - } +--- @param cbs function[] +--- @param error_id integer +--- @param ... any +function Client:_run_callbacks(cbs, error_id, ...) + for _, cb in pairs(cbs) do + --- @type boolean, string? + local status, err = pcall(cb, ...) + if not status then + self:write_error(error_id, err) + end + end +end +--- @package +function Client:initialize() local config = self.config - local workspace_folders --- @type lsp.WorkspaceFolder[]? local root_uri --- @type string? local root_path --- @type string? - if config.workspace_folders or config.root_dir then - if config.root_dir and not config.workspace_folders then - workspace_folders = { - { - uri = vim.uri_from_fname(config.root_dir), - name = string.format('%s', config.root_dir), - }, - } - else - workspace_folders = config.workspace_folders - end - root_uri = workspace_folders[1].uri + if self.workspace_folders then + root_uri = self.workspace_folders[1].uri root_path = vim.uri_to_fname(root_uri) - else - workspace_folders = nil - root_uri = nil - root_path = nil end local initialize_params = { @@ -383,26 +464,19 @@ function Client:initialize() -- The rootUri of the workspace. Is null if no folder is open. If both -- `rootPath` and `rootUri` are set `rootUri` wins. rootUri = root_uri or vim.NIL, - -- The workspace folders configured in the client when the server starts. - -- This property is only available if the client supports workspace folders. - -- It can be `null` if the client supports workspace folders but none are - -- configured. - workspaceFolders = workspace_folders or vim.NIL, + workspaceFolders = self.workspace_folders or vim.NIL, -- User provided initialization options. initializationOptions = config.init_options, - -- The capabilities provided by the client (editor or tool) - capabilities = config.capabilities, - -- The initial trace setting. If omitted trace is disabled ("off"). - -- trace = "off" | "messages" | "verbose"; - trace = valid_traces[config.trace] or 'off', + capabilities = self.capabilities, + trace = self._trace, } - if config.before_init then - --- @type boolean, string? - local status, err = pcall(config.before_init, initialize_params, config) - if not status then - self:write_error(lsp.client_errors.BEFORE_INIT_CALLBACK_ERROR, err) - end - end + + self:_run_callbacks( + { self._before_init_cb }, + lsp.client_errors.BEFORE_INIT_CALLBACK_ERROR, + initialize_params, + config + ) log.trace(self._log_prefix, 'initialize_params', initialize_params) @@ -413,7 +487,6 @@ function Client:initialize() assert(result, 'server sent empty result') rpc.notify('initialized', vim.empty_dict()) self.initialized = true - self.workspace_folders = workspace_folders -- These are the cleaned up capabilities we use for dynamically deciding -- when to send certain events to clients. @@ -425,17 +498,11 @@ function Client:initialize() self.offset_encoding = self.server_capabilities.positionEncoding end - if next(config.settings) then - self:_notify(ms.workspace_didChangeConfiguration, { settings = config.settings }) + if next(self.settings) then + self:_notify(ms.workspace_didChangeConfiguration, { settings = self.settings }) end - if config.on_init then - --- @type boolean, string? - local status, err = pcall(config.on_init, self, result) - if not status then - self:write_error(lsp.client_errors.ON_INIT_CALLBACK_ERROR, err) - end - end + self:_run_callbacks(self._on_init_cbs, lsp.client_errors.ON_INIT_CALLBACK_ERROR, self, result) log.info( self._log_prefix, @@ -672,7 +739,7 @@ function Client:_is_stopped() return self.rpc.is_closing() end ---- @private +--- @package --- Execute a lsp command, either via client command function (if available) --- or via workspace/executeCommand (if supported by the server) --- @@ -684,7 +751,7 @@ function Client:_exec_cmd(command, context, handler) context.bufnr = context.bufnr or api.nvim_get_current_buf() context.client_id = self.id local cmdname = command.command - local fn = self.config.commands[cmdname] or lsp.commands[cmdname] + local fn = self.commands[cmdname] or lsp.commands[cmdname] if fn then fn(command, context) return @@ -730,7 +797,7 @@ function Client:_text_document_did_open_handler(bufnr) textDocument = { version = 0, uri = vim.uri_from_bufnr(bufnr), - languageId = self.config.get_language_id(bufnr, filetype), + languageId = self.get_language_id(bufnr, filetype), text = lsp._buf_get_full_text(bufnr), }, } @@ -742,13 +809,13 @@ function Client:_text_document_did_open_handler(bufnr) -- Protect against a race where the buffer disappears -- between `did_open_handler` and the scheduled function firing. if api.nvim_buf_is_valid(bufnr) then - local namespace = vim.lsp.diagnostic.get_namespace(self.id) + local namespace = lsp.diagnostic.get_namespace(self.id) vim.diagnostic.show(namespace, bufnr) end end) end ---- @private +--- @package --- Runs the on_attach function from the client's config if it was defined. --- @param bufnr integer Buffer number function Client:_on_attach(bufnr) @@ -762,13 +829,7 @@ function Client:_on_attach(bufnr) data = { client_id = self.id }, }) - if self.config.on_attach then - --- @type boolean, string? - local status, err = pcall(self.config.on_attach, self, bufnr) - if not status then - self:write_error(lsp.client_errors.ON_ATTACH_ERROR, err) - end - end + self:_run_callbacks(self._on_attach_cbs, lsp.client_errors.ON_ATTACH_ERROR, self, bufnr) -- schedule the initialization of semantic tokens to give the above -- on_attach and LspAttach callbacks the ability to schedule wrap the @@ -795,7 +856,6 @@ end --- @param method string --- @param opts? {bufnr: integer?} function Client:_supports_method(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 @@ -803,12 +863,11 @@ function Client:_supports_method(method, opts) end if vim.tbl_get(self.server_capabilities, unpack(required_capability)) then return true - else - if self.dynamic_capabilities:supports_registration(method) then - return self.dynamic_capabilities:supports(method, opts) - end - return false end + if self.dynamic_capabilities:supports_registration(method) then + return self.dynamic_capabilities:supports(method, opts) + end + return false end --- @private @@ -853,9 +912,9 @@ end --- `vim.lsp.rpc.client_errors[code]` to get a human-friendly name. function Client:_on_error(code, err) self:write_error(code, err) - if self.config.on_error then + if self._on_error_cb then --- @type boolean, string - local status, usererr = pcall(self.config.on_error, code, err) + local status, usererr = pcall(self._on_error_cb, code, err) if not status then log.error(self._log_prefix, 'user on_error failed', { err = usererr }) err_message(self._log_prefix, ' user on_error failed: ', tostring(usererr)) @@ -869,9 +928,13 @@ end --- @param code integer) exit code of the process --- @param signal integer the signal used to terminate (if any) function Client:_on_exit(code, signal) - if self.config.on_exit then - pcall(self.config.on_exit, code, signal, self.id) - end + self:_run_callbacks( + self._on_exit_cbs, + lsp.client_errors.ON_EXIT_CALLBACK_ERROR, + code, + signal, + self.id + ) end return Client |