From 4784f96e592836c489503d0049e926022ecac770 Mon Sep 17 00:00:00 2001 From: Yi Ming Date: Sun, 29 Mar 2026 20:37:13 +0800 Subject: [PATCH] refactor(lsp): merge `vim.lsp.rpc.Client` and `vim.lsp.rpc.PublicClient` --- runtime/doc/lsp.txt | 16 ++-- runtime/lua/vim/lsp.lua | 2 +- runtime/lua/vim/lsp/client.lua | 4 +- runtime/lua/vim/lsp/rpc.lua | 157 ++++++++++++++++----------------- 4 files changed, 85 insertions(+), 94 deletions(-) diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt index 89e2886d2f..d13e9a23e7 100644 --- a/runtime/doc/lsp.txt +++ b/runtime/doc/lsp.txt @@ -798,7 +798,7 @@ Lua module: vim.lsp *lsp-core* Fields: ~ - • {cmd}? (`string[]|fun(dispatchers: vim.lsp.rpc.Dispatchers, config: vim.lsp.ClientConfig): vim.lsp.rpc.PublicClient`) + • {cmd}? (`string[]|fun(dispatchers: vim.lsp.rpc.Dispatchers, config: vim.lsp.ClientConfig): vim.lsp.rpc.Client`) See `cmd` in |vim.lsp.ClientConfig|. See also `reuse_client` to dynamically decide (per-buffer) when `cmd` should be re-invoked. @@ -1684,9 +1684,9 @@ Lua module: vim.lsp.client *lsp-client* autocmds when replies are received from the server. • {root_dir} (`string?`) See |vim.lsp.ClientConfig|. - • {rpc} (`vim.lsp.rpc.PublicClient`) RPC client - object, for low level interaction with the - client. See |vim.lsp.rpc.start()|. + • {rpc} (`vim.lsp.rpc.Client`) RPC client object, for + low level interaction with the client. See + |vim.lsp.rpc.start()|. • {server_capabilities} (`lsp.ServerCapabilities?`) Response from the server sent on `initialize` describing the server's capabilities. @@ -1742,7 +1742,7 @@ Lua module: vim.lsp.client *lsp-client* • Note: To send an empty dictionary use |vim.empty_dict()|, else it will be encoded as an array. - • {cmd} (`string[]|fun(dispatchers: vim.lsp.rpc.Dispatchers, config: vim.lsp.ClientConfig): vim.lsp.rpc.PublicClient`) + • {cmd} (`string[]|fun(dispatchers: vim.lsp.rpc.Dispatchers, config: vim.lsp.ClientConfig): vim.lsp.rpc.Client`) Command `string[]` that launches the language server (treated as in |jobstart()|, must be absolute or on `$PATH`, shell constructs like @@ -2536,7 +2536,7 @@ enable({enable}, {filter}) *vim.lsp.on_type_formatting.enable()* ============================================================================== Lua module: vim.lsp.rpc *lsp-rpc* -*vim.lsp.rpc.PublicClient* +*vim.lsp.rpc.Client* Client RPC object Fields: ~ @@ -2564,7 +2564,7 @@ connect({host_or_path}, {port}) *vim.lsp.rpc.connect()* first argument must be a pipe Return: ~ - (`fun(dispatchers: vim.lsp.rpc.Dispatchers): vim.lsp.rpc.PublicClient`) + (`fun(dispatchers: vim.lsp.rpc.Dispatchers): vim.lsp.rpc.Client`) format_rpc_error({err}) *vim.lsp.rpc.format_rpc_error()* Constructs an error message from an LSP error object. @@ -2646,7 +2646,7 @@ start({cmd}, {dispatchers}, {extra_spawn_params}) *vim.lsp.rpc.start()* See |vim.system()| Return: ~ - (`vim.lsp.rpc.PublicClient`) See |vim.lsp.rpc.PublicClient|. + (`vim.lsp.rpc.Client`) See |vim.lsp.rpc.Client|. ============================================================================== diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 9b8ab699ad..fdf6e187c7 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -169,7 +169,7 @@ end --- --- See `cmd` in [vim.lsp.ClientConfig]. --- See also `reuse_client` to dynamically decide (per-buffer) when `cmd` should be re-invoked. ---- @field cmd? string[]|fun(dispatchers: vim.lsp.rpc.Dispatchers, config: vim.lsp.ClientConfig): vim.lsp.rpc.PublicClient +--- @field cmd? string[]|fun(dispatchers: vim.lsp.rpc.Dispatchers, config: vim.lsp.ClientConfig): vim.lsp.rpc.Client --- --- Filetypes the client will attach to, or `nil` for ALL filetypes. To match files by name, --- pattern, or contents, you can define a custom filetype using |vim.filetype.add()|: diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua index 3c9376d42b..781bce5ddb 100644 --- a/runtime/lua/vim/lsp/client.lua +++ b/runtime/lua/vim/lsp/client.lua @@ -45,7 +45,7 @@ local all_clients = {} --- 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, config: vim.lsp.ClientConfig): vim.lsp.rpc.PublicClient +--- @field cmd string[]|fun(dispatchers: vim.lsp.rpc.Dispatchers, config: vim.lsp.ClientConfig): vim.lsp.rpc.Client --- --- Directory to launch the `cmd` process. Not related to `root_dir`. --- (default: cwd) @@ -199,7 +199,7 @@ local all_clients = {} --- --- RPC client object, for low level interaction with the client. --- See |vim.lsp.rpc.start()|. ---- @field rpc vim.lsp.rpc.PublicClient +--- @field rpc vim.lsp.rpc.Client --- --- Response from the server sent on `initialize` describing the server's capabilities. --- @field server_capabilities lsp.ServerCapabilities? diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua index ed32fdabce..85613a264f 100644 --- a/runtime/lua/vim/lsp/rpc.lua +++ b/runtime/lua/vim/lsp/rpc.lua @@ -252,14 +252,73 @@ function M.create_read_loop(handle_body, on_exit, on_error) end end ----@class (private) vim.lsp.rpc.Client ----@field message_index integer ----@field message_callbacks table dict of message_id to callback ----@field notify_reply_callbacks table dict of message_id to callback ----@field transport vim.lsp.rpc.Transport ----@field dispatchers vim.lsp.rpc.Dispatchers +--- Client RPC object +--- @class vim.lsp.rpc.Client +--- @field private message_index integer +--- @field private message_callbacks table dict of message_id to callback +--- @field private notify_reply_callbacks table dict of message_id to callback +--- @field private transport vim.lsp.rpc.Transport +--- @field private dispatchers vim.lsp.rpc.Dispatchers +--- +--- See [vim.lsp.rpc.request()] +--- @field request fun(method: vim.lsp.protocol.Method.ClientToServer.Request, params: table?, callback: fun(err?: lsp.ResponseError, result: any, request_id: integer), notify_reply_callback?: fun(message_id: integer)):boolean,integer? +--- +--- See [vim.lsp.rpc.notify()] +--- @field notify fun(method: vim.lsp.protocol.Method.ClientToServer.Notification, params: any): boolean +--- +--- Indicates if the RPC is closing. +--- @field is_closing fun(): boolean +--- +--- Terminates the RPC client. +--- @field terminate fun() local Client = {} +---@package +---@param dispatchers vim.lsp.rpc.Dispatchers +---@param transport vim.lsp.rpc.Transport +---@return vim.lsp.rpc.Client +function Client.new(dispatchers, transport) + local result = { + message_index = 0, + message_callbacks = {}, + notify_reply_callbacks = {}, + transport = transport, + dispatchers = dispatchers, + } + + ---@private + function result.is_closing() + return result.transport:is_closing() + end + + ---@private + function result.terminate() + result.transport:terminate() + end + + --- Sends a request to the LSP server and runs {callback} upon response. + --- + ---@param method (vim.lsp.protocol.Method.ClientToServer.Request) The invoked LSP method + ---@param params (table?) Parameters for the invoked LSP method + ---@param callback fun(err: lsp.ResponseError?, result: any) Callback to invoke + ---@param notify_reply_callback? fun(message_id: integer) Callback to invoke as soon as a request is no longer pending + ---@return boolean success `true` if request could be sent, `false` if not + ---@return integer? message_id if request could be sent, `nil` if not + function result.request(method, params, callback, notify_reply_callback) + return result:_request(method, params, callback, notify_reply_callback) + end + + --- Sends a notification to the LSP server. + ---@param method (vim.lsp.protocol.Method.ClientToServer.Notification) The invoked LSP method + ---@param params (table?) Parameters for the invoked LSP method + ---@return boolean `true` if notification could be sent, `false` if not + function result.notify(method, params) + return result:_notify(method, params) + end + + return setmetatable(result, { __index = Client }) +end + ---@private function Client:encode_and_send(payload) log.debug('rpc.send', payload) @@ -277,7 +336,7 @@ end ---@param method vim.lsp.protocol.Method The invoked LSP method ---@param params any Parameters for the invoked LSP method ---@return boolean `true` if notification could be sent, `false` if not -function Client:notify(method, params) +function Client:_notify(method, params) return self:encode_and_send({ jsonrpc = '2.0', method = method, @@ -305,7 +364,7 @@ end ---@param notify_reply_callback? fun(message_id: integer) Callback to invoke as soon as a request is no longer pending ---@return boolean success `true` if request could be sent, `false` if not ---@return integer? message_id if request could be sent, `nil` if not -function Client:request(method, params, callback, notify_reply_callback) +function Client:_request(method, params, callback, notify_reply_callback) validate('callback', callback, 'function') validate('notify_reply_callback', notify_reply_callback, 'function', true) self.message_index = self.message_index + 1 @@ -491,75 +550,6 @@ function Client:handle_body(body) end end ----@param dispatchers vim.lsp.rpc.Dispatchers ----@param transport vim.lsp.rpc.Transport ----@return vim.lsp.rpc.Client -local function new_client(dispatchers, transport) - local state = { - message_index = 0, - message_callbacks = {}, - notify_reply_callbacks = {}, - transport = transport, - dispatchers = dispatchers, - } - return setmetatable(state, { __index = Client }) -end - ---- Client RPC object ---- @class vim.lsp.rpc.PublicClient ---- ---- See [vim.lsp.rpc.request()] ---- @field request fun(method: vim.lsp.protocol.Method.ClientToServer.Request, params: table?, callback: fun(err?: lsp.ResponseError, result: any, request_id: integer), notify_reply_callback?: fun(message_id: integer)):boolean,integer? ---- ---- See [vim.lsp.rpc.notify()] ---- @field notify fun(method: vim.lsp.protocol.Method.ClientToServer.Notification, params: any): boolean ---- ---- Indicates if the RPC is closing. ---- @field is_closing fun(): boolean ---- ---- Terminates the RPC client. ---- @field terminate fun() - ----@param client vim.lsp.rpc.Client ----@return vim.lsp.rpc.PublicClient -local function public_client(client) - ---@type vim.lsp.rpc.PublicClient - ---@diagnostic disable-next-line: missing-fields - local result = {} - - ---@private - function result.is_closing() - return client.transport:is_closing() - end - - ---@private - function result.terminate() - client.transport:terminate() - end - - --- Sends a request to the LSP server and runs {callback} upon response. - --- - ---@param method (vim.lsp.protocol.Method.ClientToServer.Request) The invoked LSP method - ---@param params (table?) Parameters for the invoked LSP method - ---@param callback fun(err: lsp.ResponseError?, result: any) Callback to invoke - ---@param notify_reply_callback? fun(message_id: integer) Callback to invoke as soon as a request is no longer pending - ---@return boolean success `true` if request could be sent, `false` if not - ---@return integer? message_id if request could be sent, `nil` if not - function result.request(method, params, callback, notify_reply_callback) - return client:request(method, params, callback, notify_reply_callback) - end - - --- Sends a notification to the LSP server. - ---@param method (vim.lsp.protocol.Method.ClientToServer.Notification) The invoked LSP method - ---@param params (table?) Parameters for the invoked LSP method - ---@return boolean `true` if notification could be sent, `false` if not - function result.notify(method, params) - return client:notify(method, params) - end - - return result -end - ---@param dispatchers vim.lsp.rpc.Dispatchers? ---@return vim.lsp.rpc.Dispatchers local function merge_dispatchers(dispatchers) @@ -600,6 +590,7 @@ local function create_client_read_loop(client, on_exit) local function on_error(err, errkind) client:on_error(errkind, err) if errkind == M.client_errors.INVALID_SERVER_MESSAGE then + ---@diagnostic disable-next-line: invisible client.transport:terminate() end end @@ -618,7 +609,7 @@ end --- ---@param host_or_path string host to connect to or path to a pipe/domain socket ---@param port integer? TCP port to connect to. If absent the first argument must be a pipe ----@return fun(dispatchers: vim.lsp.rpc.Dispatchers): vim.lsp.rpc.PublicClient +---@return fun(dispatchers: vim.lsp.rpc.Dispatchers): vim.lsp.rpc.Client function M.connect(host_or_path, port) validate('host_or_path', host_or_path, 'string') validate('port', port, 'number', true) @@ -629,13 +620,13 @@ function M.connect(host_or_path, port) dispatchers = merge_dispatchers(dispatchers) local transport = lsp_transport.TransportConnect.new() - local client = new_client(dispatchers, transport) + local client = Client.new(dispatchers, transport) local on_read = create_client_read_loop(client, function() transport:terminate() end) transport:connect(host_or_path, port, on_read, dispatchers.on_exit) - return public_client(client) + return client end end @@ -653,7 +644,7 @@ end --- @param cmd string[] Command to start the LSP server. --- @param dispatchers? vim.lsp.rpc.Dispatchers --- @param extra_spawn_params? vim.lsp.rpc.ExtraSpawnParams ---- @return vim.lsp.rpc.PublicClient +--- @return vim.lsp.rpc.Client function M.start(cmd, dispatchers, extra_spawn_params) log.info('Starting RPC client', { cmd = cmd, extra = extra_spawn_params }) @@ -663,11 +654,11 @@ function M.start(cmd, dispatchers, extra_spawn_params) dispatchers = merge_dispatchers(dispatchers) local transport = lsp_transport.TransportRun.new() - local client = new_client(dispatchers, transport) + local client = Client.new(dispatchers, transport) local on_read = create_client_read_loop(client) transport:run(cmd, extra_spawn_params, on_read, dispatchers.on_exit) - return public_client(client) + return client end return M