lspleanklib 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. lspleanklib-0.1.1/.gitignore +5 -0
  2. lspleanklib-0.1.1/LICENSE +21 -0
  3. lspleanklib-0.1.1/PKG-INFO +34 -0
  4. lspleanklib-0.1.1/justfile +19 -0
  5. lspleanklib-0.1.1/lspleanklib/__init__.py +40 -0
  6. lspleanklib-0.1.1/lspleanklib/__main__.py +3 -0
  7. lspleanklib-0.1.1/lspleanklib/aio.py +100 -0
  8. lspleanklib-0.1.1/lspleanklib/cli.py +25 -0
  9. lspleanklib-0.1.1/lspleanklib/jsonrpc.py +332 -0
  10. lspleanklib-0.1.1/lspleanklib/lake.py +87 -0
  11. lspleanklib-0.1.1/lspleanklib/lakelspout.py +135 -0
  12. lspleanklib-0.1.1/lspleanklib/lspleank.py +306 -0
  13. lspleanklib-0.1.1/lspleanklib/py.typed +0 -0
  14. lspleanklib-0.1.1/lspleanklib/server.py +345 -0
  15. lspleanklib-0.1.1/lspleanklib/util.py +54 -0
  16. lspleanklib-0.1.1/lspleanklib.egg-info/PKG-INFO +34 -0
  17. lspleanklib-0.1.1/lspleanklib.egg-info/SOURCES.txt +37 -0
  18. lspleanklib-0.1.1/lspleanklib.egg-info/dependency_links.txt +1 -0
  19. lspleanklib-0.1.1/lspleanklib.egg-info/entry_points.txt +3 -0
  20. lspleanklib-0.1.1/lspleanklib.egg-info/requires.txt +3 -0
  21. lspleanklib-0.1.1/lspleanklib.egg-info/top_level.txt +1 -0
  22. lspleanklib-0.1.1/pyproject.toml +47 -0
  23. lspleanklib-0.1.1/setup.cfg +4 -0
  24. lspleanklib-0.1.1/tests/cases/alt_import/Main.lean +2 -0
  25. lspleanklib-0.1.1/tests/cases/alt_import/Min/Sub.lean +1 -0
  26. lspleanklib-0.1.1/tests/cases/alt_import/Min.lean +3 -0
  27. lspleanklib-0.1.1/tests/cases/alt_import/lake-manifest.json +5 -0
  28. lspleanklib-0.1.1/tests/cases/alt_import/lakefile.toml +4 -0
  29. lspleanklib-0.1.1/tests/cases/alt_import/lean-toolchain +1 -0
  30. lspleanklib-0.1.1/tests/cases/min_import/Main.lean +2 -0
  31. lspleanklib-0.1.1/tests/cases/min_import/Min.lean +1 -0
  32. lspleanklib-0.1.1/tests/cases/min_import/lake-manifest.json +5 -0
  33. lspleanklib-0.1.1/tests/cases/min_import/lakefile.toml +4 -0
  34. lspleanklib-0.1.1/tests/cases/min_import/lean-toolchain +1 -0
  35. lspleanklib-0.1.1/tests/cases/vim9-lsp-init.txt +3 -0
  36. lspleanklib-0.1.1/tests/test_lake.py +123 -0
  37. lspleanklib-0.1.1/tests/test_rpc.py +74 -0
  38. lspleanklib-0.1.1/tests/test_server.py +285 -0
  39. lspleanklib-0.1.1/tests/util.py +94 -0
@@ -0,0 +1,5 @@
1
+ __pycache__
2
+ dist
3
+ build
4
+ *.egg-info
5
+ _version.py
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 E. Castedo Ellerman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
21
+ DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,34 @@
1
+ Metadata-Version: 2.1
2
+ Name: lspleanklib
3
+ Version: 0.1.1
4
+ Summary: LSP proxy and library to link LSP-enabled editors to Lake LSP servers
5
+ Author-email: Castedo Ellerman <castedo@castedo.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 E. Castedo Ellerman
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be
18
+ included in all copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
26
+ DEALINGS IN THE SOFTWARE.
27
+
28
+ Keywords: lsp,language-server-protocol,jsonrpc
29
+ Classifier: Programming Language :: Python :: 3
30
+ Classifier: Operating System :: POSIX
31
+ Requires-Python: >=3.12
32
+ License-File: LICENSE
33
+ Provides-Extra: crossplatform
34
+ Requires-Dist: platformdirs; extra == "crossplatform"
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env -S just --justfile
2
+
3
+ default:
4
+ just --list
5
+
6
+ test:
7
+ ruff check lspleanklib || true
8
+ mypy --strict lspleanklib
9
+ mypy tests --cache-dir tests/.mypy_cache
10
+ pytest -vv tests --timeout=2 \
11
+ # -m 'not slow' \
12
+ # --durations=3 \
13
+ # --log-cli-level=DEBUG \
14
+
15
+ clean:
16
+ rm -rf dist
17
+ rm -rf build
18
+ rm -rf *.egg-info
19
+ rm -f _version.py
@@ -0,0 +1,40 @@
1
+ from .jsonrpc import (
2
+ ErrorCode,
3
+ JsonRpcMsg,
4
+ MethodCall,
5
+ Response,
6
+ RpcChannel,
7
+ RpcInterface,
8
+ RpcMsgChannel,
9
+ RpcMsgConnection,
10
+ awaitable_error,
11
+ json_rpc_channel,
12
+ )
13
+ from .lspleank import lspleank_connect_main
14
+ from .server import (
15
+ RpcDirChannelFactory,
16
+ RpcSubprocessFactory,
17
+ channel_lsp_server,
18
+ get_user_socket_path,
19
+ )
20
+ from .util import LspAny, LspObject
21
+
22
+ __all__ = (
23
+ 'ErrorCode',
24
+ 'JsonRpcMsg',
25
+ 'LspAny',
26
+ 'LspObject',
27
+ 'MethodCall',
28
+ 'Response',
29
+ 'RpcChannel',
30
+ 'RpcDirChannelFactory',
31
+ 'RpcInterface',
32
+ 'RpcMsgChannel',
33
+ 'RpcMsgConnection',
34
+ 'RpcSubprocessFactory',
35
+ 'awaitable_error',
36
+ 'channel_lsp_server',
37
+ 'get_user_socket_path',
38
+ 'json_rpc_channel',
39
+ 'lspleank_connect_main',
40
+ )
@@ -0,0 +1,3 @@
1
+ from .lspleank import main
2
+
3
+ exit(main())
@@ -0,0 +1,100 @@
1
+ """
2
+ Asynchronous IO
3
+ """
4
+
5
+ import asyncio, io, os, typing
6
+ from asyncio import AbstractEventLoop, Future
7
+ from collections.abc import Callable
8
+ from concurrent.futures import ThreadPoolExecutor
9
+ from typing import BinaryIO
10
+
11
+
12
+ class MinimalWriter(typing.Protocol):
13
+ def write(self, data: bytes) -> None: ...
14
+ async def drain(self) -> None: ...
15
+ def close(self) -> None: ...
16
+ async def wait_closed(self) -> None: ...
17
+ def is_closing(self) -> bool: ...
18
+
19
+
20
+ class MinimalReader(typing.Protocol):
21
+ async def readuntil(self) -> bytes: ... # read until and return with '\n'
22
+ async def readexactly(self, n: int) -> bytes: ...
23
+ async def read(self) -> bytes: ...
24
+ def at_eof(self) -> bool: ...
25
+
26
+
27
+ class DuplexStream:
28
+ def __init__(self, ain: MinimalReader, aout: MinimalWriter):
29
+ self.ain = ain
30
+ self.aout = aout
31
+
32
+
33
+ class WriterFileAdapter(MinimalWriter):
34
+ def __init__(self, fout: BinaryIO, *, loop: AbstractEventLoop):
35
+ self._fout = fout
36
+ self._loop = loop
37
+ self._pool = ThreadPoolExecutor(1, thread_name_prefix="blocking_write")
38
+ self._buf: list[bytes] = []
39
+ self._closing: Future[None] | None = None
40
+
41
+ def write(self, data: bytes) -> None:
42
+ if self._closing is not None:
43
+ raise ValueError('write to closed file')
44
+ if data:
45
+ self._buf.append(data)
46
+
47
+ async def drain(self) -> None:
48
+ if self._buf:
49
+ todo = tuple(self._buf)
50
+ self._buf.clear()
51
+ await self._loop.run_in_executor(self._pool, self._drain, todo)
52
+
53
+ def _drain(self, todo: tuple[bytes, ...]) -> None:
54
+ for data in todo:
55
+ self._fout.write(data)
56
+ self._fout.flush()
57
+
58
+ def close(self) -> None:
59
+ if self._closing is None:
60
+ self._closing = self._loop.run_in_executor(self._pool, self._fout.close)
61
+
62
+ async def wait_closed(self) -> None:
63
+ if self._closing is None:
64
+ raise ValueError('file not closed')
65
+ await self._closing
66
+
67
+ def is_closing(self) -> bool:
68
+ return self._closing is not None
69
+
70
+
71
+ class ReadFilePump:
72
+ def __init__(
73
+ self,
74
+ fd: int,
75
+ *,
76
+ loop: AbstractEventLoop,
77
+ on_eof: Callable[[], None] = lambda: None,
78
+ ) -> None:
79
+ self._fd = fd
80
+ self._loop = loop
81
+ self.stream = asyncio.StreamReader(loop=loop)
82
+ self._on_eof = on_eof
83
+
84
+ def run(self) -> None:
85
+ try:
86
+ while True:
87
+ try:
88
+ data = os.read(self._fd, io.DEFAULT_BUFFER_SIZE)
89
+ except KeyboardInterrupt as ex:
90
+ nex = IOError('file read interrupted')
91
+ nex.__cause__ = ex
92
+ self._loop.call_soon_threadsafe(self.stream.set_exception, nex)
93
+ return
94
+ if data:
95
+ self._loop.call_soon_threadsafe(self.stream.feed_data, data)
96
+ else:
97
+ self._loop.call_soon_threadsafe(self.stream.feed_eof)
98
+ return
99
+ finally:
100
+ self._loop.call_soon_threadsafe(self._on_eof)
@@ -0,0 +1,25 @@
1
+ """
2
+ CLI common code
3
+ """
4
+
5
+
6
+ def version() -> str:
7
+ try:
8
+ from ._version import version # type: ignore[import-not-found, import-untyped, unused-ignore]
9
+
10
+ return str(version)
11
+ except ImportError:
12
+ return '0.0.0'
13
+
14
+
15
+ def split_cmd_line(
16
+ cmd_line_args: list[str], default_extra_args: list[str] | None = None
17
+ ) -> tuple[list[str], list[str]]:
18
+ try:
19
+ cut = cmd_line_args.index('--')
20
+ except ValueError:
21
+ cut = len(cmd_line_args)
22
+ extra_args = cmd_line_args[cut + 1 :]
23
+ if default_extra_args is None:
24
+ default_extra_args = []
25
+ return (cmd_line_args[:cut], extra_args or default_extra_args)
@@ -0,0 +1,332 @@
1
+ """
2
+ JSON-RPC for Language Server Protocol
3
+ """
4
+
5
+ from __future__ import annotations
6
+ import asyncio, copy, enum, json, typing
7
+ from asyncio import AbstractEventLoop, Future, TaskGroup
8
+ from collections.abc import Awaitable, Mapping, Sequence
9
+ from dataclasses import asdict, dataclass
10
+ from typing import Any, TypeAlias
11
+ from warnings import warn
12
+
13
+ from .aio import DuplexStream, MinimalReader, MinimalWriter
14
+ from .util import LspAny, LspObject, log
15
+
16
+
17
+ class ErrorCode(enum.IntEnum):
18
+ UnknownErrorCode = -32001
19
+ ServerNotInitialized = -32002
20
+ InvalidRequest = -32600
21
+ MethodNotFound = -32601
22
+ InvalidParams = -32602
23
+ InternalError = -32603
24
+ RequestFailed = -32803
25
+
26
+
27
+ ErrorCodes = ErrorCode # in the LSP specification the name is unpythonic plural
28
+
29
+
30
+ async def write_message(stream: MinimalWriter, msg: Mapping[str, Any]) -> None:
31
+ body = json.dumps(msg, separators=(',', ':')).encode()
32
+ header = f"Content-Length: {len(body)}\r\n\r\n".encode()
33
+ stream.write(header)
34
+ stream.write(body)
35
+ await stream.drain()
36
+
37
+
38
+ async def read_message(stream: MinimalReader) -> dict[str, Any] | None:
39
+ body_length = 0
40
+ try:
41
+ while True:
42
+ line = await stream.readuntil()
43
+ key_val = line.split(b':', 2)
44
+ if len(key_val) == 2:
45
+ if key_val[0].lower() == b'content-length':
46
+ body_length = int(key_val[1].strip())
47
+ else:
48
+ # end of header (empty line if correct LSP)
49
+ break
50
+ body = await stream.readexactly(body_length)
51
+ ret = json.loads(body)
52
+ if not isinstance(ret, dict):
53
+ raise ValueError("expecting JSON-RPC message to be JSON object")
54
+ return ret
55
+ except asyncio.exceptions.IncompleteReadError as ex:
56
+ if ex.partial:
57
+ raise ValueError("truncated stream input") from ex
58
+ return None
59
+
60
+
61
+ MsgParams: TypeAlias = Sequence[LspAny] | Mapping[str, LspAny]
62
+
63
+
64
+ @dataclass
65
+ class MethodCall:
66
+ method: str
67
+ params: MsgParams | None = None
68
+
69
+ def to_lsp_obj(self) -> LspObject:
70
+ lobj: dict[str, LspAny] = {'method': self.method}
71
+ if self.params is not None:
72
+ lobj['params'] = copy.deepcopy(self.params)
73
+ return lobj
74
+
75
+
76
+ @dataclass
77
+ class ResponseError:
78
+ code: int
79
+ message: str
80
+ data: LspAny | None = None
81
+
82
+ @staticmethod
83
+ def from_lsp_obj(msg: LspObject) -> ResponseError:
84
+ code = msg.get('code')
85
+ if not isinstance(code, int):
86
+ code = ErrorCode.UnknownErrorCode
87
+ return ResponseError(code, str(msg.get('message')), msg.get('data'))
88
+
89
+
90
+ async def awaitable_error(ec: ErrorCode) -> Response:
91
+ return Response.from_error_code(ec)
92
+
93
+
94
+ @dataclass
95
+ class Response:
96
+ result: LspAny
97
+ error: ResponseError | None = None
98
+
99
+ @staticmethod
100
+ def from_lsp_obj(msg: LspObject) -> Response:
101
+ error = msg.get('error')
102
+ if error is None:
103
+ return Response(msg.get('result'))
104
+ elif not isinstance(error, dict):
105
+ raise ValueError('LSP errors must be JSON objects')
106
+ else:
107
+ return Response(None, ResponseError.from_lsp_obj(error))
108
+
109
+ @staticmethod
110
+ def from_error_code(ec: ErrorCode) -> Response:
111
+ return Response(None, ResponseError(ec, ec.name))
112
+
113
+ def to_lsp_obj(self) -> LspObject:
114
+ if self.error:
115
+ return {'error': asdict(self.error)}
116
+ else:
117
+ return {'result': self.result}
118
+
119
+
120
+ @dataclass
121
+ class JsonRpcMsg:
122
+ payload: MethodCall | Response
123
+ id: int | str | None = None
124
+
125
+ @staticmethod
126
+ def from_jsonrpc(msg: LspObject) -> JsonRpcMsg:
127
+ if msg.get('jsonrpc') != '2.0':
128
+ raise ValueError('JSON object is not JSON-RPC 2.0 message')
129
+ msg_id = typing.cast(int | str | None, msg.get('id'))
130
+ method = msg.get('method')
131
+ if method is None:
132
+ return JsonRpcMsg(Response.from_lsp_obj(msg), msg_id)
133
+ elif not isinstance(method, str):
134
+ raise ValueError('LSP method names must be strings')
135
+ else:
136
+ params = typing.cast(MsgParams | None, msg.get('params'))
137
+ return JsonRpcMsg(MethodCall(method, params), msg_id)
138
+
139
+ def to_lsp_obj(self) -> LspObject:
140
+ ret: dict[str, LspAny] = {} if self.id is None else {'id': self.id}
141
+ ret.update(self.payload.to_lsp_obj())
142
+ ret['jsonrpc'] = '2.0'
143
+ return ret
144
+
145
+
146
+ class IncommingResponses:
147
+ def __init__(self, loop: AbstractEventLoop) -> None:
148
+ self._loop = loop
149
+ self._todo: dict[int | str | None, Future[Response]] = {}
150
+ self.next_id = 1
151
+
152
+ def __bool__(self) -> bool:
153
+ return bool(self._todo)
154
+
155
+ def cancel_all(self) -> None:
156
+ while self._todo:
157
+ msg_id, expect = self._todo.popitem()
158
+ expect.cancel()
159
+
160
+ def prepare(self, fixed_id: str | None) -> tuple[int | str, Future[Response]]:
161
+ msg_id = self.next_id if fixed_id is None else fixed_id
162
+ if fixed_id is None:
163
+ self.next_id += 1
164
+ stale = self._todo.pop(msg_id, None)
165
+ if stale is not None:
166
+ warn(f"Response abandoned due to id reuse by new request: {msg_id}")
167
+ stale.cancel()
168
+ expect: Future[Response] = self._loop.create_future()
169
+ self._todo[msg_id] = expect
170
+ return (msg_id, expect)
171
+
172
+ def got_response(self, response: Response, msg_id: int | str | None) -> None:
173
+ expect = self._todo.pop(msg_id, None)
174
+ if expect:
175
+ expect.set_result(response)
176
+ else:
177
+ warn(f"Unexpected response with id: {msg_id}")
178
+
179
+
180
+ class RpcMsgConnection(typing.Protocol):
181
+ async def close_and_wait(self) -> None: ...
182
+ async def write(self, msg: JsonRpcMsg) -> None: ...
183
+ async def read(self) -> JsonRpcMsg | None: ...
184
+
185
+
186
+ class JsonRpcMsgStream(RpcMsgConnection):
187
+ def __init__(self, aio: DuplexStream):
188
+ self._aio = aio
189
+
190
+ async def close_and_wait(self) -> None:
191
+ self._aio.aout.close()
192
+ await self._aio.aout.wait_closed()
193
+
194
+ async def write(self, msg: JsonRpcMsg) -> None:
195
+ await write_message(self._aio.aout, msg.to_lsp_obj())
196
+
197
+ async def read(self) -> JsonRpcMsg | None:
198
+ while (msg := await read_message(self._aio.ain)) is not None:
199
+ try:
200
+ return JsonRpcMsg.from_jsonrpc(msg)
201
+ except ValueError as ex:
202
+ log.exception(ex)
203
+ return None
204
+
205
+
206
+ class RpcInterface(typing.Protocol):
207
+ async def close_and_wait(self) -> None: ...
208
+ async def notify(self, mc: MethodCall) -> None: ...
209
+ async def request(
210
+ self, mc: MethodCall, fix_id: str | None = None
211
+ ) -> Awaitable[Response]: ...
212
+
213
+
214
+ class RemoteRpcProxy(RpcInterface):
215
+ def __init__(self, conn: RpcMsgConnection, *, loop: AbstractEventLoop):
216
+ self._conn = conn
217
+ self._expecting: IncommingResponses | None = IncommingResponses(loop)
218
+
219
+ async def close_and_wait(self) -> None:
220
+ log.debug(f"closing {self.__class__.__name__}")
221
+ await self._conn.close_and_wait()
222
+
223
+ async def notify(self, mc: MethodCall) -> None:
224
+ msg = JsonRpcMsg(mc)
225
+ try:
226
+ await self._conn.write(msg)
227
+ except RuntimeError:
228
+ log.exception(f"Write failed for RPC call '{mc.method}'")
229
+
230
+ async def request(
231
+ self, mc: MethodCall, fix_id: str | None = None
232
+ ) -> Awaitable[Response]:
233
+ if self._expecting is None:
234
+ warn('request called on closed or closing RPC connection')
235
+ return awaitable_error(ErrorCode.InternalError)
236
+ (msg_id, expect) = self._expecting.prepare(fix_id)
237
+ try:
238
+ await self._conn.write(JsonRpcMsg(mc, msg_id))
239
+ except RuntimeError:
240
+ error_response = Response.from_error_code(ErrorCode.InternalError)
241
+ self._expecting.got_response(error_response, msg_id)
242
+ log.exception(f"Write failed for RPC call '{mc.method}'")
243
+ return expect
244
+
245
+ def got_response(self, response: Response, msg_id: int | str | None) -> None:
246
+ if self._expecting is None:
247
+ warn('RemoteRpcProxy.got_response called after end_of_incoming_responses')
248
+ else:
249
+ self._expecting.got_response(response, msg_id)
250
+
251
+ def end_of_incomming_responses(self) -> None:
252
+ if self._expecting:
253
+ self._expecting.cancel_all()
254
+ log.info("orphaned incoming responses cancelled")
255
+ self._expecting = None
256
+
257
+
258
+ class RpcChannel(typing.Protocol):
259
+ @property
260
+ def proxy(self) -> RpcInterface: ...
261
+
262
+ async def pump(self, impl: RpcInterface | None = None) -> None: ...
263
+
264
+
265
+ class NoClient(RpcInterface):
266
+ async def notify(self, mc: MethodCall) -> None:
267
+ warn(f"No client RPC implementation for '{mc.method}' notification")
268
+
269
+ async def request(
270
+ self, mc: MethodCall, fix_id: str | None = None
271
+ ) -> Awaitable[Response]:
272
+ warn(f"No client RPC implementation for '{mc.method}' request")
273
+ return awaitable_error(ErrorCode.MethodNotFound)
274
+
275
+ async def close_and_wait(self) -> None:
276
+ pass
277
+
278
+
279
+ async def await_send_response(
280
+ conn: RpcMsgConnection, tbd: Awaitable[Response], msg_id: int | str | None
281
+ ) -> None:
282
+ try:
283
+ response = await tbd
284
+ await conn.write(JsonRpcMsg(response, msg_id))
285
+ except (asyncio.CancelledError, ValueError) as ex:
286
+ # writing to closed aout stream raises ValueError
287
+ log.exception(ex)
288
+
289
+
290
+ class RpcMsgChannel(RpcChannel):
291
+ def __init__(self, conn: RpcMsgConnection, *, name: str, loop: AbstractEventLoop):
292
+ self._conn = conn
293
+ self._proxy = RemoteRpcProxy(conn, loop=loop)
294
+ self.name = name
295
+
296
+ @property
297
+ def proxy(self) -> RpcInterface:
298
+ return self._proxy
299
+
300
+ async def pump(self, impl: RpcInterface | None = None) -> None:
301
+ """Listen for JSONRPC message on connection input until stream EOF.
302
+
303
+ impl: implements the methods for RPC calls received on stream input
304
+ """
305
+ impl = impl or NoClient()
306
+ try:
307
+ async with TaskGroup() as response_tasks:
308
+ try:
309
+ while (msg := await self._conn.read()) is not None:
310
+ if isinstance(msg.payload, Response):
311
+ self._proxy.got_response(msg.payload, msg.id)
312
+ elif msg.id is None:
313
+ await impl.notify(msg.payload)
314
+ else:
315
+ fix_id = msg.id if isinstance(msg.id, str) else None
316
+ tbd = await impl.request(msg.payload, fix_id)
317
+ coro = await_send_response(self._conn, tbd, msg.id)
318
+ response_tasks.create_task(coro)
319
+ finally:
320
+ await impl.close_and_wait()
321
+ self._proxy.end_of_incomming_responses()
322
+ log.debug(f"{self.name} pump done reading responses")
323
+ finally:
324
+ log.debug(f"{self.name} pump closing connection")
325
+ await self._conn.close_and_wait()
326
+
327
+
328
+ def json_rpc_channel(
329
+ ain: MinimalReader, aout: MinimalWriter, *, name: str, loop: AbstractEventLoop
330
+ ) -> RpcMsgChannel:
331
+ stream = JsonRpcMsgStream(DuplexStream(ain, aout))
332
+ return RpcMsgChannel(stream, name=name, loop=loop)
@@ -0,0 +1,87 @@
1
+ """
2
+ Code for the Lake LSP server
3
+ """
4
+
5
+ from collections.abc import Awaitable
6
+ from pathlib import Path
7
+
8
+ from .jsonrpc import (
9
+ ErrorCode,
10
+ MethodCall,
11
+ Response,
12
+ RpcChannel,
13
+ RpcInterface,
14
+ awaitable_error,
15
+ )
16
+ from .server import (
17
+ RpcDirChannelFactory,
18
+ leank_init_call,
19
+ leank_init_response,
20
+ text_doc_caps,
21
+ )
22
+ from .util import awaitable, get_uri_path
23
+
24
+
25
+ class LakeClient(RpcInterface):
26
+ def __init__(self, leank_client: RpcInterface):
27
+ self.client = leank_client
28
+
29
+ async def close_and_wait(self) -> None:
30
+ await self.client.close_and_wait()
31
+
32
+ async def notify(self, mc: MethodCall) -> None:
33
+ await self.client.notify(mc)
34
+
35
+ async def request(
36
+ self, mc: MethodCall, fix_id: str | None = None
37
+ ) -> Awaitable[Response]:
38
+ if mc.method == "client/registerCapability":
39
+ return awaitable_error(ErrorCode.MethodNotFound)
40
+ return await self.client.request(mc, fix_id)
41
+
42
+
43
+ class LeankServer(RpcInterface):
44
+ def __init__(self, lake_server: RpcInterface):
45
+ self._lake_server = lake_server
46
+
47
+ async def close_and_wait(self) -> None:
48
+ await self._lake_server.close_and_wait()
49
+
50
+ async def notify(self, mc: MethodCall) -> None:
51
+ await self._lake_server.notify(mc)
52
+
53
+ async def request(
54
+ self, mc: MethodCall, fix_id: str | None = None
55
+ ) -> Awaitable[Response]:
56
+ if mc.method != "initialized":
57
+ return await self._lake_server.request(mc)
58
+ else:
59
+ init_params = mc.params if isinstance(mc.params, dict) else {}
60
+ lsp_root = get_uri_path(init_params, 'rootUri')
61
+ init_call = leank_init_call(lsp_root, text_doc_caps(init_params))
62
+ aw_response = await self._lake_server.request(init_call)
63
+ response = await aw_response
64
+ return awaitable(leank_init_response(response))
65
+
66
+
67
+ class LeankLakeChannel(RpcChannel):
68
+ def __init__(self, lake_channel: RpcChannel):
69
+ self._lake_channel = lake_channel
70
+ self._leank_server = LeankServer(lake_channel.proxy)
71
+
72
+ @property
73
+ def proxy(self) -> RpcInterface:
74
+ return self._leank_server
75
+
76
+ async def pump(self, leank_client: RpcInterface | None = None) -> None:
77
+ impl = None if leank_client is None else LakeClient(leank_client)
78
+ await self._lake_channel.pump(impl)
79
+
80
+
81
+ class LeankLakeFactory(RpcDirChannelFactory):
82
+ def __init__(self, lake_factory: RpcDirChannelFactory):
83
+ self._lake_factory = lake_factory
84
+
85
+ async def anew(self, work_root: Path) -> RpcChannel:
86
+ lake_channel = await self._lake_factory.anew(work_root)
87
+ return LeankLakeChannel(lake_channel)