|
| 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.Spawn : vim.lsp.rpc.Transport |
| 20 | +--- @field new fun(): vim.lsp.rpc.Transport.Spawn |
| 21 | +--- @field sysobj? vim.SystemObj |
| 22 | +local Spawn = {} |
| 23 | + |
| 24 | +--- @return vim.lsp.rpc.Transport.Spawn |
| 25 | +function Spawn.new() |
| 26 | + return setmetatable({}, { __index = Spawn }) |
| 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 Spawn:spawn(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 Spawn:write(msg) |
| 75 | + assert(self.sysobj):write(msg) |
| 76 | +end |
| 77 | + |
| 78 | +function Spawn:is_closing() |
| 79 | + return self.sysobj == nil or self.sysobj:is_closing() |
| 80 | +end |
| 81 | + |
| 82 | +function Spawn: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 Connect = {} |
| 97 | + |
| 98 | +--- @return vim.lsp.rpc.Transport.Connect |
| 99 | +function Connect.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 = Connect }) |
| 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 Connect: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 Connect: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 Connect:is_closing() |
| 162 | + return self.closing |
| 163 | +end |
| 164 | + |
| 165 | +function Connect: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 | + TransportSpawn = Spawn, |
| 181 | + TransportConnect = Connect, |
| 182 | +} |
0 commit comments