diff options
Diffstat (limited to 'runtime/lua/vim/lsp.lua')
-rw-r--r-- | runtime/lua/vim/lsp.lua | 515 |
1 files changed, 383 insertions, 132 deletions
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 6fe1d15b7e..dacdbcfa17 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -1,4 +1,4 @@ -local default_callbacks = require 'vim.lsp.callbacks' +local default_handlers = require 'vim.lsp.handlers' local log = require 'vim.lsp.log' local lsp_rpc = require 'vim.lsp.rpc' local protocol = require 'vim.lsp.protocol' @@ -13,25 +13,61 @@ local validate = vim.validate local lsp = { protocol = protocol; - callbacks = default_callbacks; + + -- TODO(tjdevries): Add in the warning that `callbacks` is no longer supported. + -- util.warn_once("vim.lsp.callbacks is deprecated. Use vim.lsp.handlers instead.") + handlers = default_handlers; + callbacks = default_handlers; + buf = require'vim.lsp.buf'; + diagnostic = require'vim.lsp.diagnostic'; util = util; + -- Allow raw RPC access. rpc = lsp_rpc; + -- Export these directly from rpc. rpc_response_error = lsp_rpc.rpc_response_error; - -- You probably won't need this directly, since __tostring is set for errors - -- by the RPC. - -- format_rpc_error = lsp_rpc.format_rpc_error; +} + +-- maps request name to the required resolved_capability in the client. +lsp._request_name_to_capability = { + ['textDocument/hover'] = 'hover'; + ['textDocument/signatureHelp'] = 'signature_help'; + ['textDocument/definition'] = 'goto_definition'; + ['textDocument/implementation'] = 'implementation'; + ['textDocument/declaration'] = 'declaration'; + ['textDocument/typeDefinition'] = 'type_definition'; + ['textDocument/documentSymbol'] = 'document_symbol'; + ['textDocument/workspaceSymbol'] = 'workspace_symbol'; + ['textDocument/prepareCallHierarchy'] = 'call_hierarchy'; + ['textDocument/rename'] = 'rename'; + ['textDocument/codeAction'] = 'code_action'; + ['workspace/executeCommand'] = 'execute_command'; + ['textDocument/references'] = 'find_references'; + ['textDocument/rangeFormatting'] = 'document_range_formatting'; + ['textDocument/formatting'] = 'document_formatting'; + ['textDocument/completion'] = 'completion'; + ['textDocument/documentHighlight'] = 'document_highlight'; } -- TODO improve handling of scratch buffers with LSP attached. +--@private +--- Concatenates and writes a list of strings to the Vim error buffer. +--- +--@param {...} (List of strings) List to write to the buffer local function err_message(...) nvim_err_writeln(table.concat(vim.tbl_flatten{...})) nvim_command("redraw") end +--@private +--- Returns the buffer number for the given {bufnr}. +--- +--@param bufnr (number) Buffer number to resolve. Defaults to the current +---buffer if not given. +--@returns bufnr (number) Number of requested buffer local function resolve_bufnr(bufnr) validate { bufnr = { bufnr, 'n', true } } if bufnr == nil or bufnr == 0 then @@ -40,6 +76,21 @@ local function resolve_bufnr(bufnr) return bufnr end +--@private +--- Called by the client when trying to call a method that's not +--- supported in any of the servers registered for the current buffer. +--@param method (string) name of the method +function lsp._unsupported_method(method) + local msg = string.format("method %s is not supported by any of the servers registered for the current buffer", method) + log.warn(msg) + return lsp.rpc_response_error(protocol.ErrorCodes.MethodNotFound, msg) +end + +--@private +--- Checks whether a given path is a directory. +--- +--@param filename (string) path to check +--@returns true if {filename} exists and is a directory, false otherwise local function is_dir(filename) validate{filename={filename,'s'}} local stat = uv.fs_stat(filename) @@ -55,6 +106,10 @@ local valid_encodings = { } local client_index = 0 +--@private +--- Returns a new, unused client id. +--- +--@returns (number) client id local function next_client_id() client_index = client_index + 1 return client_index @@ -64,9 +119,15 @@ local active_clients = {} local all_buffer_active_clients = {} local uninitialized_clients = {} -local function for_each_buffer_client(bufnr, callback) +--@private +--- Invokes a function for each LSP client attached to the buffer {bufnr}. +--- +--@param bufnr (Number) of buffer +--@param fn (function({client}, {client_id}, {bufnr}) Function to run on +---each client attached to that buffer. +local function for_each_buffer_client(bufnr, fn) validate { - callback = { callback, 'f' }; + fn = { fn, 'f' }; } bufnr = resolve_bufnr(bufnr) local client_ids = all_buffer_active_clients[bufnr] @@ -76,7 +137,7 @@ local function for_each_buffer_client(bufnr, callback) for client_id in pairs(client_ids) do local client = active_clients[client_id] if client then - callback(client, client_id, bufnr) + fn(client, client_id, bufnr) end end end @@ -88,6 +149,11 @@ lsp.client_errors = tbl_extend("error", lsp_rpc.client_errors, vim.tbl_add_rever ON_INIT_CALLBACK_ERROR = table.maxn(lsp_rpc.client_errors) + 1; }) +--@private +--- Normalizes {encoding} to valid LSP encoding names. +--- +--@param encoding (string) Encoding to normalize +--@returns (string) normalized encoding name local function validate_encoding(encoding) validate { encoding = { encoding, 's' }; @@ -96,6 +162,13 @@ local function validate_encoding(encoding) or error(string.format("Invalid offset encoding %q. Must be one of: 'utf-8', 'utf-16', 'utf-32'", encoding)) end +--@internal +--- Parses a command invocation into the command itself and its args. If there +--- are no arguments, an empty table is returned as the second argument. +--- +--@param input (List) +--@returns (string) the command +--@returns (list of strings) its arguments function lsp._cmd_parts(input) vim.validate{cmd={ input, @@ -114,19 +187,36 @@ function lsp._cmd_parts(input) return cmd, cmd_args end +--@private +--- Augments a validator function with support for optional (nil) values. +--- +--@param fn (function(v)) The original validator function; should return a +---bool. +--@returns (function(v)) 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 +--@private +--- Validates a client configuration as given to |vim.lsp.start_client()|. +--- +--@param config (table) +--@returns (table) "Cleaned" config, containing only the command, its +---arguments, and a valid encoding. +--- +--@see |vim.lsp.start_client()| local function validate_client_config(config) validate { config = { config, 't' }; } validate { root_dir = { config.root_dir, is_dir, "directory" }; + -- TODO(remove-callbacks) callbacks = { config.callbacks, "t", true }; + 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 }; @@ -137,17 +227,32 @@ local function validate_client_config(config) before_init = { config.before_init, "f", true }; offset_encoding = { config.offset_encoding, "s", true }; } + + -- TODO(remove-callbacks) + if config.handlers and config.callbacks then + error(debug.traceback( + "Unable to configure LSP with both 'config.handlers' and 'config.callbacks'. Use 'config.handlers' exclusively." + )) + end + local cmd, cmd_args = lsp._cmd_parts(config.cmd) local offset_encoding = valid_encodings.UTF16 if config.offset_encoding then offset_encoding = validate_encoding(config.offset_encoding) end + return { - cmd = cmd; cmd_args = cmd_args; + cmd = cmd; + cmd_args = cmd_args; offset_encoding = offset_encoding; } end +--@private +--- Returns full text of buffer {bufnr} as a string. +--- +--@param bufnr (number) Buffer handle, or 0 for current. +--@returns Buffer text as string. local function buf_get_full_text(bufnr) local text = table.concat(nvim_buf_get_lines(bufnr, 0, -1, true), '\n') if nvim_buf_get_option(bufnr, 'eol') then @@ -156,6 +261,11 @@ local function buf_get_full_text(bufnr) return text end +--@private +--- Default handler for the 'textDocument/didOpen' LSP notification. +--- +--@param bufnr (Number) Number of the buffer, or 0 for current +--@param client Client object local function text_document_did_open_handler(bufnr, client) if not client.resolved_capabilities.text_document_open_close then return @@ -176,74 +286,88 @@ local function text_document_did_open_handler(bufnr, client) util.buf_versions[bufnr] = params.textDocument.version end ---- LSP client object. +-- FIXME: DOC: Shouldn't need to use a dummy function +-- +--- LSP client object. You can get an active client object via +--- |vim.lsp.get_client_by_id()| or |vim.lsp.get_active_clients()|. --- --- - Methods: --- ---- - request(method, params, [callback]) ---- Send a request to the server. If callback is not specified, it will use ---- {client.callbacks} to try to find a callback. If one is not found there, ---- then an error will occur. +--- - request(method, params, [handler], bufnr) +--- Sends a request to the server. --- This is a thin wrapper around {client.rpc.request} with some additional --- checking. ---- Returns a boolean to indicate if the notification was successful. If it ---- is false, then it will always be false (the client has shutdown). ---- If it was successful, then it will return the request id as the second ---- result. You can use this with `notify("$/cancel", { id = request_id })` ---- to cancel the request. This helper is made automatically with ---- |vim.lsp.buf_request()| ---- Returns: status, [client_id] +--- 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. --- --- - notify(method, params) ---- This is just {client.rpc.notify}() ---- Returns a boolean to indicate if the notification was successful. If it ---- is false, then it will always be false (the client has shutdown). ---- Returns: status +--- 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). --- --- - cancel_request(id) ---- This is just {client.rpc.notify}("$/cancelRequest", { id = id }) ---- Returns the same as `notify()`. +--- Cancels a request with a given request id. +--- Returns: same as `notify()`. --- --- - stop([force]) ---- Stop a client, optionally with force. +--- 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. --- --- - is_stopped() ---- Returns true if the client is fully stopped. +--- Checks whether a client is stopped. +--- Returns: true if the client is fully stopped. +--- +--- - on_attach(bufnr) +--- Runs the on_attach function from the client's config if it was defined. --- --- - Members ---- - id (number): The id allocated to the client. +--- - {id} (number): The id allocated to the client. --- ---- - name (string): If a name is specified on creation, that will be +--- - {name} (string): 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. --- ---- - offset_encoding (string): The encoding used for communicating ---- with the server. You can modify this in the `on_init` method +--- - {rpc} (table): RPC client object, for low level interaction with the +--- client. See |vim.lsp.rpc.start()|. +--- +--- - {offset_encoding} (string): 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. --- ---- - callbacks (table): The callbacks used by the client as ---- described in |lsp-callbacks|. +--- - {handlers} (table): The handlers used by the client as described in |lsp-handler|. --- ---- - config (table): copy of the table that was passed by the user +--- - {config} (table): copy of the table that was passed by the user --- to |vim.lsp.start_client()|. --- ---- - server_capabilities (table): Response from the server sent on +--- - {server_capabilities} (table): Response from the server sent on --- `initialize` describing the server's capabilities. --- ---- - resolved_capabilities (table): Normalized table of +--- - {resolved_capabilities} (table): Normalized table of --- capabilities that we have detected based on the initialize --- response from the server in `server_capabilities`. function lsp.client() error() end +-- FIXME: DOC: Currently all methods on the `vim.lsp.client` object are +-- documented twice: Here, and on the methods themselves (e.g. +-- `client.request()`). This is a workaround for the vimdoc generator script +-- not handling method names correctly. If you change the documentation on +-- either, please make sure to update the other as well. +-- --- Starts and initializes a client with the given configuration. --- --- Parameters `cmd` and `root_dir` are required. --- +--- The following parameters describe fields in the {config} table. +--- --@param root_dir: (required, string) Directory where the LSP server will base --- its rootUri on initialization. --- @@ -269,15 +393,7 @@ end --- `{[vim.type_idx]=vim.types.dictionary}`, else it will be encoded as an --- array. --- ---@param callbacks Map of language server method names to ---- `function(err, method, params, client_id)` handler. Invoked for: ---- - Notifications from the server, where `err` will always be `nil`. ---- - Requests initiated by the server. For these you can respond by returning ---- two values: `result, err` where err must be shaped like a RPC error, ---- i.e. `{ code, message, data? }`. Use |vim.lsp.rpc_response_error()| to ---- help with this. ---- - Default callback for client requests not explicitly specifying ---- a callback. +--@param handlers Map of language server method names to |lsp-handler| --- --@param init_options Values to pass in the initialization request --- as `initializationOptions`. See `initialize` in the LSP spec. @@ -297,7 +413,7 @@ end --@param before_init Callback with parameters (initialize_params, config) --- 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 `start_client()`. You can use this to modify parameters before +--- passed to |vim.lsp.start_client()|. You can use this to modify parameters before --- they are sent. --- --@param on_init Callback (client, initialize_result) invoked after LSP @@ -319,9 +435,8 @@ end --@param trace: "off" | "messages" | "verbose" | nil passed directly to the language --- server in the initialize request. Invalid/empty values will default to "off" --- ---@returns Client id. |vim.lsp.get_client_by_id()| Note: client is only ---- available after it has been initialized, which may happen after a small ---- delay (or never if there is an error). Use `on_init` to do any actions once +--@returns Client id. |vim.lsp.get_client_by_id()| Note: client may not be +--- fully initialized. Use `on_init` to do any actions once --- the client has been initialized. function lsp.start_client(config) local cleaned_config = validate_client_config(config) @@ -329,37 +444,62 @@ function lsp.start_client(config) local client_id = next_client_id() - local callbacks = config.callbacks or {} + -- TODO(remove-callbacks) + local handlers = config.handlers or config.callbacks or {} local name = config.name or tostring(client_id) local log_prefix = string.format("LSP[%s]", name) - local handlers = {} + local dispatch = {} - local function resolve_callback(method) - return callbacks[method] or default_callbacks[method] + --@private + --- Returns the handler associated with an LSP method. + --- Returns the default handler if the user hasn't set a custom one. + --- + --@param method (string) LSP method name + --@returns (fn) The handler for the given method, if defined, or the default from |vim.lsp.handlers| + local function resolve_handler(method) + return handlers[method] or default_handlers[method] end - function handlers.notification(method, params) + --@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 dispatch.notification(method, params) local _ = log.debug() and log.debug('notification', method, params) - local callback = resolve_callback(method) - if callback then + local handler = resolve_handler(method) + if handler then -- Method name is provided here for convenience. - callback(nil, method, params, client_id) + handler(nil, method, params, client_id) end end - function handlers.server_request(method, params) + --@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 + function dispatch.server_request(method, params) local _ = log.debug() and log.debug('server_request', method, params) - local callback = resolve_callback(method) - if callback then - local _ = log.debug() and log.debug("server_request: found callback for", method) - return callback(nil, method, params, client_id) + local handler = resolve_handler(method) + if handler then + local _ = log.debug() and log.debug("server_request: found handler for", method) + return handler(nil, method, params, client_id) end - local _ = log.debug() and log.debug("server_request: no callback found for", method) + local _ = log.debug() and log.debug("server_request: no handler found for", method) return nil, lsp.rpc_response_error(protocol.ErrorCodes.MethodNotFound) end - function handlers.on_error(code, err) + --@private + --- Invoked when the client operation throws an error. + --- + --@param code (number) Error code + --@param err (...) Other arguments may be passed depending on the error kind + --@see |vim.lsp.client_errors| for possible errors. Use + ---`vim.lsp.client_errors[code]` to get a human-friendly name. + function dispatch.on_error(code, err) local _ = log.error() and log.error(log_prefix, "on_error", { code = lsp.client_errors[code], err = err }) err_message(log_prefix, ': Error ', lsp.client_errors[code], ': ', vim.inspect(err)) if config.on_error then @@ -371,7 +511,12 @@ function lsp.start_client(config) end end - function handlers.on_exit(code, signal) + --@private + --- Invoked on client exit. + --- + --@param code (number) exit code of the process + --@param signal (number) the signal used to terminate (if any) + function dispatch.on_exit(code, signal) active_clients[client_id] = nil uninitialized_clients[client_id] = nil local active_buffers = {} @@ -384,7 +529,7 @@ function lsp.start_client(config) -- Buffer level cleanup vim.schedule(function() for _, bufnr in ipairs(active_buffers) do - util.buf_clear_diagnostics(bufnr) + lsp.diagnostic.clear(bufnr) end end) if config.on_exit then @@ -393,7 +538,7 @@ function lsp.start_client(config) end -- Start the RPC client. - local rpc = lsp_rpc.start(cmd, cmd_args, handlers, { + local rpc = lsp_rpc.start(cmd, cmd_args, dispatch, { cwd = config.cmd_cwd; env = config.cmd_env; }) @@ -403,14 +548,17 @@ function lsp.start_client(config) name = name; rpc = rpc; offset_encoding = offset_encoding; - callbacks = callbacks; config = config; + + -- TODO(remove-callbacks) + callbacks = handlers; + handlers = handlers; } - -- Store the uninitialized_clients for cleanup in case we exit before - -- initialize finishes. + -- Store the uninitialized_clients for cleanup in case we exit before initialize finishes. uninitialized_clients[client_id] = client; + --@private local function initialize() local valid_traces = { off = 'off'; messages = 'messages'; verbose = 'verbose'; @@ -466,6 +614,15 @@ function lsp.start_client(config) -- These are the cleaned up capabilities we use for dynamically deciding -- when to send certain events to clients. client.resolved_capabilities = protocol.resolve_capabilities(client.server_capabilities) + client.supports_method = function(method) + local required_capability = lsp._request_name_to_capability[method] + -- if we don't know about the method, assume that the client supports it. + if not required_capability then + return true + end + + return client.resolved_capabilities[required_capability] + end if config.on_init then local status, err = pcall(config.on_init, client, result) if not status then @@ -488,43 +645,53 @@ function lsp.start_client(config) end) end - local function unsupported_method(method) - local msg = "server doesn't support "..method - local _ = log.warn() and log.warn(msg) - err_message(msg) - return lsp.rpc_response_error(protocol.ErrorCodes.MethodNotFound, msg) - end - - --- Checks capabilities before rpc.request-ing. - function client.request(method, params, callback, bufnr) - if not callback then - callback = resolve_callback(method) - or error(string.format("not found: %q request callback for client %q.", method, client.name)) - end - local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, callback, bufnr) - -- TODO keep these checks or just let it go anyway? - if (not client.resolved_capabilities.hover and method == 'textDocument/hover') - or (not client.resolved_capabilities.signature_help and method == 'textDocument/signatureHelp') - or (not client.resolved_capabilities.goto_definition and method == 'textDocument/definition') - or (not client.resolved_capabilities.implementation and method == 'textDocument/implementation') - or (not client.resolved_capabilities.declaration and method == 'textDocument/declaration') - or (not client.resolved_capabilities.type_definition and method == 'textDocument/typeDefinition') - or (not client.resolved_capabilities.document_symbol and method == 'textDocument/documentSymbol') - or (not client.resolved_capabilities.workspace_symbol and method == 'textDocument/workspaceSymbol') - or (not client.resolved_capabilities.call_hierarchy and method == 'textDocument/prepareCallHierarchy') - then - callback(unsupported_method(method), method, nil, client_id, bufnr) - return + --@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) LSP request params. + --@param handler (function, optional) Response |lsp-handler| for this method. + --@param bufnr (number) Buffer handle (0 for current). + --@returns ({status}, [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()| + function client.request(method, params, handler, bufnr) + if not handler then + handler = resolve_handler(method) + or error(string.format("not found: %q request handler for client %q.", method, client.name)) end + local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, handler, bufnr) return rpc.request(method, params, function(err, result) - callback(err, method, result, client_id, bufnr) + handler(err, method, result, client_id, bufnr) end) end + --@private + --- Sends a notification to an LSP server. + --- + --@param method (string) LSP method name. + --@param params (optional, table) LSP request params. + --@param bufnr (number) Buffer handle, or 0 for current. + --@returns {status} (bool) true if the notification was successful. + ---If it is false, then it will always be false + ---(the client has shutdown). function client.notify(...) return rpc.notify(...) end + --@private + --- Cancels a request with a given request id. + --- + --@param id (number) id of request to cancel + --@returns true if any client returns true; false otherwise + --@see |vim.lsp.client.notify()| function client.cancel_request(id) validate{id = {id, 'n'}} return rpc.notify("$/cancelRequest", { id = id }) @@ -533,6 +700,14 @@ function lsp.start_client(config) -- Track this so that we can escalate automatically if we've alredy tried a -- graceful shutdown local tried_graceful_shutdown = false + --@private + --- 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 (bool, optional) function client.stop(force) local handle = rpc.handle if handle:is_closing() then @@ -554,10 +729,18 @@ function lsp.start_client(config) end) end + --@private + --- Checks whether a client is stopped. + --- + --@returns (bool) true if client is stopped or in the process of being + ---stopped; false otherwise function client.is_stopped() return rpc.handle:is_closing() end + --@private + --- Runs the on_attach function from the client's config if it was defined. + --@param bufnr (number) Buffer number function client._on_attach(bufnr) text_document_did_open_handler(bufnr, client) if config.on_attach then @@ -571,6 +754,12 @@ function lsp.start_client(config) return client_id end +--@private +--- Memoizes a function. On first run, the function return value is saved and +--- immediately returned on subsequent runs. +--- +--@param fn (function) Function to run +--@returns (function) Memoized function local function once(fn) local value return function(...) @@ -579,14 +768,21 @@ local function once(fn) end end +--@private +--@fn text_document_did_change_handler(_, bufnr, changedtick, firstline, lastline, new_lastline, old_byte_size, old_utf32_size, old_utf16_size) +--- Notify all attached clients that a buffer has changed. local text_document_did_change_handler do local encoding_index = { ["utf-8"] = 1; ["utf-16"] = 2; ["utf-32"] = 3; } text_document_did_change_handler = function(_, bufnr, changedtick, firstline, lastline, new_lastline, old_byte_size, old_utf32_size, old_utf16_size) - local _ = log.debug() and log.debug("on_lines", bufnr, changedtick, firstline, - lastline, new_lastline, old_byte_size, old_utf32_size, old_utf16_size, nvim_buf_get_lines(bufnr, firstline, new_lastline, true)) + + local _ = log.debug() and log.debug( + string.format("on_lines bufnr: %s, changedtick: %s, firstline: %s, lastline: %s, new_lastline: %s, old_byte_size: %s, old_utf32_size: %s, old_utf16_size: %s", + bufnr, changedtick, firstline, lastline, new_lastline, old_byte_size, old_utf32_size, old_utf16_size), + nvim_buf_get_lines(bufnr, firstline, new_lastline, true) + ) -- Don't do anything if there are no clients attached. if tbl_isempty(all_buffer_active_clients[bufnr] or {}) then @@ -730,14 +926,14 @@ function lsp.buf_is_attached(bufnr, client_id) return (all_buffer_active_clients[bufnr] or {})[client_id] == true end ---- Gets an active client by id, or nil if the id is invalid or the ---- client is not yet initialized. ---- +--- Gets a client by id, or nil if the id is invalid. +--- The returned client may not yet be fully initialized. +-- --@param client_id client id number --- ---@return |vim.lsp.client| object, or nil +--@returns |vim.lsp.client| object, or nil function lsp.get_client_by_id(client_id) - return active_clients[client_id] + return active_clients[client_id] or uninitialized_clients[client_id] end --- Stops a client(s). @@ -746,7 +942,7 @@ end --- To stop all clients: --- --- <pre> ---- vim.lsp.stop_client(lsp.get_active_clients()) +--- vim.lsp.stop_client(vim.lsp.get_active_clients()) --- </pre> --- --- By default asks the server to shutdown, unless stop was requested @@ -769,7 +965,7 @@ end --- Gets all active clients. --- ---@return Table of |vim.lsp.client| objects +--@returns Table of |vim.lsp.client| objects function lsp.get_active_clients() return vim.tbl_values(active_clients) end @@ -803,31 +999,46 @@ nvim_command("autocmd VimLeavePre * lua vim.lsp._vim_exit_handler()") --@param bufnr (number) Buffer handle, or 0 for current. --@param method (string) LSP method name --@param params (optional, table) Parameters to send to the server ---@param callback (optional, functionnil) Handler --- `function(err, method, params, client_id)` for this request. Defaults --- to the client callback in `client.callbacks`. See |lsp-callbacks|. +--@param handler (optional, function) See |lsp-handler| +-- If nil, follows resolution strategy defined in |lsp-handler-configuration| -- --@returns 2-tuple: --- - Map of client-id:request-id pairs for all successful requests. --- - Function which can be used to cancel all the requests. You could instead --- iterate all clients and call their `cancel_request()` methods. -function lsp.buf_request(bufnr, method, params, callback) +function lsp.buf_request(bufnr, method, params, handler) validate { bufnr = { bufnr, 'n', true }; method = { method, 's' }; - callback = { callback, 'f', true }; + handler = { handler, 'f', true }; } local client_request_ids = {} - for_each_buffer_client(bufnr, function(client, client_id, resolved_bufnr) - local request_success, request_id = client.request(method, params, callback, resolved_bufnr) - -- This could only fail if the client shut down in the time since we looked - -- it up and we did the request, which should be rare. - if request_success then - client_request_ids[client_id] = request_id + local method_supported = false + for_each_buffer_client(bufnr, function(client, client_id, resolved_bufnr) + if client.supports_method(method) then + method_supported = true + local request_success, request_id = client.request(method, params, handler, resolved_bufnr) + + -- This could only fail if the client shut down in the time since we looked + -- it up and we did the request, which should be rare. + if request_success then + client_request_ids[client_id] = request_id + end end end) + -- if no clients support the given method, call the handler with the proper + -- error message. + if not method_supported then + local unsupported_err = lsp._unsupported_method(method) + handler = handler or lsp.handlers[method] + if handler then + handler(unsupported_err, method, bufnr) + end + return + end + local function _cancel_all_requests() for client_id, request_id in pairs(client_request_ids) do local client = active_clients[client_id] @@ -856,11 +1067,11 @@ end function lsp.buf_request_sync(bufnr, method, params, timeout_ms) local request_results = {} local result_count = 0 - local function _callback(err, _method, result, client_id) + local function _sync_handler(err, _, result, client_id) request_results[client_id] = { error = err, result = result } result_count = result_count + 1 end - local client_request_ids, cancel = lsp.buf_request(bufnr, method, params, _callback) + local client_request_ids, cancel = lsp.buf_request(bufnr, method, params, _sync_handler) local expected_result_count = 0 for _ in pairs(client_request_ids) do expected_result_count = expected_result_count + 1 @@ -904,7 +1115,7 @@ end --@param findstart 0 or 1, decides behavior --@param base If findstart=0, text to match against --- ---@return (number) Decided by `findstart`: +--@returns (number) Decided by `findstart`: --- - findstart=0: column where the completion starts, or -2 or -3 --- - findstart=1: list of matches (actually just calls |complete()|) function lsp.omnifunc(findstart, base) @@ -948,6 +1159,10 @@ function lsp.omnifunc(findstart, base) return -2 end +---Checks whether a client is stopped. +--- +--@param client_id (Number) +--@returns true if client is stopped, false otherwise. function lsp.client_is_stopped(client_id) return active_clients[client_id] == nil end @@ -992,22 +1207,58 @@ function lsp.set_log_level(level) end --- Gets the path of the logfile used by the LSP client. +--@returns (String) Path to logfile. function lsp.get_log_path() return log.get_filename() end --- Define the LspDiagnostics signs if they're not defined already. -do - local function define_default_sign(name, properties) - if vim.tbl_isempty(vim.fn.sign_getdefined(name)) then - vim.fn.sign_define(name, properties) +--- Call {fn} for every client attached to {bufnr} +function lsp.for_each_buffer_client(bufnr, fn) + return for_each_buffer_client(bufnr, fn) +end + +--- Function to manage overriding defaults for LSP handlers. +--@param handler (function) See |lsp-handler| +--@param override_config (table) Table containing the keys to override behavior of the {handler} +function lsp.with(handler, override_config) + return function(err, method, params, client_id, bufnr, config) + return handler(err, method, params, client_id, bufnr, vim.tbl_deep_extend("force", config or {}, override_config)) + end +end + +--- Helper function to use when implementing a handler. +--- This will check that all of the keys in the user configuration +--- are valid keys and make sense to include for this handler. +--- +--- Will error on invalid keys (i.e. keys that do not exist in the options) +function lsp._with_extend(name, options, user_config) + user_config = user_config or {} + + local resulting_config = {} + for k, v in pairs(user_config) do + if options[k] == nil then + error(debug.traceback(string.format( + "Invalid option for `%s`: %s. Valid options are:\n%s", + name, + k, + vim.inspect(vim.tbl_keys(options)) + ))) + end + + resulting_config[k] = v + end + + for k, v in pairs(options) do + if resulting_config[k] == nil then + resulting_config[k] = v end end - define_default_sign('LspDiagnosticsErrorSign', {text='E', texthl='LspDiagnosticsErrorSign', linehl='', numhl=''}) - define_default_sign('LspDiagnosticsWarningSign', {text='W', texthl='LspDiagnosticsWarningSign', linehl='', numhl=''}) - define_default_sign('LspDiagnosticsInformationSign', {text='I', texthl='LspDiagnosticsInformationSign', linehl='', numhl=''}) - define_default_sign('LspDiagnosticsHintSign', {text='H', texthl='LspDiagnosticsHintSign', linehl='', numhl=''}) + + return resulting_config end +-- Define the LspDiagnostics signs if they're not defined already. +require('vim.lsp.diagnostic')._define_default_signs_and_highlights() + return lsp -- vim:sw=2 ts=2 et |