123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183 |
- 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,
- }
|