refactor(lsp): move more code to client.lua

The dispatchers used by the RPC client should be defined in the client,
so they have been moved there. Due to this, it also made sense to move
all code related to client configuration and the creation of the RPC
client there too.

Now vim.lsp.start_client is significantly simplified and now mostly
contains logic for tracking open clients.

- Renamed client.new -> client.start
This commit is contained in:
Lewis Russell 2024-02-11 12:37:20 +00:00 committed by GitHub
parent 8e86193502
commit ed1b66bd99
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 357 additions and 432 deletions

View File

@ -932,7 +932,7 @@ start({config}, {opts}) *vim.lsp.start()*
`ftplugin/<filetype_name>.lua` (See |ftplugin-name|)
Parameters: ~
• {config} (`table`) Same configuration as documented in
• {config} (`lsp.ClientConfig`) Same configuration as documented in
|vim.lsp.start_client()|
• {opts} (`lsp.StartOpts?`) Optional keyword arguments:
• reuse_client (fun(client: client, config: table): boolean)
@ -2173,17 +2173,14 @@ rpc_response_error({code}, {message}, {data})
See also: ~
• lsp.ErrorCodes See `vim.lsp.protocol.ErrorCodes`
*vim.lsp.rpc.start()*
start({cmd}, {cmd_args}, {dispatchers}, {extra_spawn_params})
start({cmd}, {dispatchers}, {extra_spawn_params}) *vim.lsp.rpc.start()*
Starts an LSP server process and create an LSP RPC client object to
interact with it. Communication with the spawned process happens via
stdio. For communication via TCP, spawn a process manually and use
|vim.lsp.rpc.connect()|
Parameters: ~
• {cmd} (`string`) Command to start the LSP server.
• {cmd_args} (`string[]`) List of additional string arguments
to pass to {cmd}.
• {cmd} (`string[]`) Command to start the LSP server.
• {dispatchers} (`vim.lsp.rpc.Dispatchers?`) Dispatchers for LSP
message types. Valid dispatcher names are:
• `"notification"`

View File

@ -1,8 +1,4 @@
---@diagnostic disable: invisible
local api = vim.api
local nvim_err_writeln, nvim_buf_get_lines, nvim_command, nvim_exec_autocmds =
api.nvim_err_writeln, api.nvim_buf_get_lines, api.nvim_command, api.nvim_exec_autocmds
local uv = vim.uv
local tbl_isempty, tbl_extend = vim.tbl_isempty, vim.tbl_extend
local validate = vim.validate
local if_nil = vim.F.if_nil
@ -71,14 +67,6 @@ lsp._request_name_to_capability = {
-- TODO improve handling of scratch buffers with LSP attached.
--- Concatenates and writes a list of strings to the Vim error buffer.
---
---@param ... string List to write to the buffer
local function err_message(...)
nvim_err_writeln(table.concat(vim.tbl_flatten({ ... })))
nvim_command('redraw')
end
--- Returns the buffer number for the given {bufnr}.
---
---@param bufnr (integer|nil) Buffer number to resolve. Defaults to current buffer
@ -104,30 +92,8 @@ function lsp._unsupported_method(method)
return msg
end
--- Checks whether a given path is a directory.
---
---@param filename (string) path to check
---@return boolean # true if {filename} exists and is a directory, false otherwise
local function is_dir(filename)
validate({ filename = { filename, 's' } })
local stat = uv.fs_stat(filename)
return stat and stat.type == 'directory' or false
end
local wait_result_reason = { [-1] = 'timeout', [-2] = 'interrupted', [-3] = 'error' }
local valid_encodings = {
['utf-8'] = 'utf-8',
['utf-16'] = 'utf-16',
['utf-32'] = 'utf-32',
['utf8'] = 'utf-8',
['utf16'] = 'utf-16',
['utf32'] = 'utf-32',
UTF8 = 'utf-8',
UTF16 = 'utf-16',
UTF32 = 'utf-32',
}
local format_line_ending = {
['unix'] = '\n',
['dos'] = '\r\n',
@ -141,14 +107,6 @@ function lsp._buf_get_line_ending(bufnr)
return format_line_ending[vim.bo[bufnr].fileformat] or '\n'
end
local client_index = 0
--- Returns a new, unused client id.
---
---@return integer client_id
local function next_client_id()
client_index = client_index + 1
return client_index
end
-- Tracks all clients created via lsp.start_client
local active_clients = {} --- @type table<integer,lsp.Client>
local all_buffer_active_clients = {} --- @type table<integer,table<integer,true>>
@ -199,115 +157,6 @@ lsp.client_errors = tbl_extend(
})
)
--- Normalizes {encoding} to valid LSP encoding names.
---
---@param encoding (string) Encoding to normalize
---@return string # normalized encoding name
local function validate_encoding(encoding)
validate({
encoding = { encoding, 's' },
})
return valid_encodings[encoding:lower()]
or error(
string.format(
"Invalid offset encoding %q. Must be one of: 'utf-8', 'utf-16', 'utf-32'",
encoding
)
)
end
---@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 string[]
---@return string command, string[] args #the command and arguments
function lsp._cmd_parts(input)
validate({
cmd = {
input,
function()
return vim.tbl_islist(input)
end,
'list',
},
})
local cmd = input[1]
local cmd_args = {}
-- Don't mutate our input.
for i, v in ipairs(input) do
validate({ ['cmd argument'] = { v, 's' } })
if i > 1 then
table.insert(cmd_args, v)
end
end
return cmd, cmd_args
end
--- Augments a validator function with support for optional (nil) values.
---
---@param fn (fun(v): boolean) The original validator function; should return a
---bool.
---@return fun(v): boolean # The augmented function. Also returns true if {v} is
---`nil`.
local function optional_validator(fn)
return function(v)
return v == nil or fn(v)
end
end
--- Validates a client configuration as given to |vim.lsp.start_client()|.
---
---@param config (lsp.ClientConfig)
---@return (string|fun(dispatchers:vim.lsp.rpc.Dispatchers):vim.lsp.rpc.PublicClient?) Command
---@return string[] Arguments
---@return string Encoding.
local function validate_client_config(config)
validate({
config = { config, 't' },
})
validate({
handlers = { config.handlers, 't', true },
capabilities = { config.capabilities, 't', true },
cmd_cwd = { config.cmd_cwd, optional_validator(is_dir), 'directory' },
cmd_env = { config.cmd_env, 't', true },
detached = { config.detached, 'b', true },
name = { config.name, 's', true },
on_error = { config.on_error, 'f', true },
on_exit = { config.on_exit, 'f', true },
on_init = { config.on_init, 'f', true },
settings = { config.settings, 't', true },
commands = { config.commands, 't', true },
before_init = { config.before_init, 'f', true },
offset_encoding = { config.offset_encoding, 's', true },
flags = { config.flags, 't', true },
get_language_id = { config.get_language_id, 'f', true },
})
assert(
(
not config.flags
or not config.flags.debounce_text_changes
or type(config.flags.debounce_text_changes) == 'number'
),
'flags.debounce_text_changes must be a number with the debounce time in milliseconds'
)
local cmd, cmd_args --- @type (string|fun(dispatchers:vim.lsp.rpc.Dispatchers):vim.lsp.rpc.PublicClient), string[]
local config_cmd = config.cmd
if type(config_cmd) == 'function' then
cmd = config_cmd
else
cmd, cmd_args = lsp._cmd_parts(config_cmd)
end
local offset_encoding = valid_encodings.UTF16
if config.offset_encoding then
offset_encoding = validate_encoding(config.offset_encoding)
end
return cmd, cmd_args, offset_encoding
end
---@private
--- Returns full text of buffer {bufnr} as a string.
---
@ -315,7 +164,7 @@ end
---@return string # Buffer text as string.
function lsp._buf_get_full_text(bufnr)
local line_ending = lsp._buf_get_line_ending(bufnr)
local text = table.concat(nvim_buf_get_lines(bufnr, 0, -1, true), line_ending)
local text = table.concat(api.nvim_buf_get_lines(bufnr, 0, -1, true), line_ending)
if vim.bo[bufnr].eol then
text = text .. line_ending
end
@ -473,8 +322,8 @@ end
--- Either use |:au|, |nvim_create_autocmd()| or put the call in a
--- `ftplugin/<filetype_name>.lua` (See |ftplugin-name|)
---
---@param config table Same configuration as documented in |vim.lsp.start_client()|
---@param opts (nil|lsp.StartOpts) Optional keyword arguments:
---@param config lsp.ClientConfig Same configuration as documented in |vim.lsp.start_client()|
---@param opts lsp.StartOpts? Optional keyword arguments:
--- - reuse_client (fun(client: client, config: table): boolean)
--- Predicate used to decide if a client should be re-used.
--- Used on all running clients.
@ -483,20 +332,16 @@ end
--- - bufnr (number)
--- Buffer handle to attach to if starting or re-using a
--- client (0 for current).
---@return integer|nil client_id
---@return integer? client_id
function lsp.start(config, opts)
opts = opts or {}
local reuse_client = opts.reuse_client
or function(client, conf)
return client.config.root_dir == conf.root_dir and client.name == conf.name
end
if not config.name and type(config.cmd) == 'table' then
config.name = config.cmd[1] and vim.fs.basename(config.cmd[1]) or nil
end
local bufnr = opts.bufnr
if bufnr == nil or bufnr == 0 then
bufnr = api.nvim_get_current_buf()
end
local bufnr = resolve_bufnr(opts.bufnr)
for _, clients in ipairs({ uninitialized_clients, lsp.get_clients() }) do
for _, client in pairs(clients) do
if reuse_client(client, config) then
@ -505,10 +350,13 @@ function lsp.start(config, opts)
end
end
end
local client_id = lsp.start_client(config)
if client_id == nil then
return nil -- lsp.start_client will have printed an error
if not client_id then
return -- lsp.start_client will have printed an error
end
lsp.buf_attach_client(bufnr, client_id)
return client_id
end
@ -599,29 +447,6 @@ function lsp._set_defaults(client, bufnr)
end
end
--- @class lsp.ClientConfig
--- @field cmd (string[]|fun(dispatchers: table):table)
--- @field cmd_cwd string
--- @field cmd_env (table)
--- @field detached boolean
--- @field workspace_folders (table)
--- @field capabilities lsp.ClientCapabilities
--- @field handlers table<string,function>
--- @field settings table
--- @field commands table
--- @field init_options table
--- @field name string
--- @field get_language_id fun(bufnr: integer, filetype: string): string
--- @field offset_encoding string
--- @field on_error fun(code: integer)
--- @field before_init function
--- @field on_init function
--- @field on_exit fun(code: integer, signal: integer, client_id: integer)
--- @field on_attach fun(client: lsp.Client, bufnr: integer)
--- @field trace 'off'|'messages'|'verbose'|nil
--- @field flags table
--- @field root_dir string
--- Reset defaults set by `set_defaults`.
--- Must only be called if the last client attached to a buffer exits.
local function reset_defaults(bufnr)
@ -642,6 +467,90 @@ local function reset_defaults(bufnr)
end)
end
--- @param client lsp.Client
local function on_client_init(client)
local id = client.id
uninitialized_clients[id] = nil
-- Only assign after initialized.
active_clients[id] = client
-- If we had been registered before we start, then send didOpen This can
-- happen if we attach to buffers before initialize finishes or if
-- someone restarts a client.
for bufnr, client_ids in pairs(all_buffer_active_clients) do
if client_ids[id] then
client.on_attach(bufnr)
end
end
end
--- @param code integer
--- @param signal integer
--- @param client_id integer
local function on_client_exit(code, signal, client_id)
local client = active_clients[client_id] or uninitialized_clients[client_id]
for bufnr, client_ids in pairs(all_buffer_active_clients) do
if client_ids[client_id] then
vim.schedule(function()
if client and client.attached_buffers[bufnr] then
api.nvim_exec_autocmds('LspDetach', {
buffer = bufnr,
modeline = false,
data = { client_id = client_id },
})
end
local namespace = vim.lsp.diagnostic.get_namespace(client_id)
vim.diagnostic.reset(namespace, bufnr)
client_ids[client_id] = nil
if vim.tbl_isempty(client_ids) then
reset_defaults(bufnr)
end
end)
end
end
local name = client.name or 'unknown'
-- Schedule the deletion of the client object so that it exists in the execution of LspDetach
-- autocommands
vim.schedule(function()
active_clients[client_id] = nil
uninitialized_clients[client_id] = nil
-- Client can be absent if executable starts, but initialize fails
-- init/attach won't have happened
if client then
changetracking.reset(client)
end
if code ~= 0 or (signal ~= 0 and signal ~= 15) then
local msg = string.format(
'Client %s quit with exit code %s and signal %s. Check log for errors: %s',
name,
code,
signal,
lsp.get_log_path()
)
vim.notify(msg, vim.log.levels.WARN)
end
end)
end
--- @generic F: function
--- @param ... F
--- @return F
local function join_cbs(...)
local funcs = vim.F.pack_len(...)
return function(...)
for i = 1, funcs.n do
if funcs[i] ~= nil then
funcs[i](...)
end
end
end
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
@ -762,192 +671,22 @@ end
--- fully initialized. Use `on_init` to do any actions once
--- the client has been initialized.
function lsp.start_client(config)
local cmd, cmd_args, offset_encoding = validate_client_config(config)
config = vim.deepcopy(config, false)
config.on_init = join_cbs(config.on_init, on_client_init)
config.on_exit = join_cbs(config.on_exit, on_client_exit)
config.flags = config.flags or {}
config.settings = config.settings or {}
local client = require('vim.lsp.client').start(config)
-- By default, get_language_id just returns the exact filetype it is passed.
-- It is possible to pass in something that will calculate a different filetype,
-- to be sent by the client.
config.get_language_id = config.get_language_id or function(_, filetype)
return filetype
end
local client_id = next_client_id()
local handlers = config.handlers or {}
local name = config.name or tostring(client_id)
local log_prefix = string.format('LSP[%s]', name)
local dispatch = {}
--- Returns the handler associated with an LSP method.
--- Returns the default handler if the user hasn't set a custom one.
---
---@param method (string) LSP method name
---@return lsp.Handler|nil handler for the given method, if defined, or the default from |vim.lsp.handlers|
local function resolve_handler(method)
return handlers[method] or lsp.handlers[method]
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 dispatch.notification(method, params)
log.trace('notification', method, params)
local handler = resolve_handler(method)
if handler then
-- Method name is provided here for convenience.
handler(nil, params, { method = method, client_id = client_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 dispatch.server_request(method, params)
log.trace('server_request', method, params)
local handler = resolve_handler(method)
if handler then
log.trace('server_request: found handler for', method)
return handler(nil, params, { method = method, client_id = client_id })
end
log.warn('server_request: no handler found for', method)
return nil, lsp.rpc_response_error(protocol.ErrorCodes.MethodNotFound)
end
--- Logs the given error to the LSP log and to the error buffer.
--- @param code integer Error code
--- @param err any Error arguments
local function write_error(code, err)
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))
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 dispatch.on_error(code, err)
write_error(code, err)
if config.on_error then
local status, usererr = pcall(config.on_error, code, err)
if not status then
log.error(log_prefix, 'user on_error failed', { err = usererr })
err_message(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 dispatch.on_exit(code, signal)
if config.on_exit then
pcall(config.on_exit, code, signal, client_id)
end
local client = active_clients[client_id] and active_clients[client_id]
or uninitialized_clients[client_id]
for bufnr, client_ids in pairs(all_buffer_active_clients) do
if client_ids[client_id] then
vim.schedule(function()
if client and client.attached_buffers[bufnr] then
nvim_exec_autocmds('LspDetach', {
buffer = bufnr,
modeline = false,
data = { client_id = client_id },
})
end
local namespace = vim.lsp.diagnostic.get_namespace(client_id)
vim.diagnostic.reset(namespace, bufnr)
client_ids[client_id] = nil
if vim.tbl_isempty(client_ids) then
reset_defaults(bufnr)
end
end)
end
end
-- Schedule the deletion of the client object so that it exists in the execution of LspDetach
-- autocommands
vim.schedule(function()
active_clients[client_id] = nil
uninitialized_clients[client_id] = nil
-- Client can be absent if executable starts, but initialize fails
-- init/attach won't have happened
if client then
changetracking.reset(client)
end
if code ~= 0 or (signal ~= 0 and signal ~= 15) then
local msg = string.format(
'Client %s quit with exit code %s and signal %s. Check log for errors: %s',
name,
code,
signal,
lsp.get_log_path()
)
vim.notify(msg, vim.log.levels.WARN)
end
end)
end
-- Start the RPC client.
local rpc --- @type vim.lsp.rpc.PublicClient?
if type(cmd) == 'function' then
rpc = cmd(dispatch)
else
rpc = lsp.rpc.start(cmd, cmd_args, dispatch, {
cwd = config.cmd_cwd,
env = config.cmd_env,
detached = config.detached,
})
end
-- Return nil if client fails to start
if not rpc then
if not client then
return
end
config.capabilities = config.capabilities or protocol.make_client_capabilities()
local client = require('vim.lsp.client').new(client_id, rpc, handlers, offset_encoding, config)
-- Store the uninitialized_clients for cleanup in case we exit before initialize finishes.
uninitialized_clients[client_id] = client
-- TODO(lewis6991): do this on before_init(). Requires API change to before_init() so it
-- can access the client_id.
uninitialized_clients[client.id] = client
client:initialize(function()
uninitialized_clients[client_id] = nil
-- Only assign after initialized.
active_clients[client_id] = client
-- If we had been registered before we start, then send didOpen This can
-- happen if we attach to buffers before initialize finishes or if
-- someone restarts a client.
for bufnr, client_ids in pairs(all_buffer_active_clients) do
if client_ids[client_id] then
client.on_attach(bufnr)
end
end
end)
return client_id
return client.id
end
--- Notify all attached clients that a buffer has changed.
@ -1149,7 +888,7 @@ function lsp.buf_detach_client(bufnr, client_id)
return
end
nvim_exec_autocmds('LspDetach', {
api.nvim_exec_autocmds('LspDetach', {
buffer = bufnr,
modeline = false,
data = { client_id = client_id },
@ -1372,7 +1111,7 @@ function lsp.buf_request(bufnr, method, params, handler)
-- if has client but no clients support the given method, notify the user
if next(clients) and not method_supported then
vim.notify(lsp._unsupported_method(method), vim.log.levels.ERROR)
nvim_command('redraw')
vim.cmd.redraw()
return {}, function() end
end

View File

@ -4,6 +4,30 @@ local lsp = vim.lsp
local log = lsp.log
local ms = lsp.protocol.Methods
local changetracking = lsp._changetracking
local validate = vim.validate
--- @class lsp.ClientConfig
--- @field cmd (string[]|fun(dispatchers: table):table)
--- @field cmd_cwd string
--- @field cmd_env (table)
--- @field detached boolean
--- @field workspace_folders (table)
--- @field capabilities lsp.ClientCapabilities
--- @field handlers table<string,function>
--- @field settings table
--- @field commands table
--- @field init_options table
--- @field name? string
--- @field get_language_id fun(bufnr: integer, filetype: string): string
--- @field offset_encoding string
--- @field on_error fun(code: integer)
--- @field before_init fun(params: lsp.InitializeParams, config: lsp.ClientConfig)
--- @field on_init fun(client: lsp.Client, initialize_result: lsp.InitializeResult)
--- @field on_exit fun(code: integer, signal: integer, client_id: integer)
--- @field on_attach fun(client: lsp.Client, bufnr: integer)
--- @field trace 'off'|'messages'|'verbose'|nil
--- @field flags table
--- @field root_dir string
--- @class lsp.Client.Progress: vim.Ringbuf<{token: integer|string, value: any}>
--- @field pending table<lsp.ProgressToken,lsp.LSPAny>
@ -51,7 +75,6 @@ local changetracking = lsp._changetracking
--- @field initialized true?
--- @field workspace_folders lsp.WorkspaceFolder[]?
--- @field attached_buffers table<integer,true>
--- @field commands table<string,function>
--- @field private _log_prefix string
--- Track this so that we can escalate automatically if we've already tried a
--- graceful shutdown
@ -119,27 +142,131 @@ local function method_wrapper(cls, meth)
end
end
--- @package
--- @param id integer
--- @param rpc vim.lsp.rpc.PublicClient
--- @param handlers table<string,lsp.Handler>
--- @param offset_encoding string
local client_index = 0
--- Checks whether a given path is a directory.
--- @param filename (string) path to check
--- @return boolean # true if {filename} exists and is a directory, false otherwise
local function is_dir(filename)
validate({ filename = { filename, 's' } })
local stat = uv.fs_stat(filename)
return stat and stat.type == 'directory' or false
end
local valid_encodings = {
['utf-8'] = 'utf-8',
['utf-16'] = 'utf-16',
['utf-32'] = 'utf-32',
['utf8'] = 'utf-8',
['utf16'] = 'utf-16',
['utf32'] = 'utf-32',
UTF8 = 'utf-8',
UTF16 = 'utf-16',
UTF32 = 'utf-32',
}
--- Normalizes {encoding} to valid LSP encoding names.
--- @param encoding string? Encoding to normalize
--- @return string # normalized encoding name
local function validate_encoding(encoding)
validate({
encoding = { encoding, 's', true },
})
if not encoding then
return valid_encodings.UTF16
end
return valid_encodings[encoding:lower()]
or error(
string.format(
"Invalid offset encoding %q. Must be one of: 'utf-8', 'utf-16', 'utf-32'",
encoding
)
)
end
--- Augments a validator function with support for optional (nil) values.
--- @param fn (fun(v): boolean) The original validator function; should return a
--- bool.
--- @return fun(v): boolean # The augmented function. Also returns true if {v} is
--- `nil`.
local function optional_validator(fn)
return function(v)
return v == nil or fn(v)
end
end
--- Validates a client configuration as given to |vim.lsp.start_client()|.
--- @param config lsp.ClientConfig
--- @return lsp.Client
function Client.new(id, rpc, handlers, offset_encoding, config)
local function process_client_config(config)
validate({
config = { config, 't' },
})
validate({
handlers = { config.handlers, 't', true },
capabilities = { config.capabilities, 't', true },
cmd_cwd = { config.cmd_cwd, optional_validator(is_dir), 'directory' },
cmd_env = { config.cmd_env, 't', true },
detached = { config.detached, 'b', true },
name = { config.name, 's', true },
on_error = { config.on_error, 'f', true },
on_exit = { config.on_exit, 'f', true },
on_init = { config.on_init, 'f', true },
settings = { config.settings, 't', true },
commands = { config.commands, 't', true },
before_init = { config.before_init, 'f', true },
offset_encoding = { config.offset_encoding, 's', true },
flags = { config.flags, 't', true },
get_language_id = { config.get_language_id, 'f', true },
})
assert(
(
not config.flags
or not config.flags.debounce_text_changes
or type(config.flags.debounce_text_changes) == 'number'
),
'flags.debounce_text_changes must be a number with the debounce time in milliseconds'
)
if not config.name and type(config.cmd) == 'table' then
config.name = config.cmd[1] and vim.fs.basename(config.cmd[1]) or nil
end
config.offset_encoding = validate_encoding(config.offset_encoding)
config.flags = config.flags or {}
config.settings = config.settings or {}
config.handlers = config.handlers or {}
-- By default, get_language_id just returns the exact filetype it is passed.
-- It is possible to pass in something that will calculate a different filetype,
-- to be sent by the client.
config.get_language_id = config.get_language_id or function(_, filetype)
return filetype
end
config.capabilities = config.capabilities or lsp.protocol.make_client_capabilities()
config.commands = config.commands or {}
end
--- @package
--- @param config lsp.ClientConfig
--- @return lsp.Client?
function Client.start(config)
process_client_config(config)
client_index = client_index + 1
local id = client_index
local name = config.name or tostring(id)
--- @class lsp.Client
local self = {
id = id,
config = config,
handlers = handlers,
rpc = rpc,
offset_encoding = offset_encoding,
handlers = config.handlers,
offset_encoding = config.offset_encoding,
name = name,
_log_prefix = string.format('LSP[%s]', name),
requests = {},
commands = config.commands or {},
attached_buffers = {},
server_capabilities = {},
dynamic_capabilities = vim.lsp._dynamic.new(id),
@ -165,15 +292,46 @@ function Client.new(id, rpc, handlers, offset_encoding, config)
self.on_attach = method_wrapper(self, Client._on_attach)
self.supports_method = method_wrapper(self, Client._supports_method)
---@type table<string|integer, string> title of unfinished progress sequences by token
--- @type table<string|integer, string> title of unfinished progress sequences by token
self.progress.pending = {}
return setmetatable(self, Client)
--- @type vim.lsp.rpc.Dispatchers
local dispatchers = {
notification = method_wrapper(self, Client._notification),
server_request = method_wrapper(self, Client._server_request),
on_error = method_wrapper(self, Client._on_error),
on_exit = method_wrapper(self, Client._on_exit),
}
-- Start the RPC client.
local rpc --- @type vim.lsp.rpc.PublicClient?
local config_cmd = config.cmd
if type(config_cmd) == 'function' then
rpc = config_cmd(dispatchers)
else
rpc = lsp.rpc.start(config_cmd, dispatchers, {
cwd = config.cmd_cwd,
env = config.cmd_env,
detached = config.detached,
})
end
-- Return nil if the rpc client fails to start
if not rpc then
return
end
self.rpc = rpc
setmetatable(self, Client)
self:initialize()
return self
end
--- @private
--- @param cb fun()
function Client:initialize(cb)
function Client:initialize()
local valid_traces = {
off = 'off',
messages = 'messages',
@ -282,8 +440,6 @@ function Client:initialize(cb)
'server_capabilities',
{ server_capabilities = self.server_capabilities }
)
cb()
end)
end
@ -302,7 +458,7 @@ end
--- @param bufnr (integer|nil) Buffer number to resolve. Defaults to current buffer
--- @return integer bufnr
local function resolve_bufnr(bufnr)
vim.validate({ bufnr = { bufnr, 'n', true } })
validate({ bufnr = { bufnr, 'n', true } })
if bufnr == nil or bufnr == 0 then
return api.nvim_get_current_buf()
end
@ -374,10 +530,9 @@ end
-- TODO(lewis6991): duplicated from lsp.lua
local wait_result_reason = { [-1] = 'timeout', [-2] = 'interrupted', [-3] = 'error' }
-- TODO(lewis6991): duplicated from lsp.lua
--- Concatenates and writes a list of strings to the Vim error buffer.
---
---@param ... string List to write to the buffer
--- @param ... string List to write to the buffer
local function err_message(...)
api.nvim_err_writeln(table.concat(vim.tbl_flatten({ ... })))
api.nvim_command('redraw')
@ -461,7 +616,7 @@ end
--- @return boolean status true if notification was successful. false otherwise
--- @see |vim.lsp.client.notify()|
function Client:_cancel_request(id)
vim.validate({ id = { id, 'n' } })
validate({ id = { id, 'n' } })
local request = self.requests[id]
if request and request.type == 'pending' then
request.type = 'cancel'
@ -527,7 +682,7 @@ function Client:_exec_cmd(command, context, handler)
context.bufnr = context.bufnr or api.nvim_get_current_buf()
context.client_id = self.id
local cmdname = command.command
local fn = self.commands[cmdname] or lsp.commands[cmdname]
local fn = self.config.commands[cmdname] or lsp.commands[cmdname]
if fn then
fn(command, context)
return
@ -654,4 +809,67 @@ function Client:_supports_method(method, opts)
end
end
--- @private
--- Handles a notification sent by an LSP server by invoking the
--- corresponding handler.
---
--- @param method string LSP method name
--- @param params table The parameters for that method.
function Client:_notification(method, params)
log.trace('notification', method, params)
local handler = self:_resolve_handler(method)
if handler then
-- Method name is provided here for convenience.
handler(nil, params, { method = method, client_id = self.id })
end
end
--- @private
--- Handles a request from an LSP server by invoking the corresponding handler.
---
--- @param method (string) LSP method name
--- @param params (table) The parameters for that method
--- @return any result
--- @return lsp.ResponseError error code and message set in case an exception happens during the request.
function Client:_server_request(method, params)
log.trace('server_request', method, params)
local handler = self:_resolve_handler(method)
if handler then
log.trace('server_request: found handler for', method)
return handler(nil, params, { method = method, client_id = self.id })
end
log.warn('server_request: no handler found for', method)
return nil, lsp.rpc_response_error(lsp.protocol.ErrorCodes.MethodNotFound)
end
--- @private
--- Invoked when the client operation throws an error.
---
--- @param code integer Error code
--- @param err any Other arguments may be passed depending on the error kind
--- @see vim.lsp.rpc.client_errors for possible errors. Use
--- `vim.lsp.rpc.client_errors[code]` to get a human-friendly name.
function Client:_on_error(code, err)
self:write_error(code, err)
if self.config.on_error then
--- @type boolean, string
local status, usererr = pcall(self.config.on_error, code, err)
if not status then
log.error(self._log_prefix, 'user on_error failed', { err = usererr })
err_message(self._log_prefix, ' user on_error failed: ', tostring(usererr))
end
end
end
--- @private
--- Invoked on client exit.
---
--- @param code integer) exit code of the process
--- @param signal integer the signal used to terminate (if any)
function Client:_on_exit(code, signal)
if self.config.on_exit then
pcall(self.config.on_exit, code, signal, self.id)
end
end
return Client

View File

@ -732,8 +732,7 @@ end
--- interact with it. Communication with the spawned process happens via stdio. For
--- communication via TCP, spawn a process manually and use |vim.lsp.rpc.connect()|
---
---@param cmd string Command to start the LSP server.
---@param cmd_args string[] List of additional string arguments to pass to {cmd}.
---@param cmd string[] Command to start the LSP server.
---
---@param dispatchers? vim.lsp.rpc.Dispatchers Dispatchers for LSP message types.
--- Valid dispatcher names are:
@ -754,12 +753,11 @@ end
--- - `request()` |vim.lsp.rpc.request()|
--- - `is_closing()` returns a boolean indicating if the RPC is closing.
--- - `terminate()` terminates the RPC client.
function M.start(cmd, cmd_args, dispatchers, extra_spawn_params)
log.info('Starting RPC client', { cmd = cmd, args = cmd_args, extra = extra_spawn_params })
function M.start(cmd, dispatchers, extra_spawn_params)
log.info('Starting RPC client', { cmd = cmd, extra = extra_spawn_params })
validate({
cmd = { cmd, 's' },
cmd_args = { cmd_args, 't' },
cmd = { cmd, 't' },
dispatchers = { dispatchers, 't', true },
})
@ -795,7 +793,7 @@ function M.start(cmd, cmd_args, dispatchers, extra_spawn_params)
local stderr_handler = function(_, chunk)
if chunk then
log.error('rpc', cmd, 'stderr', chunk)
log.error('rpc', cmd[1], 'stderr', chunk)
end
end
@ -804,10 +802,7 @@ function M.start(cmd, cmd_args, dispatchers, extra_spawn_params)
detached = extra_spawn_params.detached
end
local cmd1 = { cmd }
vim.list_extend(cmd1, cmd_args)
local ok, sysobj_or_err = pcall(vim.system, cmd1, {
local ok, sysobj_or_err = pcall(vim.system, cmd, {
stdin = true,
stdout = stdout_handler,
stderr = stderr_handler,

View File

@ -139,30 +139,6 @@ describe('LSP', function()
end)
end)
end)
describe('lsp._cmd_parts test', function()
local function _cmd_parts(input)
return exec_lua(
[[
lsp = require('vim.lsp')
return lsp._cmd_parts(...)
]],
input
)
end
it('should valid cmd argument', function()
eq(true, pcall(_cmd_parts, { 'nvim' }))
eq(true, pcall(_cmd_parts, { 'nvim', '--head' }))
end)
it('should invalid cmd argument', function()
eq('.../lsp.lua:0: cmd: expected list, got nvim', pcall_err(_cmd_parts, 'nvim'))
eq(
'.../lsp.lua:0: cmd argument: expected string, got number',
pcall_err(_cmd_parts, { 'nvim', 1 })
)
end)
end)
end)
describe('LSP', function()