_transport.lua 5.2 KB

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