neovim

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

_transport.lua (5276B)


      1 local uv = vim.uv
      2 local log = require('vim.lsp.log')
      3 
      4 local is_win = vim.fn.has('win32') == 1
      5 
      6 --- Checks whether a given path exists and is a directory.
      7 ---@param filename string path to check
      8 ---@return boolean
      9 local function is_dir(filename)
     10  local stat = uv.fs_stat(filename)
     11  return stat and stat.type == 'directory' or false
     12 end
     13 
     14 --- @class (private) vim.lsp.rpc.Transport
     15 --- @field write fun(self: vim.lsp.rpc.Transport, msg: string)
     16 --- @field is_closing fun(self: vim.lsp.rpc.Transport): boolean
     17 --- @field terminate fun(self: vim.lsp.rpc.Transport)
     18 
     19 --- @class (private,exact) vim.lsp.rpc.Transport.Run : vim.lsp.rpc.Transport
     20 --- @field new fun(): vim.lsp.rpc.Transport.Run
     21 --- @field sysobj? vim.SystemObj
     22 local TransportRun = {}
     23 
     24 --- @return vim.lsp.rpc.Transport.Run
     25 function TransportRun.new()
     26  return setmetatable({}, { __index = TransportRun })
     27 end
     28 
     29 --- @param cmd string[] Command to start the LSP server.
     30 --- @param extra_spawn_params? vim.lsp.rpc.ExtraSpawnParams
     31 --- @param on_read fun(err: any, data: string)
     32 --- @param on_exit fun(code: integer, signal: integer)
     33 function TransportRun:run(cmd, extra_spawn_params, on_read, on_exit)
     34  local function on_stderr(_, chunk)
     35    if chunk then
     36      log.error('rpc', cmd[1], 'stderr', chunk)
     37    end
     38  end
     39 
     40  extra_spawn_params = extra_spawn_params or {}
     41 
     42  if extra_spawn_params.cwd then
     43    assert(is_dir(extra_spawn_params.cwd), 'cwd must be a directory')
     44  end
     45 
     46  local detached = not is_win
     47  if extra_spawn_params.detached ~= nil then
     48    detached = extra_spawn_params.detached
     49  end
     50 
     51  local ok, sysobj_or_err = pcall(vim.system, cmd, {
     52    stdin = true,
     53    stdout = on_read,
     54    stderr = on_stderr,
     55    cwd = extra_spawn_params.cwd,
     56    env = extra_spawn_params.env,
     57    detach = detached,
     58  }, function(obj)
     59    on_exit(obj.code, obj.signal)
     60  end)
     61 
     62  if not ok then
     63    local err = sysobj_or_err --[[@as string]]
     64    local sfx = err:match('ENOENT')
     65        and '. The language server is either not installed, missing from PATH, or not executable.'
     66      or string.format(' with error message: %s', err)
     67 
     68    error(('Spawning language server with cmd: `%s` failed%s'):format(vim.inspect(cmd), sfx))
     69  end
     70 
     71  self.sysobj = sysobj_or_err --[[@as vim.SystemObj]]
     72 end
     73 
     74 function TransportRun:write(msg)
     75  assert(self.sysobj):write(msg)
     76 end
     77 
     78 function TransportRun:is_closing()
     79  return self.sysobj == nil or self.sysobj:is_closing()
     80 end
     81 
     82 function TransportRun:terminate()
     83  assert(self.sysobj):kill(15)
     84 end
     85 
     86 --- @class (private,exact) vim.lsp.rpc.Transport.Connect : vim.lsp.rpc.Transport
     87 --- @field new fun(): vim.lsp.rpc.Transport.Connect
     88 --- @field handle? uv.uv_pipe_t|uv.uv_tcp_t
     89 --- Connect returns a PublicClient synchronously so the caller
     90 --- can immediately send messages before the connection is established
     91 --- -> Need to buffer them until that happens
     92 --- @field connected boolean
     93 --- @field closing boolean
     94 --- @field msgbuf vim.Ringbuf
     95 --- @field on_exit? fun(code: integer, signal: integer)
     96 local TransportConnect = {}
     97 
     98 --- @return vim.lsp.rpc.Transport.Connect
     99 function TransportConnect.new()
    100  return setmetatable({
    101    connected = false,
    102    -- size should be enough because the client can't really do anything until initialization is done
    103    -- which required a response from the server - implying the connection got established
    104    msgbuf = vim.ringbuf(10),
    105    closing = false,
    106  }, { __index = TransportConnect })
    107 end
    108 
    109 --- @param host_or_path string
    110 --- @param port? integer
    111 --- @param on_read fun(err: any, data: string)
    112 --- @param on_exit? fun(code: integer, signal: integer)
    113 function TransportConnect:connect(host_or_path, port, on_read, on_exit)
    114  self.on_exit = on_exit
    115  self.handle = (
    116    port and assert(uv.new_tcp(), 'Could not create new TCP socket')
    117    or assert(uv.new_pipe(false), 'Pipe could not be opened.')
    118  )
    119 
    120  local function on_connect(err)
    121    if err then
    122      local address = not port and host_or_path or (host_or_path .. ':' .. port)
    123      vim.schedule(function()
    124        vim.notify(
    125          string.format('Could not connect to %s, reason: %s', address, vim.inspect(err)),
    126          vim.log.levels.WARN
    127        )
    128      end)
    129      return
    130    end
    131    self.handle:read_start(on_read)
    132    self.connected = true
    133    for msg in self.msgbuf do
    134      self.handle:write(msg)
    135    end
    136  end
    137 
    138  if not port then
    139    self.handle:connect(host_or_path, on_connect)
    140    return
    141  end
    142 
    143  --- @diagnostic disable-next-line:param-type-mismatch bad UV typing
    144  local info = uv.getaddrinfo(host_or_path, nil)
    145  local resolved_host = info and info[1] and info[1].addr or host_or_path
    146  self.handle:connect(resolved_host, port, on_connect)
    147 end
    148 
    149 function TransportConnect:write(msg)
    150  if self.connected then
    151    local _, err = self.handle:write(msg)
    152    if err and not self.closing then
    153      log.error('Error on handle:write: %q', err)
    154    end
    155    return
    156  end
    157 
    158  self.msgbuf:push(msg)
    159 end
    160 
    161 function TransportConnect:is_closing()
    162  return self.closing
    163 end
    164 
    165 function TransportConnect:terminate()
    166  if self.closing then
    167    return
    168  end
    169  self.closing = true
    170  if self.handle then
    171    self.handle:shutdown()
    172    self.handle:close()
    173  end
    174  if self.on_exit then
    175    self.on_exit(0, 0)
    176  end
    177 end
    178 
    179 return {
    180  TransportRun = TransportRun,
    181  TransportConnect = TransportConnect,
    182 }