_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 }