mirror of
https://github.com/neovim/neovim.git
synced 2024-12-20 03:05:11 -07:00
07d5dc8938
Problem: Language server version information missing from `:checkhealth vim.lsp`. Solution: Store `InitializeResult.serverInfo.version` from the `initialize` response and display for each client in `:checkhealth vim.lsp`.
1202 lines
40 KiB
Lua
1202 lines
40 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, 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: 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[]
|
|
---
|
|
--- 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
|
|
--- `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<fun(client: vim.lsp.Client, initialize_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 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,
|
|
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,
|
|
initialize_params,
|
|
config
|
|
)
|
|
|
|
log.trace(self._log_prefix, 'initialize_params', initialize_params)
|
|
|
|
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
|
|
|
|
-- 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)
|
|
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).
|
|
self:_process_request(request_id, 'complete')
|
|
end)
|
|
|
|
if success and request_id then
|
|
self:_process_request(request_id, 'pending', bufnr, method)
|
|
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 message = table.concat(vim.iter({ ... }):flatten():totable())
|
|
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
|
|
|
|
--- 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
|
|
|
|
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
|
|
vim.lsp._watchfiles.cancel(self.id)
|
|
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
|
|
if not reg.registerOptions or not reg.registerOptions.documentSelector then
|
|
return reg
|
|
end
|
|
local documentSelector = reg.registerOptions.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
|
|
if
|
|
not (filter.language and language ~= filter.language)
|
|
and not (filter.scheme and not vim.startswith(uri, filter.scheme .. ':'))
|
|
and not (filter.pattern and not vim.glob.to_lpeg(filter.pattern):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._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
|