neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

rpc.lua (22510B)


      1 local log = require('vim.lsp.log')
      2 local protocol = require('vim.lsp.protocol')
      3 local lsp_transport = require('vim.lsp._transport')
      4 local strbuffer = require('vim._core.stringbuffer')
      5 local validate, schedule_wrap = vim.validate, vim.schedule_wrap
      6 
      7 --- Embeds the given string into a table and correctly computes `Content-Length`.
      8 ---
      9 --- @param message string
     10 --- @return string message with `Content-Length` attribute
     11 local function format_message_with_content_length(message)
     12  return table.concat({
     13    'Content-Length: ',
     14    tostring(#message),
     15    '\r\n\r\n',
     16    message,
     17  })
     18 end
     19 
     20 --- Extract `content-length` from the header.
     21 ---
     22 --- The structure of header fields conforms to [HTTP semantics](https://tools.ietf.org/html/rfc7230#section-3.2),
     23 --- i.e., `header-field = field-name : OWS field-value OWS`. OWS means optional whitespace (space/horizontal tabs).
     24 ---
     25 --- We ignore lines ending with `\n` that don't contain `content-length`, since some servers
     26 --- write log to standard output and there's no way to avoid it.
     27 --- See https://github.com/neovim/neovim/pull/35743#pullrequestreview-3379705828
     28 --- @param header string The header to parse
     29 --- @return integer
     30 local function get_content_length(header)
     31  local state = 'name'
     32  local i, len = 1, #header
     33  local j, name = 1, 'content-length'
     34  local buf = strbuffer.new()
     35  local digit = true
     36  while i <= len do
     37    local c = header:byte(i)
     38    if state == 'name' then
     39      if c >= 65 and c <= 90 then -- lower case
     40        c = c + 32
     41      end
     42      if (c == 32 or c == 9) and j == 1 then -- luacheck: ignore 542
     43        -- skip OWS for compatibility only
     44      elseif c == name:byte(j) then
     45        j = j + 1
     46      elseif c == 58 and j == 15 then
     47        state = 'colon'
     48      else
     49        state = 'invalid'
     50      end
     51    elseif state == 'colon' then
     52      if c ~= 32 and c ~= 9 then -- skip OWS normally
     53        state = 'value'
     54        i = i - 1
     55      end
     56    elseif state == 'value' then
     57      if c == 13 and header:byte(i + 1) == 10 then -- must end with \r\n
     58        local value = buf:get()
     59        return assert(digit and tonumber(value), 'value of Content-Length is not number: ' .. value)
     60      else
     61        buf:put(string.char(c))
     62      end
     63      if c < 48 and c ~= 32 and c ~= 9 or c > 57 then
     64        digit = false
     65      end
     66    elseif state == 'invalid' then
     67      if c == 10 then -- reset for next line
     68        state, j = 'name', 1
     69      end
     70    end
     71    i = i + 1
     72  end
     73  error('Content-Length not found in header: ' .. header)
     74 end
     75 
     76 local M = {}
     77 
     78 --- Mapping of error codes used by the client
     79 --- @enum vim.lsp.rpc.ClientErrors
     80 local client_errors = {
     81  INVALID_SERVER_MESSAGE = 1,
     82  INVALID_SERVER_JSON = 2,
     83  NO_RESULT_CALLBACK_FOUND = 3,
     84  READ_ERROR = 4,
     85  NOTIFICATION_HANDLER_ERROR = 5,
     86  SERVER_REQUEST_HANDLER_ERROR = 6,
     87  SERVER_RESULT_CALLBACK_ERROR = 7,
     88 }
     89 
     90 --- @type table<string,integer> | table<integer,string>
     91 --- @nodoc
     92 M.client_errors = vim.deepcopy(client_errors)
     93 for k, v in pairs(client_errors) do
     94  M.client_errors[v] = k
     95 end
     96 
     97 --- Constructs an error message from an LSP error object.
     98 ---
     99 ---@param err table The error object
    100 ---@return string error_message The formatted error message
    101 function M.format_rpc_error(err)
    102  validate('err', err, 'table')
    103 
    104  -- There is ErrorCodes in the LSP specification,
    105  -- but in ResponseError.code it is not used and the actual type is number.
    106  local code --- @type string
    107  if protocol.ErrorCodes[err.code] then
    108    code = string.format('code_name = %s,', protocol.ErrorCodes[err.code])
    109  else
    110    code = string.format('code_name = unknown, code = %s,', err.code)
    111  end
    112 
    113  local message_parts = { 'RPC[Error]', code }
    114  if err.message then
    115    table.insert(message_parts, 'message =')
    116    table.insert(message_parts, string.format('%q', err.message))
    117  end
    118  if err.data then
    119    table.insert(message_parts, 'data =')
    120    table.insert(message_parts, vim.inspect(err.data))
    121  end
    122  return table.concat(message_parts, ' ')
    123 end
    124 
    125 --- Creates an RPC response table `error` to be sent to the LSP response.
    126 ---
    127 ---@param code integer RPC error code defined, see `vim.lsp.protocol.ErrorCodes`
    128 ---@param message? string arbitrary message to send to server
    129 ---@param data? any arbitrary data to send to server
    130 ---
    131 ---@see lsp.ErrorCodes See `vim.lsp.protocol.ErrorCodes`
    132 ---@return lsp.ResponseError
    133 function M.rpc_response_error(code, message, data)
    134  -- TODO should this error or just pick a sane error (like InternalError)?
    135  ---@type string
    136  local code_name = assert(protocol.ErrorCodes[code], 'Invalid RPC error code')
    137  return setmetatable({
    138    code = code,
    139    message = message or code_name,
    140    data = data,
    141  }, {
    142    __tostring = M.format_rpc_error,
    143  })
    144 end
    145 
    146 --- Dispatchers for LSP message types.
    147 --- @class vim.lsp.rpc.Dispatchers
    148 --- @inlinedoc
    149 --- @field notification fun(method: vim.lsp.protocol.Method.ClientToServer.Notification, params: table)
    150 --- @field server_request fun(method: vim.lsp.protocol.Method.ClientToServer.Request, params: table): any?, lsp.ResponseError?
    151 --- @field on_exit fun(code: integer, signal: integer)
    152 --- @field on_error fun(code: integer, err: any)
    153 
    154 --- @type vim.lsp.rpc.Dispatchers
    155 local default_dispatchers = {
    156  --- Default dispatcher for notifications sent to an LSP server.
    157  ---
    158  ---@param method vim.lsp.protocol.Method The invoked LSP method
    159  ---@param params table Parameters for the invoked LSP method
    160  notification = function(method, params)
    161    log.debug('notification', method, params)
    162  end,
    163 
    164  --- Default dispatcher for requests sent to an LSP server.
    165  ---
    166  ---@param method vim.lsp.protocol.Method The invoked LSP method
    167  ---@param params table Parameters for the invoked LSP method
    168  ---@return any result (always nil for the default dispatchers)
    169  ---@return lsp.ResponseError error `vim.lsp.protocol.ErrorCodes.MethodNotFound`
    170  server_request = function(method, params)
    171    log.debug('server_request', method, params)
    172    return nil, M.rpc_response_error(protocol.ErrorCodes.MethodNotFound)
    173  end,
    174 
    175  --- Default dispatcher for when a client exits.
    176  ---
    177  ---@param code integer Exit code
    178  ---@param signal integer Number describing the signal used to terminate (if any)
    179  on_exit = function(code, signal)
    180    log.info('client_exit', { code = code, signal = signal })
    181  end,
    182 
    183  --- Default dispatcher for client errors.
    184  ---
    185  ---@param code integer Error code
    186  ---@param err any Details about the error
    187  on_error = function(code, err)
    188    log.error('client_error:', M.client_errors[code], err)
    189  end,
    190 }
    191 
    192 --- @async
    193 local function request_parser_loop()
    194  local buf = strbuffer.new()
    195  while true do
    196    local msg = buf:tostring()
    197    local header_end = msg:find('\r\n\r\n', 1, true)
    198    if header_end then
    199      local header = buf:get(header_end + 1)
    200      buf:skip(2) -- skip past header boundary
    201      local content_length = get_content_length(header)
    202      while strbuffer.len(buf) < content_length do
    203        buf:put(coroutine.yield())
    204      end
    205      local body = buf:get(content_length)
    206      buf:put(coroutine.yield(body))
    207    else
    208      buf:put(coroutine.yield())
    209    end
    210  end
    211 end
    212 
    213 --- @private
    214 --- @param handle_body fun(body: string)
    215 --- @param on_exit? fun()
    216 --- @param on_error? fun(err: any, errkind: vim.lsp.rpc.ClientErrors)
    217 function M.create_read_loop(handle_body, on_exit, on_error)
    218  on_exit = on_exit or function() end
    219  on_error = on_error or function() end
    220  local co = coroutine.create(request_parser_loop)
    221  coroutine.resume(co)
    222  return function(err, chunk)
    223    if err then
    224      on_error(err, M.client_errors.READ_ERROR)
    225      return
    226    end
    227 
    228    if not chunk then
    229      on_exit()
    230      return
    231    end
    232 
    233    if coroutine.status(co) == 'dead' then
    234      return
    235    end
    236 
    237    while true do
    238      local ok, res = coroutine.resume(co, chunk)
    239      if not ok then
    240        on_error(res, M.client_errors.INVALID_SERVER_MESSAGE)
    241        break
    242      elseif res then
    243        handle_body(res)
    244        chunk = ''
    245      else
    246        break
    247      end
    248    end
    249  end
    250 end
    251 
    252 ---@class (private) vim.lsp.rpc.Client
    253 ---@field message_index integer
    254 ---@field message_callbacks table<integer, function> dict of message_id to callback
    255 ---@field notify_reply_callbacks table<integer, function> dict of message_id to callback
    256 ---@field transport vim.lsp.rpc.Transport
    257 ---@field dispatchers vim.lsp.rpc.Dispatchers
    258 local Client = {}
    259 
    260 ---@private
    261 function Client:encode_and_send(payload)
    262  log.debug('rpc.send', payload)
    263  if self.transport:is_closing() then
    264    return false
    265  end
    266  local jsonstr = vim.json.encode(payload)
    267 
    268  self.transport:write(format_message_with_content_length(jsonstr))
    269  return true
    270 end
    271 
    272 ---@package
    273 --- Sends a notification to the LSP server.
    274 ---@param method vim.lsp.protocol.Method The invoked LSP method
    275 ---@param params any Parameters for the invoked LSP method
    276 ---@return boolean `true` if notification could be sent, `false` if not
    277 function Client:notify(method, params)
    278  return self:encode_and_send({
    279    jsonrpc = '2.0',
    280    method = method,
    281    params = params,
    282  })
    283 end
    284 
    285 ---@private
    286 --- sends an error object to the remote LSP process.
    287 function Client:send_response(request_id, err, result)
    288  return self:encode_and_send({
    289    id = request_id,
    290    jsonrpc = '2.0',
    291    error = err,
    292    result = result,
    293  })
    294 end
    295 
    296 ---@package
    297 --- Sends a request to the LSP server and runs {callback} upon response. |vim.lsp.rpc.request()|
    298 ---
    299 ---@param method vim.lsp.protocol.Method The invoked LSP method
    300 ---@param params table? Parameters for the invoked LSP method
    301 ---@param callback fun(err?: lsp.ResponseError, result: any, message_id: integer) Callback to invoke
    302 ---@param notify_reply_callback? fun(message_id: integer) Callback to invoke as soon as a request is no longer pending
    303 ---@return boolean success `true` if request could be sent, `false` if not
    304 ---@return integer? message_id if request could be sent, `nil` if not
    305 function Client:request(method, params, callback, notify_reply_callback)
    306  validate('callback', callback, 'function')
    307  validate('notify_reply_callback', notify_reply_callback, 'function', true)
    308  self.message_index = self.message_index + 1
    309  local message_id = self.message_index
    310  local result = self:encode_and_send({
    311    id = message_id,
    312    jsonrpc = '2.0',
    313    method = method,
    314    params = params,
    315  })
    316 
    317  if not result then
    318    return false
    319  end
    320 
    321  self.message_callbacks[message_id] = schedule_wrap(callback)
    322  if notify_reply_callback then
    323    self.notify_reply_callbacks[message_id] = schedule_wrap(notify_reply_callback)
    324  end
    325  return result, message_id
    326 end
    327 
    328 ---@package
    329 ---@param errkind vim.lsp.rpc.ClientErrors
    330 ---@param ... any
    331 function Client:on_error(errkind, ...)
    332  assert(M.client_errors[errkind])
    333  -- TODO what to do if this fails?
    334  pcall(self.dispatchers.on_error, errkind, ...)
    335 end
    336 
    337 ---@private
    338 ---@param errkind integer
    339 ---@param status boolean
    340 ---@param head any
    341 ---@param ... any
    342 ---@return boolean status
    343 ---@return any head
    344 ---@return any? ...
    345 function Client:pcall_handler(errkind, status, head, ...)
    346  if not status then
    347    self:on_error(errkind, head, ...)
    348    return status, head
    349  end
    350  return status, head, ...
    351 end
    352 
    353 ---@private
    354 ---@param errkind integer
    355 ---@param fn function
    356 ---@param ... any
    357 ---@return boolean status
    358 ---@return any head
    359 ---@return any? ...
    360 function Client:try_call(errkind, fn, ...)
    361  return self:pcall_handler(errkind, pcall(fn, ...))
    362 end
    363 
    364 -- TODO periodically check message_callbacks for old requests past a certain
    365 -- time and log them. This would require storing the timestamp. I could call
    366 -- them with an error then, perhaps.
    367 
    368 --- @package
    369 --- @param body string
    370 function Client:handle_body(body)
    371  local ok, decoded = pcall(vim.json.decode, body)
    372  if not ok then
    373    self:on_error(M.client_errors.INVALID_SERVER_JSON, decoded)
    374    return
    375  end
    376  log.debug('rpc.receive', decoded)
    377 
    378  if type(decoded) ~= 'table' then
    379    self:on_error(M.client_errors.INVALID_SERVER_MESSAGE, decoded)
    380  elseif type(decoded.method) == 'string' and decoded.id then
    381    local err --- @type lsp.ResponseError?
    382    -- Schedule here so that the users functions don't trigger an error and
    383    -- we can still use the result.
    384    vim.schedule(coroutine.wrap(function()
    385      local status, result
    386      status, result, err = self:try_call(
    387        M.client_errors.SERVER_REQUEST_HANDLER_ERROR,
    388        self.dispatchers.server_request,
    389        decoded.method,
    390        decoded.params
    391      )
    392      log.debug('server_request: callback result', { status = status, result = result, err = err })
    393      if status then
    394        if result == nil and err == nil then
    395          error(
    396            string.format(
    397              'method %q: either a result or an error must be sent to the server in response',
    398              decoded.method
    399            )
    400          )
    401        end
    402        if err then
    403          assert(
    404            type(err) == 'table',
    405            'err must be a table. Use rpc_response_error to help format errors.'
    406          )
    407          ---@type string
    408          local code_name = assert(
    409            protocol.ErrorCodes[err.code],
    410            'Errors must use protocol.ErrorCodes. Use rpc_response_error to help format errors.'
    411          )
    412          err.message = err.message or code_name
    413        end
    414      else
    415        -- On an exception, result will contain the error message.
    416        err = M.rpc_response_error(protocol.ErrorCodes.InternalError, result)
    417        result = nil
    418      end
    419      self:send_response(decoded.id, err, result)
    420    end))
    421  -- Proceed only if exactly one of 'result' or 'error' is present, as required by the LSP spec:
    422  -- - If 'error' is nil, then 'result' must be present.
    423  -- - If 'result' is nil, then 'error' must be present (and not vim.NIL).
    424  elseif
    425    decoded.id
    426    and (
    427      (decoded.error == nil and decoded.result ~= nil)
    428      or (decoded.result == nil and decoded.error ~= nil and decoded.error ~= vim.NIL)
    429    )
    430  then
    431    -- We sent a number, so we expect a number.
    432    local result_id = assert(tonumber(decoded.id), 'response id must be a number') --[[@as integer]]
    433 
    434    -- Notify the user that a response was received for the request
    435    local notify_reply_callback = self.notify_reply_callbacks[result_id]
    436    if notify_reply_callback then
    437      validate('notify_reply_callback', notify_reply_callback, 'function')
    438      notify_reply_callback(result_id)
    439      self.notify_reply_callbacks[result_id] = nil
    440    end
    441 
    442    -- Do not surface RequestCancelled to users, it is RPC-internal.
    443    if decoded.error then
    444      assert(type(decoded.error) == 'table')
    445      if decoded.error.code == protocol.ErrorCodes.RequestCancelled then
    446        log.debug('Received cancellation ack', decoded)
    447        -- Clear any callback since this is cancelled now.
    448        -- This is safe to do assuming that these conditions hold:
    449        -- - The server will not send a result callback after this cancellation.
    450        -- - If the server sent this cancellation ACK after sending the result, the user of this RPC
    451        -- client will ignore the result themselves.
    452        if result_id then
    453          self.message_callbacks[result_id] = nil
    454        end
    455        return
    456      end
    457    end
    458 
    459    local callback = self.message_callbacks[result_id]
    460    if callback then
    461      self.message_callbacks[result_id] = nil
    462      validate('callback', callback, 'function')
    463      if decoded.error then
    464        setmetatable(decoded.error, { __tostring = M.format_rpc_error })
    465      end
    466      self:try_call(
    467        M.client_errors.SERVER_RESULT_CALLBACK_ERROR,
    468        callback,
    469        decoded.error,
    470        decoded.result ~= vim.NIL and decoded.result or nil,
    471        result_id
    472      )
    473    else
    474      self:on_error(M.client_errors.NO_RESULT_CALLBACK_FOUND, decoded)
    475      log.error('No callback found for server response id ' .. result_id)
    476    end
    477  elseif type(decoded.method) == 'string' then
    478    -- Notification
    479    self:try_call(
    480      M.client_errors.NOTIFICATION_HANDLER_ERROR,
    481      self.dispatchers.notification,
    482      decoded.method,
    483      decoded.params
    484    )
    485  else
    486    -- Invalid server message
    487    self:on_error(M.client_errors.INVALID_SERVER_MESSAGE, decoded)
    488  end
    489 end
    490 
    491 ---@param dispatchers vim.lsp.rpc.Dispatchers
    492 ---@param transport vim.lsp.rpc.Transport
    493 ---@return vim.lsp.rpc.Client
    494 local function new_client(dispatchers, transport)
    495  local state = {
    496    message_index = 0,
    497    message_callbacks = {},
    498    notify_reply_callbacks = {},
    499    transport = transport,
    500    dispatchers = dispatchers,
    501  }
    502  return setmetatable(state, { __index = Client })
    503 end
    504 
    505 --- Client RPC object
    506 --- @class vim.lsp.rpc.PublicClient
    507 ---
    508 --- See [vim.lsp.rpc.request()]
    509 --- @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?
    510 ---
    511 --- See [vim.lsp.rpc.notify()]
    512 --- @field notify fun(method: vim.lsp.protocol.Method.ClientToServer.Notification, params: any): boolean
    513 ---
    514 --- Indicates if the RPC is closing.
    515 --- @field is_closing fun(): boolean
    516 ---
    517 --- Terminates the RPC client.
    518 --- @field terminate fun()
    519 
    520 ---@param client vim.lsp.rpc.Client
    521 ---@return vim.lsp.rpc.PublicClient
    522 local function public_client(client)
    523  ---@type vim.lsp.rpc.PublicClient
    524  ---@diagnostic disable-next-line: missing-fields
    525  local result = {}
    526 
    527  ---@private
    528  function result.is_closing()
    529    return client.transport:is_closing()
    530  end
    531 
    532  ---@private
    533  function result.terminate()
    534    client.transport:terminate()
    535  end
    536 
    537  --- Sends a request to the LSP server and runs {callback} upon response.
    538  ---
    539  ---@param method (vim.lsp.protocol.Method.ClientToServer.Request) The invoked LSP method
    540  ---@param params (table?) Parameters for the invoked LSP method
    541  ---@param callback fun(err: lsp.ResponseError?, result: any) Callback to invoke
    542  ---@param notify_reply_callback? fun(message_id: integer) Callback to invoke as soon as a request is no longer pending
    543  ---@return boolean success `true` if request could be sent, `false` if not
    544  ---@return integer? message_id if request could be sent, `nil` if not
    545  function result.request(method, params, callback, notify_reply_callback)
    546    return client:request(method, params, callback, notify_reply_callback)
    547  end
    548 
    549  --- Sends a notification to the LSP server.
    550  ---@param method (vim.lsp.protocol.Method.ClientToServer.Notification) The invoked LSP method
    551  ---@param params (table?) Parameters for the invoked LSP method
    552  ---@return boolean `true` if notification could be sent, `false` if not
    553  function result.notify(method, params)
    554    return client:notify(method, params)
    555  end
    556 
    557  return result
    558 end
    559 
    560 ---@param dispatchers vim.lsp.rpc.Dispatchers?
    561 ---@return vim.lsp.rpc.Dispatchers
    562 local function merge_dispatchers(dispatchers)
    563  if not dispatchers then
    564    return default_dispatchers
    565  end
    566  ---@diagnostic disable-next-line: no-unknown
    567  for name, fn in pairs(dispatchers) do
    568    if type(fn) ~= 'function' then
    569      error(string.format('dispatcher.%s must be a function', name))
    570    end
    571  end
    572  ---@type vim.lsp.rpc.Dispatchers
    573  local merged = {
    574    notification = (
    575      dispatchers.notification and schedule_wrap(dispatchers.notification)
    576      or default_dispatchers.notification
    577    ),
    578    on_error = (
    579      dispatchers.on_error and schedule_wrap(dispatchers.on_error)
    580      or default_dispatchers.on_error
    581    ),
    582    on_exit = dispatchers.on_exit or default_dispatchers.on_exit,
    583    server_request = dispatchers.server_request or default_dispatchers.server_request,
    584  }
    585  return merged
    586 end
    587 
    588 --- @param client vim.lsp.rpc.Client
    589 --- @param on_exit? fun()
    590 local function create_client_read_loop(client, on_exit)
    591  --- @param body string
    592  local function handle_body(body)
    593    client:handle_body(body)
    594  end
    595 
    596  --- @param errkind vim.lsp.rpc.ClientErrors
    597  local function on_error(err, errkind)
    598    client:on_error(errkind, err)
    599    if errkind == M.client_errors.INVALID_SERVER_MESSAGE then
    600      client.transport:terminate()
    601    end
    602  end
    603 
    604  return M.create_read_loop(handle_body, on_exit, on_error)
    605 end
    606 
    607 --- Create a LSP RPC client factory that connects to either:
    608 ---
    609 ---  - a named pipe (windows)
    610 ---  - a domain socket (unix)
    611 ---  - a host and port via TCP
    612 ---
    613 --- Return a function that can be passed to the `cmd` field for
    614 --- |vim.lsp.start()|.
    615 ---
    616 ---@param host_or_path string host to connect to or path to a pipe/domain socket
    617 ---@param port integer? TCP port to connect to. If absent the first argument must be a pipe
    618 ---@return fun(dispatchers: vim.lsp.rpc.Dispatchers): vim.lsp.rpc.PublicClient
    619 function M.connect(host_or_path, port)
    620  validate('host_or_path', host_or_path, 'string')
    621  validate('port', port, 'number', true)
    622 
    623  return function(dispatchers)
    624    validate('dispatchers', dispatchers, 'table', true)
    625 
    626    dispatchers = merge_dispatchers(dispatchers)
    627 
    628    local transport = lsp_transport.TransportConnect.new()
    629    local client = new_client(dispatchers, transport)
    630    local on_read = create_client_read_loop(client, function()
    631      transport:terminate()
    632    end)
    633    transport:connect(host_or_path, port, on_read, dispatchers.on_exit)
    634 
    635    return public_client(client)
    636  end
    637 end
    638 
    639 --- Additional context for the LSP server process.
    640 --- @class vim.lsp.rpc.ExtraSpawnParams
    641 --- @inlinedoc
    642 --- @field cwd? string Working directory for the LSP server process
    643 --- @field detached? boolean Detach the LSP server process from the current process
    644 --- @field env? table<string,string> Additional environment variables for LSP server process. See |vim.system()|
    645 
    646 --- Starts an LSP server process and create an LSP RPC client object to
    647 --- interact with it. Communication with the spawned process happens via stdio. For
    648 --- communication via TCP, spawn a process manually and use |vim.lsp.rpc.connect()|
    649 ---
    650 --- @param cmd string[] Command to start the LSP server.
    651 --- @param dispatchers? vim.lsp.rpc.Dispatchers
    652 --- @param extra_spawn_params? vim.lsp.rpc.ExtraSpawnParams
    653 --- @return vim.lsp.rpc.PublicClient
    654 function M.start(cmd, dispatchers, extra_spawn_params)
    655  log.info('Starting RPC client', { cmd = cmd, extra = extra_spawn_params })
    656 
    657  validate('cmd', cmd, 'table')
    658  validate('dispatchers', dispatchers, 'table', true)
    659 
    660  dispatchers = merge_dispatchers(dispatchers)
    661 
    662  local transport = lsp_transport.TransportRun.new()
    663  local client = new_client(dispatchers, transport)
    664  local on_read = create_client_read_loop(client)
    665  transport:run(cmd, extra_spawn_params, on_read, dispatchers.on_exit)
    666 
    667  return public_client(client)
    668 end
    669 
    670 return M