From 59cf827f99d53ec8dbb90e48a7561c0cb8b8ca6f Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Wed, 7 Feb 2024 17:22:03 +0000 Subject: refactor(lsp): move client code to a regular Lua class Problem: The LSP client code is implemented as a complicated closure-class (class defined in a single function). Solution: Move LSP client code to a more conventional Lua class and move to a separate file. --- runtime/lua/vim/lsp/client.lua | 663 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 663 insertions(+) create mode 100644 runtime/lua/vim/lsp/client.lua (limited to 'runtime/lua/vim/lsp/client.lua') diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua new file mode 100644 index 0000000000..2daf564f63 --- /dev/null +++ b/runtime/lua/vim/lsp/client.lua @@ -0,0 +1,663 @@ +local uv = vim.uv +local api = vim.api +local lsp = vim.lsp +local log = lsp.log +local ms = lsp.protocol.Methods +local changetracking = lsp._changetracking + +--- @class lsp.Client.Progress: vim.Ringbuf<{token: integer|string, value: any}> +--- @field pending table + +--- @class lsp.Client +--- +--- The id allocated to the client. +--- @field id integer +--- +--- If a name is specified on creation, that will be used. Otherwise it is just +--- the client id. This is used for logs and messages. +--- @field name string +--- +--- RPC client object, for low level interaction with the client. +--- See |vim.lsp.rpc.start()|. +--- @field rpc vim.lsp.rpc.PublicClient +--- +--- The encoding used for communicating with the server. You can modify this in +--- the `config`'s `on_init` method before text is sent to the server. +--- @field offset_encoding string +--- +--- The handlers used by the client as described in |lsp-handler|. +--- @field handlers table +--- +--- The current pending requests in flight to the server. Entries are key-value +--- pairs with the key being the request ID while the value is a table with +--- `type`, `bufnr`, and `method` key-value pairs. `type` is either "pending" +--- for an active request, or "cancel" for a cancel request. It will be +--- "complete" ephemerally while executing |LspRequest| autocmds when replies +--- are received from the server. +--- @field requests table +--- +--- copy of the table that was passed by the user +--- to |vim.lsp.start_client()|. +--- @field config lsp.ClientConfig +--- +--- Response from the server sent on +--- initialize` describing the server's capabilities. +--- @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? +--- @field workspace_folders lsp.WorkspaceFolder[]? +--- @field attached_buffers table +--- @field commands table +--- @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 dynamic_capabilities lsp.DynamicCapabilities +--- +--- Sends a request to the server. +--- This is a thin wrapper around {client.rpc.request} with some additional +--- checking. +--- If {handler} is not specified, If one is not found there, then an error +--- will occur. Returns: {status}, {[client_id]}. {status} is a boolean +--- indicating if the notification was successful. If it is `false`, then it +--- will always be `false` (the client has shutdown). +--- If {status} is `true`, the function returns {request_id} as the second +--- result. You can use this with `client.cancel_request(request_id)` to cancel +--- the request. +--- @field request fun(method: string, params: table?, handler: lsp.Handler?, bufnr: integer): boolean, integer? +--- +--- Sends a request to the server and synchronously waits for the response. +--- This is a wrapper around {client.request} +--- Returns: { err=err, result=result }, a dictionary, where `err` and `result` +--- come from the |lsp-handler|. On timeout, cancel or error, returns `(nil, +--- err)` where `err` is a string describing the failure reason. If the request +--- was unsuccessful returns `nil`. +--- @field request_sync fun(method: string, params: table?, timeout_ms: integer?, bufnr: integer): {err: lsp.ResponseError|nil, result:any}|nil, string|nil err # a dictionary, where +--- +--- Sends a notification to an LSP server. +--- Returns: a boolean to indicate if the notification was successful. If +--- it is false, then it will always be false (the client has shutdown). +--- @field notify fun(method: string, params: table?): boolean +--- +--- Cancels a request with a given request id. +--- Returns: same as `notify()`. +--- @field cancel_request fun(id: integer): boolean +--- +--- Stops a client, optionally with force. +--- By default, it will just ask the server to shutdown without force. +--- If you request to stop a client which has previously been requested to +--- shutdown, it will automatically escalate and force shutdown. +--- @field stop fun(force?: boolean) +--- +--- Runs the on_attach function from the client's config if it was defined. +--- Useful for buffer-local setup. +--- @field on_attach fun(bufnr: integer) +--- +--- Checks if a client supports a given method. +--- Always returns true for unknown off-spec methods. +--- [opts] is a optional `{bufnr?: integer}` table. +--- Some language server capabilities can be file specific. +--- @field supports_method fun(method: string, opts?: {bufnr: integer?}): boolean +--- +--- Checks whether a client is stopped. +--- Returns: true if the client is fully stopped. +--- @field is_stopped fun(): boolean +local Client = {} +Client.__index = Client + +--- @param cls table +--- @param meth any +--- @return function +local function method_wrapper(cls, meth) + return function(...) + return meth(cls, ...) + end +end + +--- @package +--- @param id integer +--- @param rpc vim.lsp.rpc.PublicClient +--- @param handlers table +--- @param offset_encoding string +--- @param config lsp.ClientConfig +--- @return lsp.Client +function Client.new(id, rpc, handlers, offset_encoding, config) + local name = config.name + + --- @class lsp.Client + local self = { + id = id, + config = config, + handlers = handlers, + rpc = rpc, + offset_encoding = offset_encoding, + name = name, + _log_prefix = string.format('LSP[%s]', name), + requests = {}, + commands = config.commands or {}, + attached_buffers = {}, + server_capabilities = {}, + dynamic_capabilities = vim.lsp._dynamic.new(id), + + --- 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) --[[@as lsp.Client.Progress]], + + --- @deprecated use client.progress instead + messages = { name = name, messages = {}, progress = {}, status = {} }, + } + + self.request = method_wrapper(self, Client._request) + self.request_sync = method_wrapper(self, Client._request_sync) + self.notify = method_wrapper(self, Client._notify) + self.cancel_request = method_wrapper(self, Client._cancel_request) + self.stop = method_wrapper(self, Client._stop) + self.is_stopped = method_wrapper(self, Client._is_stopped) + self.on_attach = method_wrapper(self, Client._on_attach) + self.supports_method = method_wrapper(self, Client._supports_method) + + ---@type table title of unfinished progress sequences by token + self.progress.pending = {} + + return setmetatable(self, Client) +end + +--- @private +--- @param cb fun() +function Client:initialize(cb) + local valid_traces = { + off = 'off', + messages = 'messages', + verbose = 'verbose', + } + + 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 + root_path = vim.uri_to_fname(root_uri) + else + workspace_folders = nil + root_uri = nil + root_path = nil + end + + local initialize_params = { + -- The process Id of the parent process that started the server. Is null if + -- 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.os_getpid(), + -- Information about the client + -- since 3.15.0 + clientInfo = { + name = 'Neovim', + version = tostring(vim.version()), + }, + -- The rootPath of the workspace. Is null if no folder is open. + -- + -- @deprecated in favour of rootUri. + rootPath = root_path or vim.NIL, + -- 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, + -- 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', + } + 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 + + if log.trace() then + log.trace(self._log_prefix, 'initialize_params', initialize_params) + end + + local rpc = self.rpc + + rpc.request('initialize', initialize_params, function(init_err, result) + assert(not init_err, tostring(init_err)) + 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. + self.server_capabilities = + assert(result.capabilities, "initialize result doesn't contain capabilities") + self.server_capabilities = assert(lsp.protocol.resolve_capabilities(self.server_capabilities)) + + if self.server_capabilities.positionEncoding then + self.offset_encoding = self.server_capabilities.positionEncoding + end + + if next(config.settings) then + self:_notify(ms.workspace_didChangeConfiguration, { settings = config.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 + if log.info() then + log.info( + self._log_prefix, + 'server_capabilities', + { server_capabilities = self.server_capabilities } + ) + end + + cb() + end) +end + +--- @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 +--- @return lsp.Handler|nil handler for the given method, if defined, or the default from |vim.lsp.handlers| +function Client:_resolve_handler(method) + return self.handlers[method] or lsp.handlers[method] +end + +--- Returns the buffer number for the given {bufnr}. +--- +--- @param bufnr (integer|nil) Buffer number to resolve. Defaults to current buffer +--- @return integer bufnr +local function resolve_bufnr(bufnr) + vim.validate({ bufnr = { bufnr, 'n', true } }) + if bufnr == nil or bufnr == 0 then + return api.nvim_get_current_buf() + end + return bufnr +end + +--- @private +--- 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|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_all()| +function Client:_request(method, params, handler, bufnr) + if not handler then + handler = assert( + self:_resolve_handler(method), + string.format('not found: %q request handler for client %q.', method, self.name) + ) + end + -- Ensure pending didChange notifications are sent so that the server doesn't operate on a stale state + changetracking.flush(self, bufnr) + local version = lsp.util.buf_versions[bufnr] + bufnr = resolve_bufnr(bufnr) + if log.debug() then + log.debug(self._log_prefix, 'client.request', self.id, method, params, handler, bufnr) + end + local success, request_id = self.rpc.request(method, params, function(err, result) + local context = { + method = method, + client_id = self.id, + bufnr = bufnr, + params = params, + version = version, + } + handler(err, result, context) + end, function(request_id) + local request = self.requests[request_id] + request.type = 'complete' + api.nvim_exec_autocmds('LspRequest', { + buffer = api.nvim_buf_is_valid(bufnr) and bufnr or nil, + modeline = false, + data = { client_id = self.id, request_id = request_id, request = request }, + }) + self.requests[request_id] = nil + end) + + if success and request_id then + local request = { type = 'pending', bufnr = bufnr, method = method } + self.requests[request_id] = request + api.nvim_exec_autocmds('LspRequest', { + buffer = bufnr, + modeline = false, + data = { client_id = self.id, request_id = request_id, request = request }, + }) + end + + return success, request_id +end + +-- TODO(lewis6991): duplicated from lsp.lua +local wait_result_reason = { [-1] = 'timeout', [-2] = 'interrupted', [-3] = 'error' } + +-- TODO(lewis6991): duplicated from lsp.lua +--- Concatenates and writes a list of strings to the Vim error buffer. +--- +---@param ... string List to write to the buffer +local function err_message(...) + api.nvim_err_writeln(table.concat(vim.tbl_flatten({ ... }))) + api.nvim_command('redraw') +end + +--- @private +--- Sends a request to the server and synchronously waits for the response. +--- +--- This is a wrapper around {client.request} +--- +--- @param method (string) LSP method name. +--- @param params (table) LSP request params. +--- @param timeout_ms (integer|nil) Maximum time in milliseconds to wait for +--- a result. Defaults to 1000 +--- @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 + local function _sync_handler(err, result) + request_result = { err = err, result = result } + end + + local success, request_id = self:_request(method, params, _sync_handler, bufnr) + if not success then + return nil + end + + local wait_result, reason = vim.wait(timeout_ms or 1000, function() + return request_result ~= nil + end, 10) + + if not wait_result then + if request_id then + self:_cancel_request(request_id) + end + return nil, wait_result_reason[reason] + end + return request_result +end + +--- @private +--- Sends a notification to an LSP server. +--- +--- @param method string LSP method name. +--- @param params table|nil LSP request params. +--- @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 ~= ms.textDocument_didChange then + changetracking.flush(self) + end + + local client_active = self.rpc.notify(method, params) + + if client_active then + vim.schedule(function() + api.nvim_exec_autocmds('LspNotify', { + modeline = false, + data = { + client_id = self.id, + method = method, + params = params, + }, + }) + end) + end + + return client_active +end + +--- @private +--- Cancels a request with a given request id. +--- +--- @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) + vim.validate({ id = { id, 'n' } }) + local request = self.requests[id] + if request and request.type == 'pending' then + request.type = 'cancel' + api.nvim_exec_autocmds('LspRequest', { + buffer = request.bufnr, + modeline = false, + data = { client_id = self.id, request_id = id, request = request }, + }) + end + return self.rpc.notify(ms.dollar_cancelRequest, { id = id }) +end + +--- @nodoc +--- Stops a client, optionally with force. +--- +--- By default, it will just ask the - server to shutdown without force. If +--- you request to stop a client which has previously been requested to +--- shutdown, it will automatically escalate and force shutdown. +--- +--- @param force boolean|nil +function Client:_stop(force) + local rpc = self.rpc + + if rpc.is_closing() then + return + end + + if force or not self.initialized or self._graceful_shutdown_failed then + rpc.terminate() + return + end + + -- Sending a signal after a process has exited is acceptable. + rpc.request(ms.shutdown, nil, function(err, _) + if err == nil then + rpc.notify(ms.exit) + else + -- If there was an error in the shutdown request, then term to be safe. + rpc.terminate() + self._graceful_shutdown_failed = true + end + end) +end + +--- @private +--- Checks whether a client is stopped. +--- +--- @return boolean # true if client is stopped or in the process of being +--- stopped; false otherwise +function Client:_is_stopped() + return self.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 {}, true) --[[@as lsp.HandlerContext]] + context.bufnr = context.bufnr or api.nvim_get_current_buf() + context.client_id = self.id + local cmdname = command.command + local fn = self.commands[cmdname] or lsp.commands[cmdname] + if fn then + fn(command, context) + return + end + + local command_provider = self.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.', + self.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, + } + self.request(ms.workspace_executeCommand, params, handler, context.bufnr) +end + +--- @package +--- Default handler for the 'textDocument/didOpen' LSP notification. +--- +--- @param bufnr integer Number of the buffer, or 0 for current +function Client:_text_document_did_open_handler(bufnr) + changetracking.init(self, bufnr) + if not vim.tbl_get(self.server_capabilities, 'textDocumentSync', 'openClose') then + return + end + if not api.nvim_buf_is_loaded(bufnr) then + return + end + local filetype = vim.bo[bufnr].filetype + + local params = { + textDocument = { + version = 0, + uri = vim.uri_from_bufnr(bufnr), + languageId = self.config.get_language_id(bufnr, filetype), + text = lsp._buf_get_full_text(bufnr), + }, + } + self.notify(ms.textDocument_didOpen, params) + lsp.util.buf_versions[bufnr] = params.textDocument.version + + -- Next chance we get, we should re-do the diagnostics + vim.schedule(function() + -- Protect against a race where the buffer disappears + -- between `did_open_handler` and the scheduled function firing. + if api.nvim_buf_is_valid(bufnr) then + local namespace = vim.lsp.diagnostic.get_namespace(self.id) + vim.diagnostic.show(namespace, bufnr) + end + end) +end + +--- @private +--- Runs the on_attach function from the client's config if it was defined. +--- @param bufnr integer Buffer number +function Client:_on_attach(bufnr) + self:_text_document_did_open_handler(bufnr) + + lsp._set_defaults(self, bufnr) + + api.nvim_exec_autocmds('LspAttach', { + buffer = bufnr, + modeline = false, + 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 + + -- schedule the initialization of semantic tokens to give the above + -- on_attach and LspAttach callbacks the ability to schedule wrap the + -- opt-out (deleting the semanticTokensProvider from capabilities) + vim.schedule(function() + if vim.tbl_get(self.server_capabilities, 'semanticTokensProvider', 'full') then + lsp.semantic_tokens.start(bufnr, self.id) + end + end) + + self.attached_buffers[bufnr] = true +end + +--- @private +--- Logs the given error to the LSP log and to the error buffer. +--- @param code integer Error code +--- @param err any Error arguments +function Client:write_error(code, err) + if log.error() then + log.error(self._log_prefix, 'on_error', { code = lsp.client_errors[code], err = err }) + end + err_message(self._log_prefix, ': Error ', lsp.client_errors[code], ': ', vim.inspect(err)) +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 + return true + 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 +end + +return Client -- cgit From 5785c32f1115833cf69ec9062c5bc8907360aab0 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Thu, 8 Feb 2024 07:34:38 +0800 Subject: fix(lsp): set fallback client name properly --- runtime/lua/vim/lsp/client.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim/lsp/client.lua') diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua index 2daf564f63..36c3a4225e 100644 --- a/runtime/lua/vim/lsp/client.lua +++ b/runtime/lua/vim/lsp/client.lua @@ -127,7 +127,7 @@ end --- @param config lsp.ClientConfig --- @return lsp.Client function Client.new(id, rpc, handlers, offset_encoding, config) - local name = config.name + local name = config.name or tostring(id) --- @class lsp.Client local self = { -- cgit From 1f9da3d0835af2cfe937de250c2cde3a59e1677e Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 8 Feb 2024 09:24:47 +0000 Subject: refactor(lsp): tidy up logging --- runtime/lua/vim/lsp/client.lua | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) (limited to 'runtime/lua/vim/lsp/client.lua') diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua index 36c3a4225e..0bcbb35be6 100644 --- a/runtime/lua/vim/lsp/client.lua +++ b/runtime/lua/vim/lsp/client.lua @@ -244,9 +244,7 @@ function Client:initialize(cb) end end - if log.trace() then - log.trace(self._log_prefix, 'initialize_params', initialize_params) - end + log.trace(self._log_prefix, 'initialize_params', initialize_params) local rpc = self.rpc @@ -278,13 +276,12 @@ function Client:initialize(cb) self:write_error(lsp.client_errors.ON_INIT_CALLBACK_ERROR, err) end end - if log.info() then - log.info( - self._log_prefix, - 'server_capabilities', - { server_capabilities = self.server_capabilities } - ) - end + + log.info( + self._log_prefix, + 'server_capabilities', + { server_capabilities = self.server_capabilities } + ) cb() end) @@ -340,9 +337,7 @@ function Client:_request(method, params, handler, bufnr) changetracking.flush(self, bufnr) local version = lsp.util.buf_versions[bufnr] bufnr = resolve_bufnr(bufnr) - if log.debug() then - log.debug(self._log_prefix, 'client.request', self.id, method, params, handler, bufnr) - end + log.debug(self._log_prefix, 'client.request', self.id, method, params, handler, bufnr) local success, request_id = self.rpc.request(method, params, function(err, result) local context = { method = method, @@ -635,9 +630,7 @@ end --- @param code integer Error code --- @param err any Error arguments function Client:write_error(code, err) - if log.error() then - log.error(self._log_prefix, 'on_error', { code = lsp.client_errors[code], err = err }) - end + log.error(self._log_prefix, 'on_error', { code = lsp.client_errors[code], err = err }) err_message(self._log_prefix, ': Error ', lsp.client_errors[code], ': ', vim.inspect(err)) end -- cgit From c73d67d283c296bdb7a44a0283346e7b61d837f0 Mon Sep 17 00:00:00 2001 From: Maria José Solano Date: Sat, 10 Feb 2024 14:03:44 -0800 Subject: refactor(lsp): add type annotations --- runtime/lua/vim/lsp/client.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim/lsp/client.lua') diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua index 0bcbb35be6..7bf83f4d2c 100644 --- a/runtime/lua/vim/lsp/client.lua +++ b/runtime/lua/vim/lsp/client.lua @@ -630,8 +630,9 @@ end --- @param code integer Error code --- @param err any Error arguments function Client:write_error(code, err) - log.error(self._log_prefix, 'on_error', { code = lsp.client_errors[code], err = err }) - err_message(self._log_prefix, ': Error ', lsp.client_errors[code], ': ', vim.inspect(err)) + local client_error = lsp.client_errors[code] --- @type string|integer + log.error(self._log_prefix, 'on_error', { code = client_error, err = err }) + err_message(self._log_prefix, ': Error ', client_error, ': ', vim.inspect(err)) end --- @param method string -- cgit From ed1b66bd998b98ee8cf76b5a23c323352588dd56 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Sun, 11 Feb 2024 12:37:20 +0000 Subject: refactor(lsp): move more code to client.lua The dispatchers used by the RPC client should be defined in the client, so they have been moved there. Due to this, it also made sense to move all code related to client configuration and the creation of the RPC client there too. Now vim.lsp.start_client is significantly simplified and now mostly contains logic for tracking open clients. - Renamed client.new -> client.start --- runtime/lua/vim/lsp/client.lua | 262 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 240 insertions(+), 22 deletions(-) (limited to 'runtime/lua/vim/lsp/client.lua') diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua index 7bf83f4d2c..58db4387b6 100644 --- a/runtime/lua/vim/lsp/client.lua +++ b/runtime/lua/vim/lsp/client.lua @@ -4,6 +4,30 @@ local lsp = vim.lsp local log = lsp.log local ms = lsp.protocol.Methods local changetracking = lsp._changetracking +local validate = vim.validate + +--- @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 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 --- @class lsp.Client.Progress: vim.Ringbuf<{token: integer|string, value: any}> --- @field pending table @@ -51,7 +75,6 @@ local changetracking = lsp._changetracking --- @field initialized true? --- @field workspace_folders lsp.WorkspaceFolder[]? --- @field attached_buffers table ---- @field commands table --- @field private _log_prefix string --- Track this so that we can escalate automatically if we've already tried a --- graceful shutdown @@ -119,27 +142,131 @@ local function method_wrapper(cls, meth) end end +local client_index = 0 + +--- Checks whether a given path is a directory. +--- @param filename (string) path to check +--- @return boolean # true if {filename} exists and is a directory, false otherwise +local function is_dir(filename) + validate({ filename = { filename, 's' } }) + local stat = uv.fs_stat(filename) + return stat and stat.type == 'directory' or false +end + +local valid_encodings = { + ['utf-8'] = 'utf-8', + ['utf-16'] = 'utf-16', + ['utf-32'] = 'utf-32', + ['utf8'] = 'utf-8', + ['utf16'] = 'utf-16', + ['utf32'] = 'utf-32', + UTF8 = 'utf-8', + UTF16 = 'utf-16', + UTF32 = 'utf-32', +} + +--- Normalizes {encoding} to valid LSP encoding names. +--- @param encoding string? Encoding to normalize +--- @return string # normalized encoding name +local function validate_encoding(encoding) + validate({ + encoding = { encoding, 's', true }, + }) + if not encoding then + return valid_encodings.UTF16 + end + return valid_encodings[encoding:lower()] + or error( + string.format( + "Invalid offset encoding %q. Must be one of: 'utf-8', 'utf-16', 'utf-32'", + encoding + ) + ) +end + +--- Augments a validator function with support for optional (nil) values. +--- @param fn (fun(v): boolean) The original validator function; should return a +--- bool. +--- @return fun(v): boolean # The augmented function. Also returns true if {v} is +--- `nil`. +local function optional_validator(fn) + return function(v) + return v == nil or fn(v) + end +end + +--- Validates a client configuration as given to |vim.lsp.start_client()|. +--- @param config lsp.ClientConfig +local function process_client_config(config) + validate({ + config = { config, 't' }, + }) + validate({ + handlers = { config.handlers, 't', true }, + capabilities = { config.capabilities, 't', true }, + cmd_cwd = { config.cmd_cwd, optional_validator(is_dir), 'directory' }, + cmd_env = { config.cmd_env, 't', true }, + detached = { config.detached, 'b', true }, + name = { config.name, 's', true }, + on_error = { config.on_error, 'f', true }, + on_exit = { config.on_exit, 'f', true }, + on_init = { config.on_init, 'f', true }, + settings = { config.settings, 't', true }, + commands = { config.commands, 't', true }, + before_init = { config.before_init, 'f', true }, + offset_encoding = { config.offset_encoding, 's', true }, + flags = { config.flags, 't', true }, + get_language_id = { config.get_language_id, 'f', true }, + }) + assert( + ( + not config.flags + or not config.flags.debounce_text_changes + or type(config.flags.debounce_text_changes) == 'number' + ), + 'flags.debounce_text_changes must be a number with the debounce time in milliseconds' + ) + + 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 + + config.offset_encoding = validate_encoding(config.offset_encoding) + config.flags = config.flags or {} + config.settings = config.settings or {} + config.handlers = config.handlers or {} + + -- 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 + end + + config.capabilities = config.capabilities or lsp.protocol.make_client_capabilities() + config.commands = config.commands or {} +end + --- @package ---- @param id integer ---- @param rpc vim.lsp.rpc.PublicClient ---- @param handlers table ---- @param offset_encoding string --- @param config lsp.ClientConfig ---- @return lsp.Client -function Client.new(id, rpc, handlers, offset_encoding, config) +--- @return lsp.Client? +function Client.start(config) + process_client_config(config) + + client_index = client_index + 1 + local id = client_index + local name = config.name or tostring(id) --- @class lsp.Client local self = { id = id, config = config, - handlers = handlers, - rpc = rpc, - offset_encoding = offset_encoding, + handlers = config.handlers, + offset_encoding = config.offset_encoding, name = name, _log_prefix = string.format('LSP[%s]', name), requests = {}, - commands = config.commands or {}, attached_buffers = {}, server_capabilities = {}, dynamic_capabilities = vim.lsp._dynamic.new(id), @@ -165,15 +292,46 @@ function Client.new(id, rpc, handlers, offset_encoding, config) self.on_attach = method_wrapper(self, Client._on_attach) self.supports_method = method_wrapper(self, Client._supports_method) - ---@type table title of unfinished progress sequences by token + --- @type table title of unfinished progress sequences by token self.progress.pending = {} - return setmetatable(self, Client) + --- @type vim.lsp.rpc.Dispatchers + local dispatchers = { + notification = method_wrapper(self, Client._notification), + server_request = method_wrapper(self, Client._server_request), + on_error = method_wrapper(self, Client._on_error), + on_exit = method_wrapper(self, Client._on_exit), + } + + -- Start the RPC client. + local rpc --- @type vim.lsp.rpc.PublicClient? + local config_cmd = config.cmd + if type(config_cmd) == 'function' then + rpc = config_cmd(dispatchers) + else + rpc = lsp.rpc.start(config_cmd, dispatchers, { + cwd = config.cmd_cwd, + env = config.cmd_env, + detached = config.detached, + }) + end + + -- Return nil if the rpc client fails to start + if not rpc then + return + end + + self.rpc = rpc + + setmetatable(self, Client) + + self:initialize() + + return self end --- @private ---- @param cb fun() -function Client:initialize(cb) +function Client:initialize() local valid_traces = { off = 'off', messages = 'messages', @@ -282,8 +440,6 @@ function Client:initialize(cb) 'server_capabilities', { server_capabilities = self.server_capabilities } ) - - cb() end) end @@ -302,7 +458,7 @@ end --- @param bufnr (integer|nil) Buffer number to resolve. Defaults to current buffer --- @return integer bufnr local function resolve_bufnr(bufnr) - vim.validate({ bufnr = { bufnr, 'n', true } }) + validate({ bufnr = { bufnr, 'n', true } }) if bufnr == nil or bufnr == 0 then return api.nvim_get_current_buf() end @@ -374,10 +530,9 @@ end -- TODO(lewis6991): duplicated from lsp.lua local wait_result_reason = { [-1] = 'timeout', [-2] = 'interrupted', [-3] = 'error' } --- TODO(lewis6991): duplicated from lsp.lua --- Concatenates and writes a list of strings to the Vim error buffer. --- ----@param ... string List to write to the buffer +--- @param ... string List to write to the buffer local function err_message(...) api.nvim_err_writeln(table.concat(vim.tbl_flatten({ ... }))) api.nvim_command('redraw') @@ -461,7 +616,7 @@ end --- @return boolean status true if notification was successful. false otherwise --- @see |vim.lsp.client.notify()| function Client:_cancel_request(id) - vim.validate({ id = { id, 'n' } }) + validate({ id = { id, 'n' } }) local request = self.requests[id] if request and request.type == 'pending' then request.type = 'cancel' @@ -527,7 +682,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.commands[cmdname] or lsp.commands[cmdname] + local fn = self.config.commands[cmdname] or lsp.commands[cmdname] if fn then fn(command, context) return @@ -654,4 +809,67 @@ function Client:_supports_method(method, opts) end end +--- @private +--- Handles a notification sent by an LSP server by invoking the +--- corresponding handler. +--- +--- @param method string LSP method name +--- @param params table The parameters for that method. +function Client:_notification(method, params) + log.trace('notification', method, params) + local handler = self:_resolve_handler(method) + if handler then + -- Method name is provided here for convenience. + handler(nil, params, { method = method, client_id = self.id }) + end +end + +--- @private +--- Handles a request from an LSP server by invoking the corresponding handler. +--- +--- @param method (string) LSP method name +--- @param params (table) The parameters for that method +--- @return any result +--- @return lsp.ResponseError error code and message set in case an exception happens during the request. +function Client:_server_request(method, params) + log.trace('server_request', method, params) + local handler = self:_resolve_handler(method) + if handler then + log.trace('server_request: found handler for', method) + return handler(nil, params, { method = method, client_id = self.id }) + end + log.warn('server_request: no handler found for', method) + return nil, lsp.rpc_response_error(lsp.protocol.ErrorCodes.MethodNotFound) +end + +--- @private +--- Invoked when the client operation throws an error. +--- +--- @param code integer Error code +--- @param err any Other arguments may be passed depending on the error kind +--- @see vim.lsp.rpc.client_errors for possible errors. Use +--- `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 + --- @type boolean, string + local status, usererr = pcall(self.config.on_error, 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)) + end + end +end + +--- @private +--- Invoked on client exit. +--- +--- @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 +end + return Client -- cgit From 597ecf751603cde2d5f58021d503ae20a12eb2f2 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Mon, 12 Feb 2024 10:18:42 +0000 Subject: fix(lsp): re-add client.commands and mark private --- runtime/lua/vim/lsp/client.lua | 2 ++ 1 file changed, 2 insertions(+) (limited to 'runtime/lua/vim/lsp/client.lua') diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua index 58db4387b6..a279be55e9 100644 --- a/runtime/lua/vim/lsp/client.lua +++ b/runtime/lua/vim/lsp/client.lua @@ -79,6 +79,7 @@ local validate = vim.validate --- 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 --- --- @field dynamic_capabilities lsp.DynamicCapabilities --- @@ -270,6 +271,7 @@ function Client.start(config) attached_buffers = {}, server_capabilities = {}, dynamic_capabilities = vim.lsp._dynamic.new(id), + commands = config.commands, -- Remove in Nvim 0.11 --- Contains $/progress report messages. --- They have the format {token: integer|string, value: any} -- cgit From 9f8c96240dc0318bd92a646966917e8fe0641144 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Mon, 12 Feb 2024 13:46:32 +0000 Subject: refactor(lsp): resolve the config-client entanglement Previously the LSP-Client object contained some fields that are also in the client config, but for a lot of other fields, the config was used directly making the two objects vaguely entangled with either not having a clear role. Now the config object is treated purely as config (read-only) from the client, and any fields the client needs from the config are now copied in as additional fields. This means: - the config object is no longet normalised and is left as the user provided it. - the client only reads the config on creation of the client and all other implementations now read the clients version of the fields. In addition, internal support for multiple callbacks has been added to the client so the client tracking logic (done in lua.lsp) can be done more robustly instead of wrapping the user callbacks which may error. --- runtime/lua/vim/lsp/client.lua | 307 +++++++++++++++++++++++++---------------- 1 file changed, 185 insertions(+), 122 deletions(-) (limited to 'runtime/lua/vim/lsp/client.lua') 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 ---- @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 +--- @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 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 +--- @field on_exit? elem_or_list +--- @field on_attach? elem_or_list +--- @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 @@ -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 --- @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 +--- +--- @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? +--- @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 -- cgit From a0394b648c2486ce7085d3e6e7541024e1a3fe9a Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Sat, 24 Feb 2024 14:49:36 +0100 Subject: docs(lsp): mark `ClientConfig.init_options` as optional Followup to neovim/neovim#27443 --- runtime/lua/vim/lsp/client.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim/lsp/client.lua') diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua index a460d95cc6..c1b41a7183 100644 --- a/runtime/lua/vim/lsp/client.lua +++ b/runtime/lua/vim/lsp/client.lua @@ -21,7 +21,7 @@ local validate = vim.validate --- @field handlers? table --- @field settings? table --- @field commands? table ---- @field init_options table +--- @field init_options? table --- @field name? string --- @field get_language_id? fun(bufnr: integer, filetype: string): string --- @field offset_encoding? string -- cgit From 816b56f878f0291c00a9018d5057b7b2b00f1891 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Wed, 7 Feb 2024 13:05:33 +0000 Subject: fix(lsp): cancel watchers when closing a client --- runtime/lua/vim/lsp/client.lua | 1 + 1 file changed, 1 insertion(+) (limited to 'runtime/lua/vim/lsp/client.lua') diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua index c1b41a7183..1e709a1992 100644 --- a/runtime/lua/vim/lsp/client.lua +++ b/runtime/lua/vim/lsp/client.lua @@ -727,6 +727,7 @@ function Client:_stop(force) rpc.terminate() self._graceful_shutdown_failed = true end + vim.lsp._watchfiles.cancel(self.id) end) end -- cgit From a5fe8f59d98398d04bed8586cee73864bbcdde92 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Tue, 27 Feb 2024 15:20:32 +0000 Subject: docs: improve/add documentation of Lua types - Added `@inlinedoc` so single use Lua types can be inlined into the functions docs. E.g. ```lua --- @class myopts --- @inlinedoc --- --- Documentation for some field --- @field somefield integer --- @param opts myOpts function foo(opts) end ``` Will be rendered as ``` foo(opts) Parameters: - {opts} (table) Object with the fields: - somefield (integer) Documentation for some field ``` - Marked many classes with with `@nodoc` or `(private)`. We can eventually introduce these when we want to. --- runtime/lua/vim/lsp/client.lua | 123 +++++++++++++++++++++++++++++++++++------ 1 file changed, 106 insertions(+), 17 deletions(-) (limited to 'runtime/lua/vim/lsp/client.lua') diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua index c1b41a7183..fcbaadbc57 100644 --- a/runtime/lua/vim/lsp/client.lua +++ b/runtime/lua/vim/lsp/client.lua @@ -6,38 +6,125 @@ 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_init_cb fun(client: vim.lsp.Client, initialize_result: lsp.InitializeResult) +--- @alias vim.lsp.client.on_attach_cb fun(client: vim.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 +--- @alias vim.lsp.client.before_init_cb fun(params: lsp.InitializeParams, config: vim.lsp.ClientConfig) + +--- @class vim.lsp.ClientConfig +--- command string[] that launches the language +--- server (treated as in |jobstart()|, must be absolute or on `$PATH`, shell constructs like +--- "~" are not expanded), or function that creates an RPC client. Function receives +--- a `dispatchers` table and returns a table with member functions `request`, `notify`, +--- `is_closing` and `terminate`. +--- See |vim.lsp.rpc.request()|, |vim.lsp.rpc.notify()|. +--- For TCP there is a builtin RPC client factory: |vim.lsp.rpc.connect()| --- @field cmd string[]|fun(dispatchers: vim.lsp.rpc.Dispatchers): vim.lsp.rpc.PublicClient? +--- +--- Directory to launch the `cmd` process. Not related to `root_dir`. +--- (default: cwd) --- @field cmd_cwd? string +--- +--- Environment flags to pass to the LSP on spawn. +--- Must be specified using a table. +--- Non-string values are coerced to string. +--- Example: +--- ```lua +--- { PORT = 8080; HOST = "0.0.0.0"; } +--- ``` --- @field cmd_env? table +--- +--- Daemonize the server process so that it runs in a separate process group from Nvim. +--- Nvim will shutdown the process on exit, but if Nvim fails to exit cleanly this could leave +--- behind orphaned server processes. +--- (default: true) --- @field detached? boolean +--- +--- List of workspace folders passed to the language server. +--- For backwards compatibility rootUri and rootPath will be derived from the first workspace +--- folder in this list. See `workspaceFolders` in the LSP spec. --- @field workspace_folders? lsp.WorkspaceFolder[] +--- +--- Map overriding the default capabilities defined by |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.empty_dict()|, else it will be encoded as an +--- array. --- @field capabilities? lsp.ClientCapabilities +--- +--- Map of language server method names to |lsp-handler| --- @field handlers? table +--- +--- Map with language server specific settings. These are returned to the language server if +--- requested via `workspace/configuration`. Keys are case-sensitive. --- @field settings? table +--- +--- Table that maps string of clientside commands to user-defined functions. +--- Commands passed to start_client take precedence over the global command registry. Each key +--- must be a unique command name, and the value is a function which is called if any LSP action +--- (code action, code lenses, ...) triggers the command. --- @field commands? table +--- +--- Values to pass in the initialization request as `initializationOptions`. See `initialize` in +--- the LSP spec. --- @field init_options? table +--- +--- Name in log messages. +--- (default: client-id) --- @field name? string +--- +--- Language ID as string. Defaults to the filetype. --- @field get_language_id? fun(bufnr: integer, filetype: string): string ---- @field offset_encoding? string +--- +--- The encoding that the LSP server expects. Client does not verify this is correct. +--- @field offset_encoding? 'utf-8'|'utf-16'|'utf-32' +--- +--- Callback invoked when the client operation throws an error. `code` is a number describing the error. +--- Other arguments may be passed depending on the error kind. See `vim.lsp.rpc.client_errors` +--- for possible errors. Use `vim.lsp.rpc.client_errors[code]` to get human-friendly name. --- @field on_error? fun(code: integer, err: string) +--- +--- Callback invoked before the LSP "initialize" phase, where `params` contains the parameters +--- being sent to the server and `config` is the config that was passed to |vim.lsp.start_client()|. +--- You can use this to modify parameters before they are sent. --- @field before_init? vim.lsp.client.before_init_cb +--- +--- Callback invoked after LSP "initialize", where `result` is a table of `capabilities` +--- and anything else the server may send. For example, clangd sends +--- `initialize_result.offsetEncoding` if `capabilities.offsetEncoding` was sent to it. +--- You can only modify the `client.offset_encoding` here before any notifications are sent. --- @field on_init? elem_or_list +--- +--- Callback invoked on client exit. +--- - code: exit code of the process +--- - signal: number describing the signal used to terminate (if any) +--- - client_id: client handle --- @field on_exit? elem_or_list +--- +--- Callback invoked when client attaches to a buffer. --- @field on_attach? elem_or_list +--- +--- Passed directly to the language server in the initialize request. Invalid/empty values will +--- (default: "off") --- @field trace? 'off'|'messages'|'verbose' +--- +--- A table with flags for the client. The current (experimental) flags are: +--- - allow_incremental_sync (bool, default true): Allow using incremental sync for buffer edits +--- - debounce_text_changes (number, default 150): Debounce didChange +--- notifications to the server by the given number in milliseconds. No debounce +--- occurs if nil +--- - exit_timeout (number|boolean, default false): Milliseconds to wait for server to +--- exit cleanly after sending the "shutdown" request before sending kill -15. +--- If set to false, nvim exits immediately after sending the "shutdown" request to the server. --- @field flags? table +--- +--- Directory where the LSP server will base its workspaceFolders, rootUri, and rootPath on initialization. --- @field root_dir? string ---- @class lsp.Client.Progress: vim.Ringbuf<{token: integer|string, value: any}> +--- @class vim.lsp.Client.Progress: vim.Ringbuf<{token: integer|string, value: any}> --- @field pending table ---- @class lsp.Client +--- @class vim.lsp.Client --- --- The id allocated to the client. --- @field id integer @@ -67,7 +154,7 @@ local validate = vim.validate --- --- copy of the table that was passed by the user --- to |vim.lsp.start_client()|. ---- @field config lsp.ClientConfig +--- @field config vim.lsp.ClientConfig --- --- Response from the server sent on --- initialize` describing the server's capabilities. @@ -75,7 +162,7 @@ local validate = vim.validate --- --- A ring buffer (|vim.ringbuf()|) containing progress messages --- sent by the server. ---- @field progress lsp.Client.Progress +--- @field progress vim.lsp.Client.Progress --- --- @field initialized true? --- @@ -239,7 +326,7 @@ local function default_get_language_id(_bufnr, filetype) end --- Validates a client configuration as given to |vim.lsp.start_client()|. ---- @param config lsp.ClientConfig +--- @param config vim.lsp.ClientConfig local function validate_config(config) validate({ config = { config, 't' }, @@ -285,7 +372,7 @@ local function get_trace(trace) end --- @param id integer ---- @param config lsp.ClientConfig +--- @param config vim.lsp.ClientConfig --- @return string local function get_name(id, config) local name = config.name @@ -328,8 +415,8 @@ local function ensure_list(x) end --- @package ---- @param config lsp.ClientConfig ---- @return lsp.Client? +--- @param config vim.lsp.ClientConfig +--- @return vim.lsp.Client? function Client.create(config) validate_config(config) @@ -337,7 +424,7 @@ function Client.create(config) local id = client_index local name = get_name(id, config) - --- @class lsp.Client + --- @class vim.lsp.Client local self = { id = id, config = config, @@ -370,7 +457,7 @@ function Client.create(config) --- - lsp.WorkDoneProgressBegin, --- - lsp.WorkDoneProgressReport (extended with title from Begin) --- - lsp.WorkDoneProgressEnd (extended with title from Begin) - progress = vim.ringbuf(50) --[[@as lsp.Client.Progress]], + progress = vim.ringbuf(50) --[[@as vim.lsp.Client.Progress]], --- @deprecated use client.progress instead messages = { name = name, messages = {}, progress = {}, status = {} }, @@ -421,6 +508,7 @@ function Client.create(config) return self end +--- @private --- @param cbs function[] --- @param error_id integer --- @param ... any @@ -698,7 +786,7 @@ function Client:_cancel_request(id) return self.rpc.notify(ms.dollar_cancelRequest, { id = id }) end ---- @nodoc +--- @private --- Stops a client, optionally with force. --- --- By default, it will just ask the - server to shutdown without force. If @@ -853,6 +941,7 @@ function Client:write_error(code, err) err_message(self._log_prefix, ': Error ', client_error, ': ', vim.inspect(err)) end +--- @private --- @param method string --- @param opts? {bufnr: integer?} function Client:_supports_method(method, opts) -- cgit From 39cc38a87b29d61e7e5342bad2e5156446eb8649 Mon Sep 17 00:00:00 2001 From: Jaehwang Jung Date: Sat, 2 Mar 2024 18:51:10 +0900 Subject: fix(lsp): defer writing error msgs (#27688) Context: Nvim catches errors from the user's `on_exit` and rpc handler callbacks and prints the error message. Problem: Printing the error message uses Nvim api functions. But callbacks mentioned above run in `:h lua-loop-callbacks` where most of `vim.api` is not allowed, so Nvim itself raises error. Solution: `vim.schedule()` the error reporting when necessary. --- runtime/lua/vim/lsp/client.lua | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim/lsp/client.lua') diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua index dfb5137e2a..0f47c57cd5 100644 --- a/runtime/lua/vim/lsp/client.lua +++ b/runtime/lua/vim/lsp/client.lua @@ -691,8 +691,16 @@ local wait_result_reason = { [-1] = 'timeout', [-2] = 'interrupted', [-3] = 'err --- --- @param ... string List to write to the buffer local function err_message(...) - api.nvim_err_writeln(table.concat(vim.tbl_flatten({ ... }))) - api.nvim_command('redraw') + local message = table.concat(vim.tbl_flatten({ ... })) + if vim.in_fast_event() then + vim.schedule(function() + api.nvim_err_writeln(message) + api.nvim_command('redraw') + end) + else + api.nvim_err_writeln(message) + api.nvim_command('redraw') + end end --- @private -- cgit From a4290f462ed7dc81e17b09bd27877b106b24b6bd Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Tue, 5 Mar 2024 12:06:15 +0000 Subject: docs(lua): improvements for LSP and Diagnostic --- runtime/lua/vim/lsp/client.lua | 47 ++++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 15 deletions(-) (limited to 'runtime/lua/vim/lsp/client.lua') diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua index 0f47c57cd5..6dd898bea7 100644 --- a/runtime/lua/vim/lsp/client.lua +++ b/runtime/lua/vim/lsp/client.lua @@ -11,6 +11,24 @@ local validate = vim.validate --- @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: vim.lsp.ClientConfig) +--- @class vim.lsp.Client.Flags +--- @inlinedoc +--- +--- Allow using incremental sync for buffer edits +--- (defailt: `true`) +--- @field allow_incremental_sync? boolean +--- +--- Debounce `didChange` notifications to the server by the given number in milliseconds. +--- No debounce occurs if `nil`. +--- (default: `150`) +--- @field debounce_text_changes integer +--- +--- Milliseconds to wait for server to exit cleanly after sending the +--- "shutdown" request before sending kill -15. If set to false, nvim exits +--- immediately after sending the "shutdown" request to the server. +--- (default: `false`) +--- @field exit_timeout integer|false + --- @class vim.lsp.ClientConfig --- command string[] that launches the language --- server (treated as in |jobstart()|, must be absolute or on `$PATH`, shell constructs like @@ -55,8 +73,8 @@ local validate = vim.validate --- Map of language server method names to |lsp-handler| --- @field handlers? table --- ---- Map with language server specific settings. These are returned to the language server if ---- requested via `workspace/configuration`. Keys are case-sensitive. +--- Map with language server specific settings. +--- See the {settings} in |vim.lsp.Client|. --- @field settings? table --- --- Table that maps string of clientside commands to user-defined functions. @@ -87,36 +105,29 @@ local validate = vim.validate --- Callback invoked before the LSP "initialize" phase, where `params` contains the parameters --- being sent to the server and `config` is the config that was passed to |vim.lsp.start_client()|. --- You can use this to modify parameters before they are sent. ---- @field before_init? vim.lsp.client.before_init_cb +--- @field before_init? fun(params: lsp.InitializeParams, config: vim.lsp.ClientConfig) --- --- Callback invoked after LSP "initialize", where `result` is a table of `capabilities` --- and anything else the server may send. For example, clangd sends --- `initialize_result.offsetEncoding` if `capabilities.offsetEncoding` was sent to it. --- You can only modify the `client.offset_encoding` here before any notifications are sent. ---- @field on_init? elem_or_list +--- @field on_init? elem_or_list --- --- Callback invoked on client exit. --- - code: exit code of the process --- - signal: number describing the signal used to terminate (if any) --- - client_id: client handle ---- @field on_exit? elem_or_list +--- @field on_exit? elem_or_list --- --- Callback invoked when client attaches to a buffer. ---- @field on_attach? elem_or_list +--- @field on_attach? elem_or_list --- --- Passed directly to the language server in the initialize request. Invalid/empty values will --- (default: "off") --- @field trace? 'off'|'messages'|'verbose' --- --- A table with flags for the client. The current (experimental) flags are: ---- - allow_incremental_sync (bool, default true): Allow using incremental sync for buffer edits ---- - debounce_text_changes (number, default 150): Debounce didChange ---- notifications to the server by the given number in milliseconds. No debounce ---- occurs if nil ---- - exit_timeout (number|boolean, default false): Milliseconds to wait for server to ---- exit cleanly after sending the "shutdown" request before sending kill -15. ---- If set to false, nvim exits immediately after sending the "shutdown" request to the server. ---- @field flags? table +--- @field flags? vim.lsp.Client.Flags --- --- Directory where the LSP server will base its workspaceFolders, rootUri, and rootPath on initialization. --- @field root_dir? string @@ -189,8 +200,14 @@ local validate = vim.validate --- Client commands take precedence over the global command registry. --- @field commands table --- +--- Map with language server specific settings. These are returned to the +--- language server if requested via `workspace/configuration`. Keys are +--- case-sensitive. --- @field settings table ---- @field flags table +--- +--- A table with flags for the client. The current (experimental) flags are: +--- @field flags vim.lsp.Client.Flags +--- --- @field get_language_id fun(bufnr: integer, filetype: string): string --- --- The capabilities provided by the client (editor or tool) -- cgit From 0101bdaa1ad8b09ebd5ef5551faf077f39be238c Mon Sep 17 00:00:00 2001 From: Maria José Solano Date: Mon, 19 Feb 2024 18:28:07 -0800 Subject: docs(lsp): nits and typos in client.lua --- runtime/lua/vim/lsp/client.lua | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) (limited to 'runtime/lua/vim/lsp/client.lua') diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua index 6dd898bea7..d0054e073c 100644 --- a/runtime/lua/vim/lsp/client.lua +++ b/runtime/lua/vim/lsp/client.lua @@ -156,7 +156,7 @@ local validate = vim.validate --- @field handlers table --- --- The current pending requests in flight to the server. Entries are key-value ---- pairs with the key being the request ID while the value is a table with +--- pairs with the key being the request id while the value is a table with --- `type`, `bufnr`, and `method` key-value pairs. `type` is either "pending" --- for an active request, or "cancel" for a cancel request. It will be --- "complete" ephemerally while executing |LspRequest| autocmds when replies @@ -167,8 +167,8 @@ local validate = vim.validate --- to |vim.lsp.start_client()|. --- @field config vim.lsp.ClientConfig --- ---- Response from the server sent on ---- initialize` describing the server's capabilities. +--- Response from the server sent on `initialize` describing the server's +--- capabilities. --- @field server_capabilities lsp.ServerCapabilities? --- --- A ring buffer (|vim.ringbuf()|) containing progress messages @@ -217,10 +217,11 @@ local validate = vim.validate --- Sends a request to the server. --- This is a thin wrapper around {client.rpc.request} with some additional --- checking. ---- If {handler} is not specified, If one is not found there, then an error ---- will occur. Returns: {status}, {[client_id]}. {status} is a boolean ---- indicating if the notification was successful. If it is `false`, then it ---- will always be `false` (the client has shutdown). +--- If {handler} is not specified and if there's no respective global +--- handler, then an error will occur. +--- Returns: {status}, {[client_id]}. {status} is a boolean indicating if +--- the notification was successful. If it is `false`, then it will always +--- be `false` (the client has shutdown). --- If {status} is `true`, the function returns {request_id} as the second --- result. You can use this with `client.cancel_request(request_id)` to cancel --- the request. -- cgit From ade1b12f49c3b3914c74847d791eb90ea90b56b7 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Fri, 8 Mar 2024 12:25:18 +0000 Subject: docs: support inline markdown - Tags are now created with `[tag]()` - References are now created with `[tag]` - Code spans are no longer wrapped --- runtime/lua/vim/lsp/client.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim/lsp/client.lua') diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua index d0054e073c..ff0db166d5 100644 --- a/runtime/lua/vim/lsp/client.lua +++ b/runtime/lua/vim/lsp/client.lua @@ -219,7 +219,7 @@ local validate = vim.validate --- checking. --- If {handler} is not specified and if there's no respective global --- handler, then an error will occur. ---- Returns: {status}, {[client_id]}. {status} is a boolean indicating if +--- Returns: {status}, {client_id}?. {status} is a boolean indicating if --- the notification was successful. If it is `false`, then it will always --- be `false` (the client has shutdown). --- If {status} is `true`, the function returns {request_id} as the second @@ -262,7 +262,7 @@ local validate = vim.validate --- --- Checks if a client supports a given method. --- Always returns true for unknown off-spec methods. ---- [opts] is a optional `{bufnr?: integer}` table. +--- {opts} is a optional `{bufnr?: integer}` table. --- Some language server capabilities can be file specific. --- @field supports_method fun(method: string, opts?: {bufnr: integer?}): boolean --- -- cgit