Files
neovim/runtime/lua/vim/lsp/client.lua
Bartłomiej Maryńczak f184c562c5 fix(lsp): detect if Client:request resolved synchronously #33624
Problem:
In cases when the (in-process) LSP server responds to the request
immediately and calls `notify_reply_callback` the request will still be
marked as pending, because the code assumes that the response will occur
asynchronously. Then the request will be pending forever, because it was
already set as "completed" before we even set it as "pending".

A workaround is to wrap `notify_replay_callback` in `vim.shedule` ([like
so](https://github.com/neovim/neovim/pull/24338#issuecomment-2809568617)]
but that seems counterintuitive.

Solution:
Handle this case in Client:request().

(cherry picked from commit 8315697449)
2025-04-27 00:54:11 +00:00

1218 lines
41 KiB
Lua

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
local validate = vim.validate
--- @alias vim.lsp.client.on_init_cb fun(client: vim.lsp.Client, init_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: vim.lsp.ClientConfig)
--- @class vim.lsp.Client.Flags
--- @inlinedoc
---
--- Allow using incremental sync for buffer edits
--- (default: `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
--- "~" 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[]
---
--- (default false) Server requires a workspace (no "single file" support).
--- @field workspace_required? boolean
---
--- 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<string,function>
---
--- Map with language server specific settings.
--- See the {settings} in |vim.lsp.Client|.
--- @field settings? lsp.LSPObject
---
--- Table that maps string of clientside commands to user-defined functions.
--- Commands passed to `start()` 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<string,fun(command: lsp.Command, ctx: table)>
---
--- Values to pass in the initialization request as `initializationOptions`. See `initialize` in
--- the LSP spec.
--- @field init_options? lsp.LSPObject
---
--- Name in log messages.
--- (default: client-id)
--- @field name? string
---
--- Language ID as string. Defaults to the buffer filetype.
--- @field get_language_id? fun(bufnr: integer, filetype: string): string
---
--- Called "position encoding" in LSP spec, 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()|.
--- You can use this to modify parameters before they are sent.
--- @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 `init_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<fun(client: vim.lsp.Client, init_result: lsp.InitializeResult)>
---
--- 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<fun(code: integer, signal: integer, client_id: integer)>
---
--- Callback invoked when client attaches to a buffer.
--- @field on_attach? elem_or_list<fun(client: vim.lsp.Client, bufnr: integer)>
---
--- 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:
--- @field flags? vim.lsp.Client.Flags
---
--- Directory where the LSP server will base its workspaceFolders, rootUri, and rootPath on initialization.
--- @field root_dir? string
--- @class vim.lsp.Client.Progress: vim.Ringbuf<{token: integer|string, value: any}>
--- @field pending table<lsp.ProgressToken,lsp.LSPAny>
--- @class vim.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
---
--- Called "position encoding" in LSP spec,
--- 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<string,lsp.Handler>
---
--- 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<integer,{ type: string, bufnr: integer, method: string}?>
---
--- copy of the table that was passed by the user
--- to |vim.lsp.start()|.
--- @field config vim.lsp.ClientConfig
---
--- Response from the server sent on `initialize` describing the server's
--- capabilities.
--- @field server_capabilities lsp.ServerCapabilities?
---
--- Response from the server sent on `initialize` describing information about
--- the server.
--- @field server_info lsp.ServerInfo?
---
--- A ring buffer (|vim.ringbuf()|) containing progress messages
--- sent by the server.
--- @field progress vim.lsp.Client.Progress
---
--- @field initialized true?
---
--- The workspace folders configured in the client when the server starts.
--- This property is only available if the client supports workspace folders.
--- It can be `null` if the client supports workspace folders but none are
--- configured.
--- @field workspace_folders lsp.WorkspaceFolder[]?
--- @field root_dir string?
---
--- @field attached_buffers table<integer,true>
---
--- @field private _log_prefix string
---
--- Track this so that we can escalate automatically if we've already tried a
--- graceful shutdown
--- @field private _graceful_shutdown_failed true?
---
--- The initial trace setting. If omitted trace is disabled ("off").
--- trace = "off" | "messages" | "verbose";
--- @field private _trace 'off'|'messages'|'verbose'
---
--- Table of command name to function which is called if any LSP action
--- (code action, code lenses, ...) triggers the command.
--- Client commands take precedence over the global command registry.
--- @field commands table<string,fun(command: lsp.Command, ctx: table)>
---
--- Map with language server specific settings. These are returned to the
--- language server if requested via `workspace/configuration`. Keys are
--- case-sensitive.
--- @field settings lsp.LSPObject
---
--- 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)
--- @field capabilities lsp.ClientCapabilities
--- @field private registrations table<string,lsp.Registration[]>
--- @field dynamic_capabilities lsp.DynamicCapabilities
---
--- @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)
local Client = {}
Client.__index = Client
--- @param obj table<string,any>
--- @param cls table<string,function>
--- @param name string
local function method_wrapper(obj, cls, name)
local meth = assert(cls[name])
obj[name] = function(...)
local arg = select(1, ...)
if arg and getmetatable(arg) == cls then
-- First argument is self, call meth directly
return meth(...)
end
vim.deprecate('client.' .. name, 'client:' .. name, '0.13')
-- First argument is not self, insert it
return meth(obj, ...)
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, 'string')
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',
}
--- 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, 'string', true)
if not encoding then
return valid_encodings.utf16
end
return valid_encodings[encoding:lower()]
or error(
string.format(
"Invalid position 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
--- 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()|.
--- @param config vim.lsp.ClientConfig
local function validate_config(config)
validate('config', config, 'table')
validate('handlers', config.handlers, 'table', true)
validate('capabilities', config.capabilities, 'table', true)
validate('cmd_cwd', config.cmd_cwd, optional_validator(is_dir), 'directory')
validate('cmd_env', config.cmd_env, 'table', true)
validate('detached', config.detached, 'boolean', true)
validate('name', config.name, 'string', true)
validate('on_error', config.on_error, 'function', true)
validate('on_exit', config.on_exit, { 'function', 'table' }, true)
validate('on_init', config.on_init, { 'function', 'table' }, true)
validate('on_attach', config.on_attach, { 'function', 'table' }, true)
validate('settings', config.settings, 'table', true)
validate('commands', config.commands, 'table', true)
validate('before_init', config.before_init, { 'function', 'table' }, true)
validate('offset_encoding', config.offset_encoding, 'string', true)
validate('flags', config.flags, 'table', true)
validate('get_language_id', config.get_language_id, 'function', 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'
)
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
--- @param id integer
--- @param config vim.lsp.ClientConfig
--- @return string
local function get_name(id, config)
local name = config.name
if name then
return name
end
if type(config.cmd) == 'table' and config.cmd[1] then
return assert(vim.fs.basename(config.cmd[1]))
end
return tostring(id)
end
--- @nodoc
--- @param config vim.lsp.ClientConfig
--- @return vim.lsp.Client?
function Client.create(config)
validate_config(config)
client_index = client_index + 1
local id = client_index
local name = get_name(id, config)
--- @class vim.lsp.Client
local self = {
id = id,
config = config,
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 = {},
registrations = {},
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,
workspace_folders = lsp._get_workspace_folders(config.workspace_folders or config.root_dir),
root_dir = config.root_dir,
_before_init_cb = config.before_init,
_on_init_cbs = vim._ensure_list(config.on_init),
_on_exit_cbs = vim._ensure_list(config.on_exit),
_on_attach_cbs = vim._ensure_list(config.on_attach),
_on_error_cb = config.on_error,
_trace = get_trace(config.trace),
--- 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 vim.lsp.Client.Progress]],
--- @deprecated use client.progress instead
messages = { name = name, messages = {}, progress = {}, status = {} },
}
self.capabilities =
vim.tbl_deep_extend('force', lsp.protocol.make_client_capabilities(), self.capabilities or {})
--- @class lsp.DynamicCapabilities
--- @nodoc
self.dynamic_capabilities = {
capabilities = self.registrations,
client_id = id,
register = function(_, registrations)
return self:_register_dynamic(registrations)
end,
unregister = function(_, unregistrations)
return self:_unregister_dynamic(unregistrations)
end,
get = function(_, method, opts)
return self:_get_registration(method, opts and opts.bufnr)
end,
supports_registration = function(_, method)
return self:_supports_registration(method)
end,
supports = function(_, method, opts)
return self:_get_registration(method, opts and opts.bufnr) ~= nil
end,
}
--- @type table<string|integer, string> title of unfinished progress sequences by token
self.progress.pending = {}
--- @type vim.lsp.rpc.Dispatchers
local dispatchers = {
notification = function(...)
return self:_notification(...)
end,
server_request = function(...)
return self:_server_request(...)
end,
on_error = function(...)
return self:_on_error(...)
end,
on_exit = function(...)
return self:_on_exit(...)
end,
}
-- Start the RPC client.
local config_cmd = config.cmd
if type(config_cmd) == 'function' then
self.rpc = config_cmd(dispatchers)
else
self.rpc = lsp.rpc.start(config_cmd, dispatchers, {
cwd = config.cmd_cwd,
env = config.cmd_env,
detached = config.detached,
})
end
setmetatable(self, Client)
method_wrapper(self, Client, 'request')
method_wrapper(self, Client, 'request_sync')
method_wrapper(self, Client, 'notify')
method_wrapper(self, Client, 'cancel_request')
method_wrapper(self, Client, 'stop')
method_wrapper(self, Client, 'is_stopped')
method_wrapper(self, Client, 'on_attach')
method_wrapper(self, Client, 'supports_method')
return self
end
--- @private
--- @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
--- @nodoc
function Client:initialize()
local config = self.config
local root_uri --- @type string?
local root_path --- @type string?
if self.workspace_folders then
root_uri = self.workspace_folders[1].uri
root_path = vim.uri_to_fname(root_uri)
end
local init_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,
workspaceFolders = self.workspace_folders or vim.NIL,
-- User provided initialization options.
initializationOptions = config.init_options,
capabilities = self.capabilities,
trace = self._trace,
workDoneToken = '1',
}
self:_run_callbacks(
{ self._before_init_cb },
lsp.client_errors.BEFORE_INIT_CALLBACK_ERROR,
init_params,
config
)
log.trace(self._log_prefix, 'init_params', init_params)
local rpc = self.rpc
rpc.request('initialize', init_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
-- 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
self.server_info = result.serverInfo
if next(self.settings) then
self:notify(ms.workspace_didChangeConfiguration, { settings = self.settings })
end
-- If server is being restarted, make sure to re-attach to any previously attached buffers.
-- Save which buffers before on_init in case new buffers are attached.
local reattach_bufs = vim.deepcopy(self.attached_buffers)
self:_run_callbacks(self._on_init_cbs, lsp.client_errors.ON_INIT_CALLBACK_ERROR, self, result)
for buf in pairs(reattach_bufs) do
-- The buffer may have been detached in the on_init callback.
if self.attached_buffers[buf] then
self:on_attach(buf)
end
end
log.info(
self._log_prefix,
'server_capabilities',
{ server_capabilities = self.server_capabilities }
)
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? 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
--- @private
--- @param id integer
--- @param req_type 'pending'|'complete'|'cancel'|
--- @param bufnr? integer (only required for req_type='pending')
--- @param method? string (only required for req_type='pending')
function Client:_process_request(id, req_type, bufnr, method)
local pending = req_type == 'pending'
validate('id', id, 'number')
if pending then
validate('bufnr', bufnr, 'number')
validate('method', method, 'string')
end
local cur_request = self.requests[id]
if pending and cur_request then
log.error(
self._log_prefix,
('Cannot create request with id %d as one already exists'):format(id)
)
return
elseif not pending and not cur_request then
log.error(
self._log_prefix,
('Cannot find request with id %d whilst attempting to %s'):format(id, req_type)
)
return
end
if cur_request then
bufnr = cur_request.bufnr
method = cur_request.method
end
assert(bufnr and method)
local request = { type = req_type, bufnr = bufnr, method = method }
-- Clear 'complete' requests
-- Note 'pending' and 'cancelled' requests are cleared when the server sends a response
-- which is processed via the notify_reply_callback argument to rpc.request.
self.requests[id] = req_type ~= 'complete' and request or nil
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 = id, request = request },
})
end
--- 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? lsp.Handler Response |lsp-handler| for this method.
--- @param bufnr? integer (default: 0) Buffer handle, or 0 for current.
--- @return boolean status indicates whether the request was successful.
--- If it is `false`, then it will always be `false` (the client has shutdown).
--- @return integer? request_id Can be used with |Client:cancel_request()|.
--- `nil` is request failed.
--- 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)
bufnr = vim._resolve_bufnr(bufnr)
local version = lsp.util.buf_versions[bufnr]
log.debug(self._log_prefix, 'client.request', self.id, method, params, handler, bufnr)
-- Detect if request resolved synchronously (only possible with in-process servers).
local already_responded = false
local request_registered = false
-- NOTE: rpc.request might call an in-process (Lua) server, thus may be synchronous.
local success, request_id = self.rpc.request(method, params, function(err, result)
handler(err, result, {
method = method,
client_id = self.id,
bufnr = bufnr,
params = params,
version = version,
})
end, function(request_id)
-- Called when the server sends a response to the request (including cancelled acknowledgment).
if request_registered then
self:_process_request(request_id, 'complete')
end
already_responded = true
end)
if success and request_id and not already_responded then
self:_process_request(request_id, 'pending', bufnr, method)
request_registered = true
end
return success, request_id
end
-- TODO(lewis6991): duplicated from lsp.lua
local wait_result_reason = { [-1] = 'timeout', [-2] = 'interrupted', [-3] = 'error' }
--- Concatenates and writes a list of strings to the Vim error buffer.
---
--- @param ... string List to write to the buffer
local function err_message(...)
local chunks = { { table.concat(vim.iter({ ... }):flatten():totable()) } }
if vim.in_fast_event() then
vim.schedule(function()
api.nvim_echo(chunks, true, { err = true })
api.nvim_command('redraw')
end)
else
api.nvim_echo(chunks, true, { err = true })
api.nvim_command('redraw')
end
end
--- 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? Maximum time in milliseconds to wait for
--- a result. Defaults to 1000
--- @param bufnr? integer (default: 0) Buffer handle, or 0 for current.
--- @return {err: lsp.ResponseError?, result:any}? `result` and `err` from the |lsp-handler|.
--- `nil` is the request was unsuccessful
--- @return string? err On timeout, cancel or error, where `err` is a
--- string describing the failure reason.
--- @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
--- Sends a notification to an LSP server.
---
--- @param method string LSP method name.
--- @param params table? LSP request params.
--- @return boolean status indicating if the notification was successful.
--- If it is false, then 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
--- Cancels a request with a given request id.
---
--- @param id integer id of request to cancel
--- @return boolean status indicating if the notification was successful.
--- @see |Client:notify()|
function Client:cancel_request(id)
self:_process_request(id, 'cancel')
return self.rpc.notify(ms.dollar_cancelRequest, { id = id })
end
--- Stops a client, optionally with force.
---
--- By default, it will just request 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
function Client:stop(force)
local rpc = self.rpc
if rpc.is_closing() then
return
end
vim.lsp._watchfiles.cancel(self.id)
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
--- Get options for a method that is registered dynamically.
--- @param method string
function Client:_supports_registration(method)
local capability = vim.tbl_get(self.capabilities, unpack(vim.split(method, '/')))
return type(capability) == 'table' and capability.dynamicRegistration
end
--- @private
--- @param registrations lsp.Registration[]
function Client:_register_dynamic(registrations)
-- remove duplicates
self:_unregister_dynamic(registrations)
for _, reg in ipairs(registrations) do
local method = reg.method
if not self.registrations[method] then
self.registrations[method] = {}
end
table.insert(self.registrations[method], reg)
end
end
--- @param registrations lsp.Registration[]
function Client:_register(registrations)
self:_register_dynamic(registrations)
local unsupported = {} --- @type string[]
for _, reg in ipairs(registrations) do
local method = reg.method
if method == ms.workspace_didChangeWatchedFiles then
vim.lsp._watchfiles.register(reg, self.id)
elseif not self:_supports_registration(method) then
unsupported[#unsupported + 1] = method
end
end
if #unsupported > 0 then
local warning_tpl = 'The language server %s triggers a registerCapability '
.. 'handler for %s despite dynamicRegistration set to false. '
.. 'Report upstream, this warning is harmless'
log.warn(string.format(warning_tpl, self.name, table.concat(unsupported, ', ')))
end
end
--- @private
--- @param unregistrations lsp.Unregistration[]
function Client:_unregister_dynamic(unregistrations)
for _, unreg in ipairs(unregistrations) do
local sreg = self.registrations[unreg.method]
-- Unegister dynamic capability
for i, reg in ipairs(sreg or {}) do
if reg.id == unreg.id then
table.remove(sreg, i)
break
end
end
end
end
--- @param unregistrations lsp.Unregistration[]
function Client:_unregister(unregistrations)
self:_unregister_dynamic(unregistrations)
for _, unreg in ipairs(unregistrations) do
if unreg.method == ms.workspace_didChangeWatchedFiles then
vim.lsp._watchfiles.unregister(unreg, self.id)
end
end
end
--- @private
function Client:_get_language_id(bufnr)
return self.get_language_id(bufnr, vim.bo[bufnr].filetype)
end
--- @param method string
--- @param bufnr? integer
--- @return lsp.Registration?
function Client:_get_registration(method, bufnr)
bufnr = vim._resolve_bufnr(bufnr)
for _, reg in ipairs(self.registrations[method] or {}) do
local regoptions = reg.registerOptions --[[@as {documentSelector:lsp.TextDocumentFilter[]}]]
if not regoptions or not regoptions.documentSelector then
return reg
end
local documentSelector = regoptions.documentSelector
local language = self:_get_language_id(bufnr)
local uri = vim.uri_from_bufnr(bufnr)
local fname = vim.uri_to_fname(uri)
for _, filter in ipairs(documentSelector) do
local flang, fscheme, fpat = filter.language, filter.scheme, filter.pattern
if
not (flang and language ~= flang)
and not (fscheme and not vim.startswith(uri, fscheme .. ':'))
and not (type(fpat) == 'string' and not vim.glob.to_lpeg(fpat):match(fname))
then
return reg
end
end
end
end
--- 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
--- 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 = vim._resolve_bufnr(context.bufnr)
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
--- @type lsp.ExecuteCommandParams
local params = {
command = cmdname,
arguments = command.arguments,
}
self:request(ms.workspace_executeCommand, params, handler, context.bufnr)
end
--- 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 self:supports_method(ms.textDocument_didOpen) then
return
end
if not api.nvim_buf_is_loaded(bufnr) then
return
end
self:notify(ms.textDocument_didOpen, {
textDocument = {
version = lsp.util.buf_versions[bufnr],
uri = vim.uri_from_bufnr(bufnr),
languageId = self:_get_language_id(bufnr),
text = lsp._buf_get_full_text(bufnr),
},
})
-- 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 = lsp.diagnostic.get_namespace(self.id)
vim.diagnostic.show(namespace, bufnr)
end
end)
end
--- Runs the on_attach function from the client's config if it was defined.
--- Useful for buffer-local setup.
--- @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 },
})
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
-- 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)
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
--- Checks if a client supports a given method.
--- Always returns true for unknown off-spec methods.
---
--- Note: Some language server capabilities can be file specific.
--- @param method string
--- @param bufnr? integer
function Client:supports_method(method, bufnr)
-- Deprecated form
if type(bufnr) == 'table' then
--- @diagnostic disable-next-line:no-unknown
bufnr = bufnr.bufnr
end
local required_capability = lsp.protocol._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
end
local rmethod = lsp._resolve_to_request[method]
if rmethod then
if self:_supports_registration(rmethod) then
local reg = self:_get_registration(rmethod, bufnr)
return vim.tbl_get(reg or {}, 'registerOptions', 'resolveProvider') or false
end
else
if self:_supports_registration(method) then
return self:_get_registration(method, bufnr) ~= nil
end
end
return false
end
--- Get options for a method that is registered dynamically.
--- @param method string
--- @param bufnr? integer
--- @return lsp.LSPAny?
function Client:_get_registration_options(method, bufnr)
if not self:_supports_registration(method) then
return
end
local reg = self:_get_registration(method, bufnr)
if reg then
return reg.registerOptions
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._on_error_cb then
--- @type boolean, string
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))
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)
self:_run_callbacks(
self._on_exit_cbs,
lsp.client_errors.ON_EXIT_CALLBACK_ERROR,
code,
signal,
self.id
)
end
--- Add a directory to the workspace folders.
--- @param dir string?
function Client:_add_workspace_folder(dir)
for _, folder in pairs(self.workspace_folders or {}) do
if folder.name == dir then
print(dir, 'is already part of this workspace')
return
end
end
local wf = assert(lsp._get_workspace_folders(dir))
self:notify(ms.workspace_didChangeWorkspaceFolders, {
event = { added = wf, removed = {} },
})
if not self.workspace_folders then
self.workspace_folders = {}
end
vim.list_extend(self.workspace_folders, wf)
end
--- Remove a directory to the workspace folders.
--- @param dir string?
function Client:_remove_workspace_folder(dir)
local wf = assert(lsp._get_workspace_folders(dir))
self:notify(ms.workspace_didChangeWorkspaceFolders, {
event = { added = {}, removed = wf },
})
for idx, folder in pairs(self.workspace_folders) do
if folder.name == dir then
table.remove(self.workspace_folders, idx)
break
end
end
end
return Client