mirror of
https://github.com/neovim/neovim.git
synced 2026-05-02 12:04:58 +00:00
Merge #38560 refactor vim.lsp.rpc
This commit is contained in:
@@ -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()|:
|
||||
|
||||
@@ -1,74 +1,76 @@
|
||||
local uv = vim.uv
|
||||
local log = require('vim.lsp.log')
|
||||
|
||||
local is_win = vim.fn.has('win32') == 1
|
||||
|
||||
--- Checks whether a given path exists and is a directory.
|
||||
---@param filename string path to check
|
||||
---@return boolean
|
||||
local function is_dir(filename)
|
||||
local stat = uv.fs_stat(filename)
|
||||
return stat and stat.type == 'directory' or false
|
||||
end
|
||||
|
||||
--- Interface for transport implementations.
|
||||
---
|
||||
--- @class (private) vim.lsp.rpc.Transport
|
||||
--- @field listen fun(self: vim.lsp.rpc.Transport, on_read: fun(err: any, data: string), on_exit: fun(code: integer, signal: integer))
|
||||
--- @field write fun(self: vim.lsp.rpc.Transport, msg: string)
|
||||
--- @field is_closing fun(self: vim.lsp.rpc.Transport): boolean
|
||||
--- @field terminate fun(self: vim.lsp.rpc.Transport)
|
||||
|
||||
--- @class (private,exact) vim.lsp.rpc.Transport.Run : vim.lsp.rpc.Transport
|
||||
--- @field new fun(): vim.lsp.rpc.Transport.Run
|
||||
--- Transport backed by newly spawned process using `vim.system()`.
|
||||
---
|
||||
--- @class (private) vim.lsp.rpc.Transport.Run : vim.lsp.rpc.Transport
|
||||
--- @field cmd string[] Command to start the LSP server.
|
||||
--- @field extra_spawn_params? vim.lsp.rpc.ExtraSpawnParams
|
||||
--- @field sysobj? vim.SystemObj
|
||||
local TransportRun = {}
|
||||
|
||||
--- @return vim.lsp.rpc.Transport.Run
|
||||
function TransportRun.new()
|
||||
return setmetatable({}, { __index = TransportRun })
|
||||
end
|
||||
|
||||
--- @param cmd string[] Command to start the LSP server.
|
||||
--- @param extra_spawn_params? vim.lsp.rpc.ExtraSpawnParams
|
||||
--- @return vim.lsp.rpc.Transport.Run
|
||||
function TransportRun.new(cmd, extra_spawn_params)
|
||||
return setmetatable({
|
||||
cmd = cmd,
|
||||
extra_spawn_params = extra_spawn_params,
|
||||
}, { __index = TransportRun })
|
||||
end
|
||||
|
||||
--- @param on_read fun(err: any, data: string)
|
||||
--- @param on_exit fun(code: integer, signal: integer)
|
||||
function TransportRun:run(cmd, extra_spawn_params, on_read, on_exit)
|
||||
function TransportRun:listen(on_read, on_exit)
|
||||
local function on_stderr(_, chunk)
|
||||
if chunk then
|
||||
log.error('rpc', cmd[1], 'stderr', chunk)
|
||||
log.error('rpc', self.cmd[1], 'stderr', chunk)
|
||||
end
|
||||
end
|
||||
|
||||
extra_spawn_params = extra_spawn_params or {}
|
||||
self.extra_spawn_params = self.extra_spawn_params or {}
|
||||
|
||||
if extra_spawn_params.cwd then
|
||||
assert(is_dir(extra_spawn_params.cwd), 'cwd must be a directory')
|
||||
if self.extra_spawn_params.cwd then
|
||||
local stat = uv.fs_stat(self.extra_spawn_params.cwd)
|
||||
assert(stat and stat.type == 'directory' or false, 'cwd must be a directory')
|
||||
end
|
||||
|
||||
local detached = not is_win
|
||||
if extra_spawn_params.detached ~= nil then
|
||||
detached = extra_spawn_params.detached
|
||||
-- Default to non-detached on Windows.
|
||||
local detached = vim.fn.has('win32') ~= 1
|
||||
if self.extra_spawn_params.detached ~= nil then
|
||||
detached = self.extra_spawn_params.detached
|
||||
end
|
||||
|
||||
local ok, sysobj_or_err = pcall(vim.system, cmd, {
|
||||
---@type boolean, vim.SystemObj|string
|
||||
local ok, sysobj_or_err = pcall(vim.system, self.cmd, {
|
||||
stdin = true,
|
||||
stdout = on_read,
|
||||
stderr = on_stderr,
|
||||
cwd = extra_spawn_params.cwd,
|
||||
env = extra_spawn_params.env,
|
||||
cwd = self.extra_spawn_params.cwd,
|
||||
env = self.extra_spawn_params.env,
|
||||
detach = detached,
|
||||
}, function(obj)
|
||||
on_exit(obj.code, obj.signal)
|
||||
end)
|
||||
|
||||
if not ok then
|
||||
local err = sysobj_or_err --[[@as string]]
|
||||
if not ok then ---@cast sysobj_or_err string
|
||||
local err = sysobj_or_err
|
||||
local sfx = err:match('ENOENT')
|
||||
and '. The language server is either not installed, missing from PATH, or not executable.'
|
||||
or string.format(' with error message: %s', err)
|
||||
|
||||
error(('Spawning language server with cmd: `%s` failed%s'):format(vim.inspect(cmd), sfx))
|
||||
end
|
||||
error(('Spawning language server with cmd: `%s` failed%s'):format(vim.inspect(self.cmd), sfx))
|
||||
end ---@cast sysobj_or_err vim.SystemObj
|
||||
|
||||
self.sysobj = sysobj_or_err --[[@as vim.SystemObj]]
|
||||
self.sysobj = sysobj_or_err
|
||||
end
|
||||
|
||||
function TransportRun:write(msg)
|
||||
@@ -80,24 +82,35 @@ function TransportRun:is_closing()
|
||||
end
|
||||
|
||||
function TransportRun:terminate()
|
||||
assert(self.sysobj):kill(15)
|
||||
local sysobj = assert(self.sysobj)
|
||||
if sysobj:is_closing() then
|
||||
return
|
||||
end
|
||||
sysobj:kill(15)
|
||||
end
|
||||
|
||||
--- @class (private,exact) vim.lsp.rpc.Transport.Connect : vim.lsp.rpc.Transport
|
||||
--- @field new fun(): vim.lsp.rpc.Transport.Connect
|
||||
--- Transport backed by an existing `uv.uv_pipe_t` or `uv.uv_tcp_t` connection.
|
||||
---
|
||||
--- @class (private) vim.lsp.rpc.Transport.Connect : vim.lsp.rpc.Transport
|
||||
--- @field host_or_path string
|
||||
--- @field port? integer
|
||||
--- @field handle? uv.uv_pipe_t|uv.uv_tcp_t
|
||||
--- Connect returns a PublicClient synchronously so the caller
|
||||
--- can immediately send messages before the connection is established
|
||||
--- -> Need to buffer them until that happens
|
||||
--- can immediately send messages before the connection is established.
|
||||
--- These messages are buffered in `msgbuf`.
|
||||
--- @field connected boolean
|
||||
--- @field closing boolean
|
||||
--- @field msgbuf vim.Ringbuf
|
||||
--- @field on_exit? fun(code: integer, signal: integer)
|
||||
local TransportConnect = {}
|
||||
|
||||
--- @param host_or_path string
|
||||
--- @param port? integer
|
||||
--- @return vim.lsp.rpc.Transport.Connect
|
||||
function TransportConnect.new()
|
||||
function TransportConnect.new(host_or_path, port)
|
||||
return setmetatable({
|
||||
host_or_path = host_or_path,
|
||||
port = port,
|
||||
connected = false,
|
||||
-- size should be enough because the client can't really do anything until initialization is done
|
||||
-- which required a response from the server - implying the connection got established
|
||||
@@ -106,20 +119,18 @@ function TransportConnect.new()
|
||||
}, { __index = TransportConnect })
|
||||
end
|
||||
|
||||
--- @param host_or_path string
|
||||
--- @param port? integer
|
||||
--- @param on_read fun(err: any, data: string)
|
||||
--- @param on_exit? fun(code: integer, signal: integer)
|
||||
function TransportConnect:connect(host_or_path, port, on_read, on_exit)
|
||||
function TransportConnect:listen(on_read, on_exit)
|
||||
self.on_exit = on_exit
|
||||
self.handle = (
|
||||
port and assert(uv.new_tcp(), 'Could not create new TCP socket')
|
||||
self.port and assert(uv.new_tcp(), 'Could not create new TCP socket')
|
||||
or assert(uv.new_pipe(false), 'Pipe could not be opened.')
|
||||
)
|
||||
|
||||
local function on_connect(err)
|
||||
if err then
|
||||
local address = not port and host_or_path or (host_or_path .. ':' .. port)
|
||||
local address = not self.port and self.host_or_path or (self.host_or_path .. ':' .. self.port)
|
||||
vim.schedule(function()
|
||||
vim.notify(
|
||||
string.format('Could not connect to %s, reason: %s', address, vim.inspect(err)),
|
||||
@@ -135,15 +146,14 @@ function TransportConnect:connect(host_or_path, port, on_read, on_exit)
|
||||
end
|
||||
end
|
||||
|
||||
if not port then
|
||||
self.handle:connect(host_or_path, on_connect)
|
||||
if not self.port then
|
||||
self.handle:connect(self.host_or_path, on_connect)
|
||||
return
|
||||
end
|
||||
|
||||
--- @diagnostic disable-next-line:param-type-mismatch bad UV typing
|
||||
local info = uv.getaddrinfo(host_or_path, nil)
|
||||
local resolved_host = info and info[1] and info[1].addr or host_or_path
|
||||
self.handle:connect(resolved_host, port, on_connect)
|
||||
local info = uv.getaddrinfo(self.host_or_path, nil)
|
||||
local resolved_host = info and info[1] and info[1].addr or self.host_or_path
|
||||
self.handle:connect(resolved_host, self.port, on_connect)
|
||||
end
|
||||
|
||||
function TransportConnect:write(msg)
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -2,7 +2,7 @@ local log = require('vim.lsp.log')
|
||||
local protocol = require('vim.lsp.protocol')
|
||||
local lsp_transport = require('vim.lsp._transport')
|
||||
local strbuffer = require('vim._core.stringbuffer')
|
||||
local validate, schedule_wrap = vim.validate, vim.schedule_wrap
|
||||
local validate = vim.validate
|
||||
|
||||
--- Embeds the given string into a table and correctly computes `Content-Length`.
|
||||
---
|
||||
@@ -252,14 +252,96 @@ 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<integer, function> dict of message_id to callback
|
||||
---@field notify_reply_callbacks table<integer, function> 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<integer, function> dict of message_id to callback
|
||||
--- @field private notify_reply_callbacks table<integer, function> 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
|
||||
|
||||
---@cast result vim.lsp.rpc.Client
|
||||
local self = setmetatable(result, { __index = Client })
|
||||
--- @param body string
|
||||
local function handle_body(body)
|
||||
self:handle_body(body)
|
||||
end
|
||||
|
||||
local function on_exit()
|
||||
---@diagnostic disable-next-line: invisible
|
||||
self.transport:terminate()
|
||||
end
|
||||
|
||||
--- @param errkind vim.lsp.rpc.ClientErrors
|
||||
local function on_error(err, errkind)
|
||||
self:on_error(errkind, err)
|
||||
if errkind == M.client_errors.INVALID_SERVER_MESSAGE then
|
||||
---@diagnostic disable-next-line: invisible
|
||||
self.transport:terminate()
|
||||
end
|
||||
end
|
||||
|
||||
local on_read = M.create_read_loop(handle_body, on_exit, on_error)
|
||||
transport:listen(on_read, dispatchers.on_exit)
|
||||
return self
|
||||
end
|
||||
|
||||
---@private
|
||||
function Client:encode_and_send(payload)
|
||||
log.debug('rpc.send', payload)
|
||||
@@ -277,7 +359,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 +387,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
|
||||
@@ -321,47 +403,37 @@ function Client:request(method, params, callback, notify_reply_callback)
|
||||
return false
|
||||
end
|
||||
|
||||
self.message_callbacks[message_id] = schedule_wrap(callback)
|
||||
self.message_callbacks[message_id] = vim.schedule_wrap(callback)
|
||||
if notify_reply_callback then
|
||||
self.notify_reply_callbacks[message_id] = schedule_wrap(notify_reply_callback)
|
||||
self.notify_reply_callbacks[message_id] = vim.schedule_wrap(notify_reply_callback)
|
||||
end
|
||||
return result, message_id
|
||||
end
|
||||
|
||||
---@package
|
||||
---@param errkind vim.lsp.rpc.ClientErrors
|
||||
---@param ... any
|
||||
function Client:on_error(errkind, ...)
|
||||
---@param err any
|
||||
function Client:on_error(errkind, err)
|
||||
assert(M.client_errors[errkind])
|
||||
-- TODO what to do if this fails?
|
||||
pcall(self.dispatchers.on_error, errkind, ...)
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param errkind integer
|
||||
---@param status boolean
|
||||
---@param head any
|
||||
---@param ... any
|
||||
---@return boolean status
|
||||
---@return any head
|
||||
---@return any? ...
|
||||
function Client:pcall_handler(errkind, status, head, ...)
|
||||
if not status then
|
||||
self:on_error(errkind, head, ...)
|
||||
return status, head
|
||||
end
|
||||
return status, head, ...
|
||||
pcall(self.dispatchers.on_error, errkind, err)
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param errkind integer
|
||||
---@param fn function
|
||||
---@param ... any
|
||||
---@return boolean status
|
||||
---@return any head
|
||||
---@return any? ...
|
||||
---@return boolean success
|
||||
---@return any result
|
||||
---@return any ...
|
||||
function Client:try_call(errkind, fn, ...)
|
||||
return self:pcall_handler(errkind, pcall(fn, ...))
|
||||
local args = vim.F.pack_len(...)
|
||||
return xpcall(function()
|
||||
-- PUC Lua 5.1 xpcall() does not support forwarding extra arguments.
|
||||
return fn(vim.F.unpack_len(args))
|
||||
end, function(err)
|
||||
self:on_error(errkind, err)
|
||||
end)
|
||||
end
|
||||
|
||||
-- TODO periodically check message_callbacks for old requests past a certain
|
||||
@@ -375,25 +447,28 @@ function Client:handle_body(body)
|
||||
if not ok then
|
||||
self:on_error(M.client_errors.INVALID_SERVER_JSON, decoded)
|
||||
return
|
||||
elseif type(decoded) ~= 'table' then
|
||||
self:on_error(M.client_errors.INVALID_SERVER_MESSAGE, decoded)
|
||||
return
|
||||
end
|
||||
|
||||
log.debug('rpc.receive', decoded)
|
||||
|
||||
if type(decoded) ~= 'table' then
|
||||
self:on_error(M.client_errors.INVALID_SERVER_MESSAGE, decoded)
|
||||
elseif type(decoded.method) == 'string' and decoded.id then
|
||||
local err --- @type lsp.ResponseError?
|
||||
-- Received a request.
|
||||
if type(decoded.method) == 'string' and decoded.id then
|
||||
-- Schedule here so that the users functions don't trigger an error and
|
||||
-- we can still use the result.
|
||||
vim.schedule(coroutine.wrap(function()
|
||||
local status, result
|
||||
status, result, err = self:try_call(
|
||||
--- @type boolean, any, lsp.ResponseError?
|
||||
local success, result, err = self:try_call(
|
||||
M.client_errors.SERVER_REQUEST_HANDLER_ERROR,
|
||||
self.dispatchers.server_request,
|
||||
decoded.method,
|
||||
decoded.params
|
||||
)
|
||||
log.debug('server_request: callback result', { status = status, result = result, err = err })
|
||||
if status then
|
||||
log.debug('server_request: callback result', { status = success, result = result, err = err })
|
||||
-- Dispatcher returns without an exception.
|
||||
if success then
|
||||
if result == nil and err == nil then
|
||||
error(
|
||||
string.format(
|
||||
@@ -421,10 +496,12 @@ function Client:handle_body(body)
|
||||
end
|
||||
self:send_response(decoded.id, err, result)
|
||||
end))
|
||||
-- Proceed only if exactly one of 'result' or 'error' is present, as required by the LSP spec:
|
||||
-- - If 'error' is nil, then 'result' must be present.
|
||||
-- - If 'result' is nil, then 'error' must be present (and not vim.NIL).
|
||||
elseif
|
||||
-- Received a response to a request we sent.
|
||||
-- Proceed only if exactly one of 'result' or 'error' is present,
|
||||
-- as required by the JSON-RPC spec:
|
||||
-- * If 'error' is nil, then 'result' must be present.
|
||||
-- * If 'result' is nil, then 'error' must be present (and not vim.NIL).
|
||||
decoded.id
|
||||
and (
|
||||
(decoded.error == nil and decoded.result ~= nil)
|
||||
@@ -478,7 +555,7 @@ function Client:handle_body(body)
|
||||
log.error('No callback found for server response id ' .. result_id)
|
||||
end
|
||||
elseif type(decoded.method) == 'string' then
|
||||
-- Notification
|
||||
-- Received a notification.
|
||||
self:try_call(
|
||||
M.client_errors.NOTIFICATION_HANDLER_ERROR,
|
||||
self.dispatchers.notification,
|
||||
@@ -491,75 +568,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)
|
||||
@@ -575,11 +583,11 @@ local function merge_dispatchers(dispatchers)
|
||||
---@type vim.lsp.rpc.Dispatchers
|
||||
local merged = {
|
||||
notification = (
|
||||
dispatchers.notification and schedule_wrap(dispatchers.notification)
|
||||
dispatchers.notification and vim.schedule_wrap(dispatchers.notification)
|
||||
or default_dispatchers.notification
|
||||
),
|
||||
on_error = (
|
||||
dispatchers.on_error and schedule_wrap(dispatchers.on_error)
|
||||
dispatchers.on_error and vim.schedule_wrap(dispatchers.on_error)
|
||||
or default_dispatchers.on_error
|
||||
),
|
||||
on_exit = dispatchers.on_exit or default_dispatchers.on_exit,
|
||||
@@ -588,25 +596,6 @@ local function merge_dispatchers(dispatchers)
|
||||
return merged
|
||||
end
|
||||
|
||||
--- @param client vim.lsp.rpc.Client
|
||||
--- @param on_exit? fun()
|
||||
local function create_client_read_loop(client, on_exit)
|
||||
--- @param body string
|
||||
local function handle_body(body)
|
||||
client:handle_body(body)
|
||||
end
|
||||
|
||||
--- @param errkind vim.lsp.rpc.ClientErrors
|
||||
local function on_error(err, errkind)
|
||||
client:on_error(errkind, err)
|
||||
if errkind == M.client_errors.INVALID_SERVER_MESSAGE then
|
||||
client.transport:terminate()
|
||||
end
|
||||
end
|
||||
|
||||
return M.create_read_loop(handle_body, on_exit, on_error)
|
||||
end
|
||||
|
||||
--- Create a LSP RPC client factory that connects to either:
|
||||
---
|
||||
--- - a named pipe (windows)
|
||||
@@ -618,8 +607,10 @@ 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)
|
||||
log.info('Connecting RPC client', { host_or_path = host_or_path, port = port })
|
||||
|
||||
validate('host_or_path', host_or_path, 'string')
|
||||
validate('port', port, 'number', true)
|
||||
|
||||
@@ -628,14 +619,8 @@ function M.connect(host_or_path, port)
|
||||
|
||||
dispatchers = merge_dispatchers(dispatchers)
|
||||
|
||||
local transport = lsp_transport.TransportConnect.new()
|
||||
local client = new_client(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)
|
||||
local transport = lsp_transport.TransportConnect.new(host_or_path, port)
|
||||
return Client.new(dispatchers, transport)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -653,7 +638,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 })
|
||||
|
||||
@@ -662,12 +647,8 @@ 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 on_read = create_client_read_loop(client)
|
||||
transport:run(cmd, extra_spawn_params, on_read, dispatchers.on_exit)
|
||||
|
||||
return public_client(client)
|
||||
local transport = lsp_transport.TransportRun.new(cmd, extra_spawn_params)
|
||||
return Client.new(dispatchers, transport)
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
Reference in New Issue
Block a user