aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/lsp/client.lua
diff options
context:
space:
mode:
authorLewis Russell <lewis6991@gmail.com>2024-02-11 12:37:20 +0000
committerGitHub <noreply@github.com>2024-02-11 12:37:20 +0000
commited1b66bd998b98ee8cf76b5a23c323352588dd56 (patch)
treec262796dcab8a9dd22dbf7acd611366d38d43900 /runtime/lua/vim/lsp/client.lua
parent8e86193502608c4a833f6996b942e8dd0eb8e476 (diff)
downloadrneovim-ed1b66bd998b98ee8cf76b5a23c323352588dd56.tar.gz
rneovim-ed1b66bd998b98ee8cf76b5a23c323352588dd56.tar.bz2
rneovim-ed1b66bd998b98ee8cf76b5a23c323352588dd56.zip
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
Diffstat (limited to 'runtime/lua/vim/lsp/client.lua')
-rw-r--r--runtime/lua/vim/lsp/client.lua262
1 files changed, 240 insertions, 22 deletions
diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua
index 7bf83f4d2c..58db4387b6 100644
--- a/runtime/lua/vim/lsp/client.lua
+++ b/runtime/lua/vim/lsp/client.lua
@@ -4,6 +4,30 @@ local lsp = vim.lsp
local log = lsp.log
local ms = lsp.protocol.Methods
local changetracking = lsp._changetracking
+local validate = vim.validate
+
+--- @class lsp.ClientConfig
+--- @field cmd (string[]|fun(dispatchers: table):table)
+--- @field cmd_cwd string
+--- @field cmd_env (table)
+--- @field detached boolean
+--- @field workspace_folders (table)
+--- @field capabilities lsp.ClientCapabilities
+--- @field handlers table<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
+local client_index = 0
+
+--- Checks whether a given path is a directory.
+--- @param filename (string) path to check
+--- @return boolean # true if {filename} exists and is a directory, false otherwise
+local function is_dir(filename)
+ validate({ filename = { filename, 's' } })
+ local stat = uv.fs_stat(filename)
+ return stat and stat.type == 'directory' or false
+end
+
+local valid_encodings = {
+ ['utf-8'] = 'utf-8',
+ ['utf-16'] = 'utf-16',
+ ['utf-32'] = 'utf-32',
+ ['utf8'] = 'utf-8',
+ ['utf16'] = 'utf-16',
+ ['utf32'] = 'utf-32',
+ UTF8 = 'utf-8',
+ UTF16 = 'utf-16',
+ UTF32 = 'utf-32',
+}
+
+--- Normalizes {encoding} to valid LSP encoding names.
+--- @param encoding string? Encoding to normalize
+--- @return string # normalized encoding name
+local function validate_encoding(encoding)
+ validate({
+ encoding = { encoding, 's', true },
+ })
+ if not encoding then
+ return valid_encodings.UTF16
+ end
+ return valid_encodings[encoding:lower()]
+ or error(
+ string.format(
+ "Invalid offset encoding %q. Must be one of: 'utf-8', 'utf-16', 'utf-32'",
+ encoding
+ )
+ )
+end
+
+--- Augments a validator function with support for optional (nil) values.
+--- @param fn (fun(v): boolean) The original validator function; should return a
+--- bool.
+--- @return fun(v): boolean # The augmented function. Also returns true if {v} is
+--- `nil`.
+local function optional_validator(fn)
+ return function(v)
+ return v == nil or fn(v)
+ end
+end
+
+--- Validates a client configuration as given to |vim.lsp.start_client()|.
+--- @param config lsp.ClientConfig
+local function process_client_config(config)
+ validate({
+ config = { config, 't' },
+ })
+ validate({
+ handlers = { config.handlers, 't', true },
+ capabilities = { config.capabilities, 't', true },
+ cmd_cwd = { config.cmd_cwd, optional_validator(is_dir), 'directory' },
+ cmd_env = { config.cmd_env, 't', true },
+ detached = { config.detached, 'b', true },
+ name = { config.name, 's', true },
+ on_error = { config.on_error, 'f', true },
+ on_exit = { config.on_exit, 'f', true },
+ on_init = { config.on_init, 'f', true },
+ settings = { config.settings, 't', true },
+ commands = { config.commands, 't', true },
+ before_init = { config.before_init, 'f', true },
+ offset_encoding = { config.offset_encoding, 's', true },
+ flags = { config.flags, 't', true },
+ get_language_id = { config.get_language_id, 'f', true },
+ })
+ assert(
+ (
+ not config.flags
+ or not config.flags.debounce_text_changes
+ or type(config.flags.debounce_text_changes) == 'number'
+ ),
+ 'flags.debounce_text_changes must be a number with the debounce time in milliseconds'
+ )
+
+ if not config.name and type(config.cmd) == 'table' then
+ config.name = config.cmd[1] and vim.fs.basename(config.cmd[1]) or nil
+ end
+
+ config.offset_encoding = validate_encoding(config.offset_encoding)
+ config.flags = config.flags or {}
+ config.settings = config.settings or {}
+ config.handlers = config.handlers or {}
+
+ -- By default, get_language_id just returns the exact filetype it is passed.
+ -- It is possible to pass in something that will calculate a different filetype,
+ -- to be sent by the client.
+ config.get_language_id = config.get_language_id or function(_, filetype)
+ return filetype
+ end
+
+ config.capabilities = config.capabilities or lsp.protocol.make_client_capabilities()
+ config.commands = config.commands or {}
+end
+
--- @package
---- @param id integer
---- @param rpc vim.lsp.rpc.PublicClient
---- @param handlers table<string,lsp.Handler>
---- @param offset_encoding string
--- @param config lsp.ClientConfig
---- @return lsp.Client
-function Client.new(id, rpc, handlers, offset_encoding, config)
+--- @return lsp.Client?
+function Client.start(config)
+ process_client_config(config)
+
+ client_index = client_index + 1
+ local id = client_index
+
local name = config.name or tostring(id)
--- @class lsp.Client
local self = {
id = id,
config = config,
- handlers = handlers,
- rpc = rpc,
- offset_encoding = offset_encoding,
+ handlers = config.handlers,
+ offset_encoding = config.offset_encoding,
name = name,
_log_prefix = string.format('LSP[%s]', name),
requests = {},
- commands = config.commands or {},
attached_buffers = {},
server_capabilities = {},
dynamic_capabilities = vim.lsp._dynamic.new(id),
@@ -165,15 +292,46 @@ function Client.new(id, rpc, handlers, offset_encoding, config)
self.on_attach = method_wrapper(self, Client._on_attach)
self.supports_method = method_wrapper(self, Client._supports_method)
- ---@type table<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