diff options
Diffstat (limited to 'runtime/lua/vim/lsp/_transport.lua')
-rw-r--r-- | runtime/lua/vim/lsp/_transport.lua | 182 |
1 files changed, 182 insertions, 0 deletions
diff --git a/runtime/lua/vim/lsp/_transport.lua b/runtime/lua/vim/lsp/_transport.lua new file mode 100644 index 0000000000..19ff2a8ab0 --- /dev/null +++ b/runtime/lua/vim/lsp/_transport.lua @@ -0,0 +1,182 @@ +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 + +--- @class (private) vim.lsp.rpc.Transport +--- @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 +--- @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 +--- @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) + local function on_stderr(_, chunk) + if chunk then + log.error('rpc', cmd[1], 'stderr', chunk) + end + end + + extra_spawn_params = extra_spawn_params or {} + + if extra_spawn_params.cwd then + assert(is_dir(extra_spawn_params.cwd), 'cwd must be a directory') + end + + local detached = not is_win + if extra_spawn_params.detached ~= nil then + detached = extra_spawn_params.detached + end + + local ok, sysobj_or_err = pcall(vim.system, cmd, { + stdin = true, + stdout = on_read, + stderr = on_stderr, + cwd = extra_spawn_params.cwd, + env = 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]] + 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 + + self.sysobj = sysobj_or_err --[[@as vim.SystemObj]] +end + +function TransportRun:write(msg) + assert(self.sysobj):write(msg) +end + +function TransportRun:is_closing() + return self.sysobj == nil or self.sysobj:is_closing() +end + +function TransportRun:terminate() + assert(self.sysobj):kill(15) +end + +--- @class (private,exact) vim.lsp.rpc.Transport.Connect : vim.lsp.rpc.Transport +--- @field new fun(): vim.lsp.rpc.Transport.Connect +--- @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 +--- @field connected boolean +--- @field closing boolean +--- @field msgbuf vim.Ringbuf +--- @field on_exit? fun(code: integer, signal: integer) +local TransportConnect = {} + +--- @return vim.lsp.rpc.Transport.Connect +function TransportConnect.new() + return setmetatable({ + 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 + msgbuf = vim.ringbuf(10), + closing = false, + }, { __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) + self.on_exit = on_exit + self.handle = ( + 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) + vim.schedule(function() + vim.notify( + string.format('Could not connect to %s, reason: %s', address, vim.inspect(err)), + vim.log.levels.WARN + ) + end) + return + end + self.handle:read_start(on_read) + self.connected = true + for msg in self.msgbuf do + self.handle:write(msg) + end + end + + if not port then + self.handle:connect(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) +end + +function TransportConnect:write(msg) + if self.connected then + local _, err = self.handle:write(msg) + if err and not self.closing then + log.error('Error on handle:write: %q', err) + end + return + end + + self.msgbuf:push(msg) +end + +function TransportConnect:is_closing() + return self.closing +end + +function TransportConnect:terminate() + if self.closing then + return + end + self.closing = true + if self.handle then + self.handle:shutdown() + self.handle:close() + end + if self.on_exit then + self.on_exit(0, 0) + end +end + +return { + TransportRun = TransportRun, + TransportConnect = TransportConnect, +} |