Skip to content

Commit f8a31fe

Browse files
committed
refactor(lsp/rpc): move transport logic to separate module
1 parent ca760e6 commit f8a31fe

File tree

4 files changed

+272
-218
lines changed

4 files changed

+272
-218
lines changed

runtime/doc/lsp.txt

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2270,14 +2270,15 @@ should_log({level}) *vim.lsp.log.should_log()*
22702270
Lua module: vim.lsp.rpc *lsp-rpc*
22712271

22722272
*vim.lsp.rpc.PublicClient*
2273+
Client RPC object
22732274

22742275
Fields: ~
2275-
{request} (`fun(method: string, params: table?, callback: fun(err: lsp.ResponseError?, result: any), notify_reply_callback: fun(message_id: integer)?):boolean,integer?`)
2276-
see |vim.lsp.rpc.request()|
2277-
{notify} (`fun(method: string, params: any):boolean`) see
2276+
{request} (`fun(method: string, params: table?, callback: fun(err?: lsp.ResponseError, result: any), notify_reply_callback?: fun(message_id: integer)):boolean,integer?`)
2277+
See |vim.lsp.rpc.request()|
2278+
{notify} (`fun(method: string, params: any): boolean`) See
22782279
|vim.lsp.rpc.notify()|
2279-
• {is_closing} (`fun(): boolean`)
2280-
{terminate} (`fun()`)
2280+
• {is_closing} (`fun(): boolean`) Indicates if the RPC is closing.
2281+
{terminate} (`fun()`) Terminates the RPC client.
22812282

22822283

22832284
connect({host_or_path}, {port}) *vim.lsp.rpc.connect()*
@@ -2378,12 +2379,7 @@ start({cmd}, {dispatchers}, {extra_spawn_params}) *vim.lsp.rpc.start()*
23782379
See |vim.system()|
23792380

23802381
Return: ~
2381-
(`vim.lsp.rpc.PublicClient`) Client RPC object, with these methods:
2382-
`notify()` |vim.lsp.rpc.notify()|
2383-
`request()` |vim.lsp.rpc.request()|
2384-
`is_closing()` returns a boolean indicating if the RPC is closing.
2385-
`terminate()` terminates the RPC client. See
2386-
|vim.lsp.rpc.PublicClient|.
2382+
(`vim.lsp.rpc.PublicClient`) See |vim.lsp.rpc.PublicClient|.
23872383

23882384

23892385
==============================================================================

runtime/lua/vim/lsp/_transport.lua

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
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

Comments
 (0)