python-fragments 0.5__tar.gz → 0.8__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.
- {python_fragments-0.5 → python_fragments-0.8}/PKG-INFO +2 -2
- {python_fragments-0.5 → python_fragments-0.8}/fragments/ast_nodes.py +123 -2
- {python_fragments-0.5 → python_fragments-0.8}/fragments/grammar.py +4 -4
- python_fragments-0.8/fragments/lsp/based_proxy.py +130 -0
- python_fragments-0.8/fragments/lsp/client_message_handlers/__init__.py +0 -0
- python_fragments-0.8/fragments/lsp/client_message_handlers/code_actions.py +34 -0
- python_fragments-0.8/fragments/lsp/client_message_handlers/completion.py +54 -0
- python_fragments-0.8/fragments/lsp/client_message_handlers/definition.py +44 -0
- python_fragments-0.8/fragments/lsp/client_message_handlers/diagnostics.py +45 -0
- python_fragments-0.8/fragments/lsp/client_message_handlers/document_highlight.py +36 -0
- python_fragments-0.8/fragments/lsp/client_message_handlers/document_symbols.py +52 -0
- python_fragments-0.8/fragments/lsp/client_message_handlers/folding_range.py +11 -0
- python_fragments-0.8/fragments/lsp/client_message_handlers/hover.py +32 -0
- python_fragments-0.8/fragments/lsp/client_message_handlers/inlay_hints.py +41 -0
- python_fragments-0.8/fragments/lsp/client_message_handlers/lifecycle.py +113 -0
- python_fragments-0.8/fragments/lsp/client_message_handlers/references.py +103 -0
- python_fragments-0.8/fragments/lsp/client_message_handlers/rename.py +62 -0
- python_fragments-0.8/fragments/lsp/client_message_handlers/semantic_tokens.py +38 -0
- python_fragments-0.8/fragments/lsp/client_message_handlers/signature_help.py +24 -0
- python_fragments-0.8/fragments/lsp/file_state.py +74 -0
- python_fragments-0.8/fragments/lsp/message_queue.py +123 -0
- python_fragments-0.8/fragments/lsp/pyright_notification_handlers/__init__.py +0 -0
- python_fragments-0.8/fragments/lsp/pyright_notification_handlers/capability.py +21 -0
- python_fragments-0.8/fragments/lsp/pyright_notification_handlers/configuration.py +25 -0
- python_fragments-0.8/fragments/lsp/pyright_notification_handlers/diagnostics.py +31 -0
- python_fragments-0.8/fragments/lsp/types.py +4 -0
- {python_fragments-0.5 → python_fragments-0.8}/pyproject.toml +3 -3
- {python_fragments-0.5 → python_fragments-0.8}/python_fragments.egg-info/PKG-INFO +2 -2
- python_fragments-0.8/python_fragments.egg-info/SOURCES.txt +43 -0
- {python_fragments-0.5 → python_fragments-0.8}/python_fragments.egg-info/entry_points.txt +1 -1
- {python_fragments-0.5 → python_fragments-0.8}/python_fragments.egg-info/requires.txt +1 -1
- {python_fragments-0.5 → python_fragments-0.8}/tests/test_grammar.py +3 -3
- {python_fragments-0.5 → python_fragments-0.8}/tests/test_source_map.py +50 -43
- python_fragments-0.5/fragments/lsp/completion.py +0 -79
- python_fragments-0.5/fragments/lsp/definition.py +0 -49
- python_fragments-0.5/fragments/lsp/file_state.py +0 -109
- python_fragments-0.5/fragments/lsp/hover.py +0 -44
- python_fragments-0.5/fragments/lsp/lifecycle.py +0 -114
- python_fragments-0.5/fragments/lsp/pyright.py +0 -94
- python_fragments-0.5/fragments/lsp/rename.py +0 -106
- python_fragments-0.5/fragments/lsp/semantic_tokens.py +0 -42
- python_fragments-0.5/fragments/lsp/server.py +0 -136
- python_fragments-0.5/python_fragments.egg-info/SOURCES.txt +0 -30
- python_fragments-0.5/tests/test_pyright.py +0 -126
- {python_fragments-0.5 → python_fragments-0.8}/README.md +0 -0
- {python_fragments-0.5 → python_fragments-0.8}/fragments/__init__.py +0 -0
- {python_fragments-0.5 → python_fragments-0.8}/fragments/cli.py +0 -0
- {python_fragments-0.5 → python_fragments-0.8}/fragments/html/__init__.py +0 -0
- {python_fragments-0.5 → python_fragments-0.8}/fragments/html/elements.py +0 -0
- {python_fragments-0.5 → python_fragments-0.8}/fragments/loader.py +0 -0
- {python_fragments-0.5 → python_fragments-0.8}/fragments/lsp/__init__.py +0 -0
- {python_fragments-0.5 → python_fragments-0.8}/fragments/source.py +0 -0
- {python_fragments-0.5 → python_fragments-0.8}/fragments/transpiler.py +0 -0
- {python_fragments-0.5 → python_fragments-0.8}/python_fragments.egg-info/dependency_links.txt +0 -0
- {python_fragments-0.5 → python_fragments-0.8}/python_fragments.egg-info/top_level.txt +0 -0
- {python_fragments-0.5 → python_fragments-0.8}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-fragments
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8
|
|
4
4
|
Summary: Modern HTML template rendering in Python
|
|
5
5
|
Author-email: The Running Algorithm <services@therunningalgorithm.info>
|
|
6
6
|
License: Proprietary
|
|
@@ -8,7 +8,7 @@ Requires-Python: >=3.12
|
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
Provides-Extra: lsp
|
|
10
10
|
Requires-Dist: basedpyright>=1.39.0; extra == "lsp"
|
|
11
|
-
Requires-Dist:
|
|
11
|
+
Requires-Dist: lsprotocol>=2025.0.0; extra == "lsp"
|
|
12
12
|
Provides-Extra: dev
|
|
13
13
|
Requires-Dist: pytest>=8.4.1; extra == "dev"
|
|
14
14
|
|
|
@@ -16,6 +16,7 @@ class ASTModule:
|
|
|
16
16
|
__template__: str = "from fragments.html.elements import el, sequence, comment\n{}"
|
|
17
17
|
|
|
18
18
|
def transpile(self, transpiled_start: int = 0) -> None:
|
|
19
|
+
"""Build transpiled outputs for the module."""
|
|
19
20
|
self.transpiled_start = transpiled_start
|
|
20
21
|
transpiled_start += len(self.__template__) - 2
|
|
21
22
|
for child in self.children:
|
|
@@ -26,9 +27,21 @@ class ASTModule:
|
|
|
26
27
|
self.transpiled_content = self.__template__.format(children)
|
|
27
28
|
self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
|
|
28
29
|
|
|
30
|
+
def map_offset(self, offset: int) -> int | None:
|
|
31
|
+
for owner in self.children:
|
|
32
|
+
if owner.source_start < offset <= owner.source_end:
|
|
33
|
+
return owner.map_offset(offset)
|
|
34
|
+
|
|
35
|
+
def unmap_offset(self, offset: int) -> int | None:
|
|
36
|
+
for owner in self.children:
|
|
37
|
+
if owner.transpiled_start < offset <= owner.transpiled_end:
|
|
38
|
+
return owner.unmap_offset(offset)
|
|
39
|
+
|
|
29
40
|
|
|
30
41
|
@dataclass(slots=True)
|
|
31
42
|
class ASTPython:
|
|
43
|
+
"""Build transpiled outputs for the vanilla Python code."""
|
|
44
|
+
|
|
32
45
|
source_start: int = field(compare=False)
|
|
33
46
|
source_end: int = field(compare=False)
|
|
34
47
|
content: str
|
|
@@ -42,6 +55,20 @@ class ASTPython:
|
|
|
42
55
|
self.transpiled_content = self.content
|
|
43
56
|
self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
|
|
44
57
|
|
|
58
|
+
def map_offset(self, offset: int) -> int | None:
|
|
59
|
+
if self.source_start < offset <= self.source_end:
|
|
60
|
+
specific_offset = offset - self.source_start
|
|
61
|
+
return self.transpiled_start + specific_offset
|
|
62
|
+
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
def unmap_offset(self, offset: int) -> int | None:
|
|
66
|
+
if self.transpiled_start < offset <= self.transpiled_end:
|
|
67
|
+
specific_offset = offset - self.transpiled_start
|
|
68
|
+
return self.source_start + specific_offset
|
|
69
|
+
|
|
70
|
+
return None
|
|
71
|
+
|
|
45
72
|
|
|
46
73
|
@dataclass(slots=True)
|
|
47
74
|
class ASTFragment:
|
|
@@ -67,6 +94,20 @@ class ASTFragment:
|
|
|
67
94
|
self.transpiled_content = self.__template__.format(",".join(child.transpiled_content for child in self.children))
|
|
68
95
|
self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
|
|
69
96
|
|
|
97
|
+
def map_offset(self, offset: int) -> int | None:
|
|
98
|
+
for owner in self.children:
|
|
99
|
+
if owner.source_start < offset <= owner.source_end:
|
|
100
|
+
return owner.map_offset(offset)
|
|
101
|
+
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
def unmap_offset(self, offset: int) -> int | None:
|
|
105
|
+
for owner in self.children:
|
|
106
|
+
if owner.transpiled_start < offset <= owner.transpiled_end:
|
|
107
|
+
return owner.unmap_offset(offset)
|
|
108
|
+
|
|
109
|
+
return None
|
|
110
|
+
|
|
70
111
|
|
|
71
112
|
@dataclass(slots=True)
|
|
72
113
|
class ASTHTMLElement:
|
|
@@ -77,7 +118,7 @@ class ASTHTMLElement:
|
|
|
77
118
|
attributes: dict[str, "ASTHTMLAttribute"]
|
|
78
119
|
if_attribute: "ASTInterpolation | None"
|
|
79
120
|
for_attribute: "ASTInterpolation | None"
|
|
80
|
-
children: Sequence["ASTHTMLElement | ASTHTMLText | ASTInterpolation"]
|
|
121
|
+
children: Sequence["ASTHTMLElement | ASTHTMLComment | ASTHTMLText | ASTInterpolation"]
|
|
81
122
|
one_line: bool
|
|
82
123
|
|
|
83
124
|
transpiled_content: str = field(init=False)
|
|
@@ -152,6 +193,40 @@ class ASTHTMLElement:
|
|
|
152
193
|
self.transpiled_content = self.__component_template__.format(self.name, children, attributes)
|
|
153
194
|
self.transpiled_end = start + len(self.transpiled_content)
|
|
154
195
|
|
|
196
|
+
def map_offset(self, offset: int) -> int | None:
|
|
197
|
+
for attribute in self.attributes.values():
|
|
198
|
+
if attribute.source_start < offset <= attribute.source_end:
|
|
199
|
+
return attribute.map_offset(offset)
|
|
200
|
+
|
|
201
|
+
for child in self.children:
|
|
202
|
+
if child.source_start < offset <= child.source_end:
|
|
203
|
+
return child.map_offset(offset)
|
|
204
|
+
|
|
205
|
+
if self.if_attribute is not None and self.if_attribute.source_start < offset <= self.if_attribute.source_end:
|
|
206
|
+
return self.if_attribute.map_offset(offset)
|
|
207
|
+
|
|
208
|
+
if self.for_attribute is not None and self.for_attribute.source_start < offset <= self.for_attribute.source_end:
|
|
209
|
+
return self.for_attribute.map_offset(offset)
|
|
210
|
+
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
def unmap_offset(self, offset: int) -> int | None:
|
|
214
|
+
for attribute in self.attributes.values():
|
|
215
|
+
if attribute.transpiled_start < offset <= attribute.transpiled_end:
|
|
216
|
+
return attribute.unmap_offset(offset)
|
|
217
|
+
|
|
218
|
+
for child in self.children:
|
|
219
|
+
if child.transpiled_start < offset <= child.transpiled_end:
|
|
220
|
+
return child.unmap_offset(offset)
|
|
221
|
+
|
|
222
|
+
if self.if_attribute is not None and self.if_attribute.transpiled_start < offset <= self.if_attribute.transpiled_end:
|
|
223
|
+
return self.if_attribute.unmap_offset(offset)
|
|
224
|
+
|
|
225
|
+
if self.for_attribute is not None and self.for_attribute.transpiled_start < offset <= self.for_attribute.transpiled_end:
|
|
226
|
+
return self.for_attribute.unmap_offset(offset)
|
|
227
|
+
|
|
228
|
+
return None
|
|
229
|
+
|
|
155
230
|
|
|
156
231
|
@dataclass(slots=True)
|
|
157
232
|
class ASTHTMLComment:
|
|
@@ -171,6 +246,12 @@ class ASTHTMLComment:
|
|
|
171
246
|
self.transpiled_content = self.__template__.format(self.content)
|
|
172
247
|
self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
|
|
173
248
|
|
|
249
|
+
def map_offset(self, offset: int) -> None:
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
def unmap_offset(self, offset: int) -> None:
|
|
253
|
+
return None
|
|
254
|
+
|
|
174
255
|
|
|
175
256
|
@dataclass(slots=True)
|
|
176
257
|
class ASTHTMLAttribute:
|
|
@@ -201,6 +282,24 @@ class ASTHTMLAttribute:
|
|
|
201
282
|
self.transpiled_content = self.__interpolation_template__.format(self.name, self.interpolation.transpiled_content)
|
|
202
283
|
self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
|
|
203
284
|
|
|
285
|
+
def map_offset(self, offset: int) -> int | None:
|
|
286
|
+
if self.interpolation is None:
|
|
287
|
+
return None
|
|
288
|
+
|
|
289
|
+
if self.interpolation.source_start < offset <= self.interpolation.source_end:
|
|
290
|
+
return self.interpolation.map_offset(offset)
|
|
291
|
+
|
|
292
|
+
return None
|
|
293
|
+
|
|
294
|
+
def unmap_offset(self, offset: int) -> int | None:
|
|
295
|
+
if self.interpolation is None:
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
if self.interpolation.transpiled_start < offset <= self.interpolation.transpiled_end:
|
|
299
|
+
return self.interpolation.unmap_offset(offset)
|
|
300
|
+
|
|
301
|
+
return None
|
|
302
|
+
|
|
204
303
|
|
|
205
304
|
@dataclass(slots=True)
|
|
206
305
|
class ASTHTMLText:
|
|
@@ -220,18 +319,40 @@ class ASTHTMLText:
|
|
|
220
319
|
self.transpiled_start = transpiled_start
|
|
221
320
|
self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
|
|
222
321
|
|
|
322
|
+
def map_offset(self, offset: int) -> None:
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
def unmap_offset(self, offset: int) -> None:
|
|
326
|
+
return None
|
|
327
|
+
|
|
223
328
|
|
|
224
329
|
@dataclass(slots=True)
|
|
225
330
|
class ASTInterpolation:
|
|
226
331
|
source_start: int = field(compare=False)
|
|
227
332
|
source_end: int = field(compare=False)
|
|
228
333
|
expression: str
|
|
334
|
+
leading_whitespace: int
|
|
335
|
+
trailing_whitespace: int
|
|
229
336
|
|
|
230
337
|
transpiled_content: str = field(init=False)
|
|
231
338
|
transpiled_start: int = field(init=False)
|
|
232
339
|
transpiled_end: int = field(init=False)
|
|
233
340
|
|
|
234
341
|
def transpile(self, transpiled_start: int) -> None:
|
|
235
|
-
self.transpiled_content = self.expression
|
|
342
|
+
self.transpiled_content = self.expression.strip()
|
|
236
343
|
self.transpiled_start = transpiled_start
|
|
237
344
|
self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
|
|
345
|
+
|
|
346
|
+
def map_offset(self, offset: int) -> int | None:
|
|
347
|
+
if self.source_start + 2 + self.leading_whitespace < offset <= self.source_end - 2 - self.trailing_whitespace:
|
|
348
|
+
specific_offset = offset - self.source_start - 2 - self.leading_whitespace
|
|
349
|
+
return self.transpiled_start + specific_offset
|
|
350
|
+
|
|
351
|
+
return None
|
|
352
|
+
|
|
353
|
+
def unmap_offset(self, offset: int) -> int | None:
|
|
354
|
+
if self.transpiled_start < offset <= self.transpiled_end:
|
|
355
|
+
specific_offset = offset - self.transpiled_start
|
|
356
|
+
return self.source_start + specific_offset + 2 + self.leading_whitespace
|
|
357
|
+
|
|
358
|
+
return None
|
|
@@ -7,7 +7,6 @@ from fragments.source import Source
|
|
|
7
7
|
PYTHON = r"([\s\S]*?)(?=<>)|[\s\S]*$"
|
|
8
8
|
IDENTIFIER = r"[a-zA-Z_][a-zA-Z0-9_]*"
|
|
9
9
|
STRING_CONTENTS = r"(.*?)(?=\")"
|
|
10
|
-
INTERPOLATION_EXPRESSION = r"(.*?)(?= }})"
|
|
11
10
|
HTML_IDENTIFIER = r"[a-zA-Z][a-zA-Z0-9_-]*"
|
|
12
11
|
HTML_TEXT = r"(.*?)(?=<)"
|
|
13
12
|
|
|
@@ -190,13 +189,14 @@ def expect_html_element(source: Source) -> tuple[Source, ASTHTMLElement]:
|
|
|
190
189
|
|
|
191
190
|
def expect_interpolation(source: Source) -> tuple[Source, ASTInterpolation]:
|
|
192
191
|
"""An interpolation block."""
|
|
192
|
+
INTERPOLATION_EXPRESSION = r"([\s\S]*?)(?= }})"
|
|
193
193
|
source_start = source.offset
|
|
194
194
|
source = expect_string(source, "{{")
|
|
195
|
-
source,
|
|
195
|
+
source, leading_whitespace = source.eat_whitespace()
|
|
196
196
|
source, expression = expect_regex(source, INTERPOLATION_EXPRESSION, "expression")
|
|
197
|
-
source,
|
|
197
|
+
source, trailing_whitespace = source.eat_whitespace()
|
|
198
198
|
source = expect_string(source, "}}")
|
|
199
|
-
return source, ASTInterpolation(source_start, source.offset, expression)
|
|
199
|
+
return source, ASTInterpolation(source_start, source.offset, expression, len(leading_whitespace), len(trailing_whitespace))
|
|
200
200
|
|
|
201
201
|
|
|
202
202
|
def expect_html_text(source: Source) -> tuple[Source, ASTHTMLText]:
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from asyncio.streams import StreamReader
|
|
2
|
+
from asyncio.transports import WriteTransport
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from asyncio.subprocess import Process
|
|
7
|
+
from typing import Callable
|
|
8
|
+
from lsprotocol import types
|
|
9
|
+
from fragments.lsp.file_state import FileState
|
|
10
|
+
from fragments.lsp.message_queue import MessageQueue
|
|
11
|
+
from fragments.lsp.types import HandlerFunc
|
|
12
|
+
|
|
13
|
+
_PYRIGHT_PROCESS: Process | None = None
|
|
14
|
+
_PYRIGHT: "MessageQueue | None" = None
|
|
15
|
+
_PYRIGHT_HANDLERS: dict[str, HandlerFunc] = {}
|
|
16
|
+
_PROXY: "MessageQueue | None" = None
|
|
17
|
+
_PROXY_HANDLERS: dict[str, HandlerFunc] = {}
|
|
18
|
+
|
|
19
|
+
FILE_STATES: dict[str, FileState] = {}
|
|
20
|
+
PARSE_ERRORS: dict[str, types.Diagnostic | None] = {}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def pyright() -> MessageQueue:
|
|
24
|
+
global _PYRIGHT
|
|
25
|
+
assert _PYRIGHT is not None
|
|
26
|
+
return _PYRIGHT
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def proxy() -> MessageQueue:
|
|
30
|
+
global _PROXY
|
|
31
|
+
assert _PROXY is not None
|
|
32
|
+
return _PROXY
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def handle_from_client(method: str) -> Callable[[HandlerFunc], HandlerFunc]:
|
|
36
|
+
"""Register a function to handle messages from the client."""
|
|
37
|
+
|
|
38
|
+
def decorator(func: HandlerFunc) -> HandlerFunc:
|
|
39
|
+
_PROXY_HANDLERS[method] = func
|
|
40
|
+
return func
|
|
41
|
+
|
|
42
|
+
return decorator
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def handle_from_pyright(method: str) -> Callable[[HandlerFunc], HandlerFunc]:
|
|
46
|
+
"""Register a function to handle messages from pyright."""
|
|
47
|
+
|
|
48
|
+
def decorator(func: HandlerFunc) -> HandlerFunc:
|
|
49
|
+
_PYRIGHT_HANDLERS[method] = func
|
|
50
|
+
return func
|
|
51
|
+
|
|
52
|
+
return decorator
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async def stop() -> None:
|
|
56
|
+
global _PYRIGHT_PROCESS
|
|
57
|
+
if _PYRIGHT_PROCESS is None:
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
_PYRIGHT_PROCESS.terminate()
|
|
61
|
+
try:
|
|
62
|
+
await asyncio.wait_for(_PYRIGHT_PROCESS.wait(), timeout=5.0)
|
|
63
|
+
except asyncio.TimeoutError:
|
|
64
|
+
_PYRIGHT_PROCESS.kill()
|
|
65
|
+
_PYRIGHT_PROCESS = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def start() -> None:
|
|
69
|
+
global _PYRIGHT_PROCESS, _PYRIGHT, _PROXY
|
|
70
|
+
|
|
71
|
+
loop: asyncio.AbstractEventLoop = asyncio.get_running_loop()
|
|
72
|
+
|
|
73
|
+
reader: asyncio.StreamReader = asyncio.StreamReader()
|
|
74
|
+
await loop.connect_read_pipe(lambda: asyncio.StreamReaderProtocol(reader), sys.stdin.buffer)
|
|
75
|
+
stdin: StreamReader = reader
|
|
76
|
+
transport, _ = await loop.connect_write_pipe(lambda: asyncio.BaseProtocol(), sys.stdout.buffer)
|
|
77
|
+
stdout: WriteTransport = transport
|
|
78
|
+
_PROXY = MessageQueue(stdin, stdout, _PROXY_HANDLERS)
|
|
79
|
+
|
|
80
|
+
bin_directory: str = os.path.dirname(sys.executable)
|
|
81
|
+
environment: dict[str, str] = dict[str, str](os.environ)
|
|
82
|
+
if sys.prefix != sys.base_prefix:
|
|
83
|
+
environment["VIRTUAL_ENV"] = sys.prefix
|
|
84
|
+
|
|
85
|
+
_PYRIGHT_PROCESS = await asyncio.create_subprocess_exec(
|
|
86
|
+
os.path.join(bin_directory, "basedpyright-langserver"),
|
|
87
|
+
"--stdio",
|
|
88
|
+
stdin=asyncio.subprocess.PIPE,
|
|
89
|
+
stdout=asyncio.subprocess.PIPE,
|
|
90
|
+
stderr=sys.stderr.buffer,
|
|
91
|
+
env=environment,
|
|
92
|
+
)
|
|
93
|
+
pyright_stdin = _PYRIGHT_PROCESS.stdin
|
|
94
|
+
pyright_stdout = _PYRIGHT_PROCESS.stdout
|
|
95
|
+
assert pyright_stdin is not None
|
|
96
|
+
assert pyright_stdout is not None
|
|
97
|
+
|
|
98
|
+
_PYRIGHT = MessageQueue(pyright_stdout, pyright_stdin, _PYRIGHT_HANDLERS)
|
|
99
|
+
|
|
100
|
+
pyright_task = asyncio.create_task(_PYRIGHT.read_loop())
|
|
101
|
+
await _PROXY.read_loop()
|
|
102
|
+
await stop()
|
|
103
|
+
pyright_task.cancel()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def main() -> None:
|
|
107
|
+
import asyncio
|
|
108
|
+
from fragments.lsp.client_message_handlers import ( # noqa: F401
|
|
109
|
+
completion,
|
|
110
|
+
definition,
|
|
111
|
+
diagnostics,
|
|
112
|
+
document_highlight,
|
|
113
|
+
document_symbols,
|
|
114
|
+
folding_range,
|
|
115
|
+
hover,
|
|
116
|
+
inlay_hints,
|
|
117
|
+
lifecycle,
|
|
118
|
+
references,
|
|
119
|
+
rename,
|
|
120
|
+
semantic_tokens,
|
|
121
|
+
signature_help,
|
|
122
|
+
code_actions,
|
|
123
|
+
)
|
|
124
|
+
from fragments.lsp.pyright_notification_handlers import ( # noqa: F401
|
|
125
|
+
capability,
|
|
126
|
+
configuration,
|
|
127
|
+
diagnostics as pyright_diagnostics,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
asyncio.run(start())
|
|
File without changes
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from lsprotocol import types
|
|
2
|
+
from lsprotocol.types import REQUESTS, NOTIFICATIONS
|
|
3
|
+
from typing import cast
|
|
4
|
+
from fragments.lsp import based_proxy
|
|
5
|
+
from fragments.lsp.based_proxy import handle_from_client
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@handle_from_client(types.TEXT_DOCUMENT_CODE_ACTION)
|
|
9
|
+
async def code_action(message: REQUESTS | NOTIFICATIONS) -> None:
|
|
10
|
+
request = cast(types.CodeActionRequest, message)
|
|
11
|
+
original_id = request.id
|
|
12
|
+
file_state = based_proxy.FILE_STATES.get(request.params.text_document.uri)
|
|
13
|
+
|
|
14
|
+
if file_state is None or file_state.vanilla:
|
|
15
|
+
based_proxy.proxy().respond(original_id, await based_proxy.pyright().request(request))
|
|
16
|
+
return
|
|
17
|
+
|
|
18
|
+
mapped_range = file_state.map_range(request.params.range)
|
|
19
|
+
if mapped_range is None:
|
|
20
|
+
based_proxy.proxy().respond(original_id, types.CodeActionResponse(id=original_id, result=[]))
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
request.params.range = mapped_range
|
|
24
|
+
response = cast(types.CodeActionResponse, await based_proxy.pyright().request(request))
|
|
25
|
+
|
|
26
|
+
for action in response.result or []:
|
|
27
|
+
if isinstance(action, types.CodeAction) and action.edit is not None:
|
|
28
|
+
for change in action.edit.document_changes or []:
|
|
29
|
+
if isinstance(change, types.TextDocumentEdit) and (state := based_proxy.FILE_STATES.get(change.text_document.uri)):
|
|
30
|
+
for edit in change.edits:
|
|
31
|
+
if not isinstance(edit, types.SnippetTextEdit) and (r := state.unmap_range(edit.range)) is not None:
|
|
32
|
+
edit.range = r
|
|
33
|
+
|
|
34
|
+
based_proxy.proxy().respond(original_id, response)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from lsprotocol import types
|
|
2
|
+
from lsprotocol.types import REQUESTS, NOTIFICATIONS
|
|
3
|
+
from typing import cast
|
|
4
|
+
from fragments.lsp import based_proxy
|
|
5
|
+
from fragments.lsp.based_proxy import handle_from_client
|
|
6
|
+
from fragments.lsp.file_state import FileState
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _remap_item(item: types.CompletionItem, file_state: FileState) -> bool:
|
|
10
|
+
if isinstance(item.text_edit, types.InsertReplaceEdit):
|
|
11
|
+
insert = file_state.unmap_range(item.text_edit.insert)
|
|
12
|
+
replace = file_state.unmap_range(item.text_edit.replace)
|
|
13
|
+
if insert is None or replace is None:
|
|
14
|
+
return False
|
|
15
|
+
item.text_edit.insert, item.text_edit.replace = insert, replace
|
|
16
|
+
elif item.text_edit is not None:
|
|
17
|
+
if (r := file_state.unmap_range(item.text_edit.range)) is None:
|
|
18
|
+
return False
|
|
19
|
+
item.text_edit.range = r
|
|
20
|
+
if item.additional_text_edits:
|
|
21
|
+
remapped_edits = []
|
|
22
|
+
for edit in item.additional_text_edits:
|
|
23
|
+
if (r := file_state.unmap_range(edit.range)) is not None:
|
|
24
|
+
edit.range = r
|
|
25
|
+
remapped_edits.append(edit)
|
|
26
|
+
item.additional_text_edits = remapped_edits
|
|
27
|
+
return True
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@handle_from_client(types.TEXT_DOCUMENT_COMPLETION)
|
|
31
|
+
async def completion(message: REQUESTS | NOTIFICATIONS) -> None:
|
|
32
|
+
request = cast(types.CompletionRequest, message)
|
|
33
|
+
original_id = request.id
|
|
34
|
+
file_state = based_proxy.FILE_STATES.get(request.params.text_document.uri)
|
|
35
|
+
|
|
36
|
+
if file_state is None or file_state.vanilla:
|
|
37
|
+
based_proxy.proxy().respond(original_id, await based_proxy.pyright().request(request))
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
if (mapped := file_state.map_position(request.params.position)) is None:
|
|
41
|
+
return
|
|
42
|
+
request.params.position = mapped
|
|
43
|
+
|
|
44
|
+
response = cast(types.CompletionResponse, await based_proxy.pyright().request(request))
|
|
45
|
+
items = response.result.items if isinstance(response.result, types.CompletionList) else list(response.result or [])
|
|
46
|
+
remapped = [item for item in items if _remap_item(item, file_state)]
|
|
47
|
+
response.result = remapped
|
|
48
|
+
based_proxy.proxy().respond(original_id, response)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@handle_from_client(types.COMPLETION_ITEM_RESOLVE)
|
|
52
|
+
async def completion_item_resolve(message: REQUESTS | NOTIFICATIONS) -> None:
|
|
53
|
+
request = cast(types.CompletionResolveRequest, message)
|
|
54
|
+
based_proxy.proxy().respond(request.id, cast(types.CompletionResolveResponse, await based_proxy.pyright().request(request)))
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from lsprotocol import types
|
|
2
|
+
from lsprotocol.types import REQUESTS, NOTIFICATIONS
|
|
3
|
+
from typing import cast
|
|
4
|
+
from fragments.lsp import based_proxy
|
|
5
|
+
from fragments.lsp.based_proxy import handle_from_client
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@handle_from_client(types.TEXT_DOCUMENT_DEFINITION)
|
|
9
|
+
async def definition(message: REQUESTS | NOTIFICATIONS) -> None:
|
|
10
|
+
request = cast(types.DefinitionRequest, message)
|
|
11
|
+
original_id = request.id
|
|
12
|
+
file_state = based_proxy.FILE_STATES.get(request.params.text_document.uri)
|
|
13
|
+
|
|
14
|
+
if file_state is None or file_state.vanilla:
|
|
15
|
+
based_proxy.proxy().respond(original_id, await based_proxy.pyright().request(request))
|
|
16
|
+
return
|
|
17
|
+
|
|
18
|
+
mapped_position = file_state.map_position(request.params.position)
|
|
19
|
+
if mapped_position is None:
|
|
20
|
+
based_proxy.proxy().respond(original_id, types.DefinitionResponse(id=original_id, result=None))
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
request.params.position = mapped_position
|
|
24
|
+
response = cast(types.DefinitionResponse, await based_proxy.pyright().request(request))
|
|
25
|
+
|
|
26
|
+
if isinstance(response.result, list):
|
|
27
|
+
remapped: list[types.Location] = []
|
|
28
|
+
for location in cast(list[types.Location], response.result):
|
|
29
|
+
target_state = based_proxy.FILE_STATES.get(location.uri)
|
|
30
|
+
if target_state is not None:
|
|
31
|
+
new_range = target_state.unmap_range(location.range)
|
|
32
|
+
if new_range is None:
|
|
33
|
+
continue
|
|
34
|
+
location.range = new_range
|
|
35
|
+
remapped.append(location)
|
|
36
|
+
response.result = remapped
|
|
37
|
+
elif isinstance(response.result, types.Location):
|
|
38
|
+
target_state = based_proxy.FILE_STATES.get(response.result.uri)
|
|
39
|
+
if target_state is not None:
|
|
40
|
+
new_range = target_state.unmap_range(response.result.range)
|
|
41
|
+
if new_range is not None:
|
|
42
|
+
response.result.range = new_range
|
|
43
|
+
|
|
44
|
+
based_proxy.proxy().respond(original_id, response)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from lsprotocol import types
|
|
2
|
+
from lsprotocol.types import REQUESTS, NOTIFICATIONS
|
|
3
|
+
from typing import cast
|
|
4
|
+
from fragments.lsp import based_proxy
|
|
5
|
+
from fragments.lsp.based_proxy import handle_from_client
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@handle_from_client(types.TEXT_DOCUMENT_DIAGNOSTIC)
|
|
9
|
+
async def text_document_diagnostic(message: REQUESTS | NOTIFICATIONS) -> None:
|
|
10
|
+
request = cast(types.DocumentDiagnosticRequest, message)
|
|
11
|
+
original_id = request.id
|
|
12
|
+
uri = request.params.text_document.uri
|
|
13
|
+
parse_error = based_proxy.PARSE_ERRORS.get(uri)
|
|
14
|
+
file_state = based_proxy.FILE_STATES.get(uri)
|
|
15
|
+
|
|
16
|
+
if parse_error is not None:
|
|
17
|
+
based_proxy.proxy().respond(
|
|
18
|
+
original_id,
|
|
19
|
+
types.DocumentDiagnosticResponse(
|
|
20
|
+
id=original_id,
|
|
21
|
+
result=types.RelatedFullDocumentDiagnosticReport(kind="full", items=[parse_error]),
|
|
22
|
+
),
|
|
23
|
+
)
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
response = cast(types.DocumentDiagnosticResponse, await based_proxy.pyright().request(request))
|
|
27
|
+
|
|
28
|
+
if file_state is None or file_state.vanilla or response.result is None:
|
|
29
|
+
based_proxy.proxy().respond(original_id, response)
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
if isinstance(response.result, types.RelatedUnchangedDocumentDiagnosticReport):
|
|
33
|
+
based_proxy.proxy().respond(original_id, response)
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
remapped_items: list[types.Diagnostic] = []
|
|
37
|
+
for diagnostic in response.result.items or []:
|
|
38
|
+
remapped_range = file_state.unmap_range(diagnostic.range)
|
|
39
|
+
if remapped_range is None:
|
|
40
|
+
continue
|
|
41
|
+
diagnostic.range = remapped_range
|
|
42
|
+
remapped_items.append(diagnostic)
|
|
43
|
+
|
|
44
|
+
response.result.items = remapped_items
|
|
45
|
+
based_proxy.proxy().respond(original_id, response)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from lsprotocol import types
|
|
2
|
+
from lsprotocol.types import REQUESTS, NOTIFICATIONS
|
|
3
|
+
from typing import cast
|
|
4
|
+
from fragments.lsp import based_proxy
|
|
5
|
+
from fragments.lsp.based_proxy import handle_from_client
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@handle_from_client(types.TEXT_DOCUMENT_DOCUMENT_HIGHLIGHT)
|
|
9
|
+
async def document_highlight(message: REQUESTS | NOTIFICATIONS) -> None:
|
|
10
|
+
request = cast(types.DocumentHighlightRequest, message)
|
|
11
|
+
original_id = request.id
|
|
12
|
+
file_state = based_proxy.FILE_STATES.get(request.params.text_document.uri)
|
|
13
|
+
|
|
14
|
+
if file_state is None or file_state.vanilla:
|
|
15
|
+
based_proxy.proxy().respond(original_id, await based_proxy.pyright().request(request))
|
|
16
|
+
return
|
|
17
|
+
|
|
18
|
+
mapped_position = file_state.map_position(request.params.position)
|
|
19
|
+
if mapped_position is None:
|
|
20
|
+
based_proxy.proxy().respond(original_id, types.DocumentHighlightResponse(id=original_id, result=None))
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
request.params.position = mapped_position
|
|
24
|
+
response = cast(types.DocumentHighlightResponse, await based_proxy.pyright().request(request))
|
|
25
|
+
|
|
26
|
+
if response.result:
|
|
27
|
+
remapped: list[types.DocumentHighlight] = []
|
|
28
|
+
for highlight in response.result:
|
|
29
|
+
new_range = file_state.unmap_range(highlight.range)
|
|
30
|
+
if new_range is None:
|
|
31
|
+
continue
|
|
32
|
+
highlight.range = new_range
|
|
33
|
+
remapped.append(highlight)
|
|
34
|
+
response.result = remapped
|
|
35
|
+
|
|
36
|
+
based_proxy.proxy().respond(original_id, response)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from lsprotocol import types
|
|
2
|
+
from lsprotocol.types import REQUESTS, NOTIFICATIONS
|
|
3
|
+
from typing import cast
|
|
4
|
+
from fragments.lsp import based_proxy
|
|
5
|
+
from fragments.lsp.based_proxy import handle_from_client
|
|
6
|
+
from fragments.lsp.file_state import FileState
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _remap_document_symbols(symbols: list[types.DocumentSymbol], file_state: FileState) -> list[types.DocumentSymbol]:
|
|
10
|
+
remapped: list[types.DocumentSymbol] = []
|
|
11
|
+
for symbol in symbols:
|
|
12
|
+
new_range = file_state.unmap_range(symbol.range)
|
|
13
|
+
new_selection_range = file_state.unmap_range(symbol.selection_range)
|
|
14
|
+
if new_range is None or new_selection_range is None:
|
|
15
|
+
continue
|
|
16
|
+
symbol.range = new_range
|
|
17
|
+
symbol.selection_range = new_selection_range
|
|
18
|
+
if symbol.children:
|
|
19
|
+
symbol.children = _remap_document_symbols(list(symbol.children), file_state)
|
|
20
|
+
remapped.append(symbol)
|
|
21
|
+
return remapped
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@handle_from_client(types.TEXT_DOCUMENT_DOCUMENT_SYMBOL)
|
|
25
|
+
async def document_symbol(message: REQUESTS | NOTIFICATIONS) -> None:
|
|
26
|
+
request = cast(types.DocumentSymbolRequest, message)
|
|
27
|
+
original_id = request.id
|
|
28
|
+
file_state = based_proxy.FILE_STATES.get(request.params.text_document.uri)
|
|
29
|
+
|
|
30
|
+
if file_state is None or file_state.vanilla:
|
|
31
|
+
based_proxy.proxy().respond(original_id, await based_proxy.pyright().request(request))
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
response = cast(types.DocumentSymbolResponse, await based_proxy.pyright().request(request))
|
|
35
|
+
|
|
36
|
+
if not response.result:
|
|
37
|
+
based_proxy.proxy().respond(original_id, response)
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
if isinstance(response.result[0], types.SymbolInformation):
|
|
41
|
+
remapped_info: list[types.SymbolInformation] = []
|
|
42
|
+
for symbol in cast(list[types.SymbolInformation], response.result):
|
|
43
|
+
new_range = file_state.unmap_range(symbol.location.range)
|
|
44
|
+
if new_range is None:
|
|
45
|
+
continue
|
|
46
|
+
symbol.location.range = new_range
|
|
47
|
+
remapped_info.append(symbol)
|
|
48
|
+
response.result = remapped_info
|
|
49
|
+
else:
|
|
50
|
+
response.result = _remap_document_symbols(cast(list[types.DocumentSymbol], response.result), file_state)
|
|
51
|
+
|
|
52
|
+
based_proxy.proxy().respond(original_id, response)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from lsprotocol import types
|
|
2
|
+
from lsprotocol.types import REQUESTS, NOTIFICATIONS
|
|
3
|
+
from typing import cast
|
|
4
|
+
from fragments.lsp import based_proxy
|
|
5
|
+
from fragments.lsp.based_proxy import handle_from_client
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@handle_from_client(types.TEXT_DOCUMENT_FOLDING_RANGE)
|
|
9
|
+
async def folding_range(message: REQUESTS | NOTIFICATIONS) -> None:
|
|
10
|
+
request = cast(types.FoldingRangeRequest, message)
|
|
11
|
+
based_proxy.proxy().respond(request.id, cast(types.FoldingRangeResponse, await based_proxy.pyright().request(request)))
|