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.
Files changed (56) hide show
  1. {python_fragments-0.5 → python_fragments-0.8}/PKG-INFO +2 -2
  2. {python_fragments-0.5 → python_fragments-0.8}/fragments/ast_nodes.py +123 -2
  3. {python_fragments-0.5 → python_fragments-0.8}/fragments/grammar.py +4 -4
  4. python_fragments-0.8/fragments/lsp/based_proxy.py +130 -0
  5. python_fragments-0.8/fragments/lsp/client_message_handlers/__init__.py +0 -0
  6. python_fragments-0.8/fragments/lsp/client_message_handlers/code_actions.py +34 -0
  7. python_fragments-0.8/fragments/lsp/client_message_handlers/completion.py +54 -0
  8. python_fragments-0.8/fragments/lsp/client_message_handlers/definition.py +44 -0
  9. python_fragments-0.8/fragments/lsp/client_message_handlers/diagnostics.py +45 -0
  10. python_fragments-0.8/fragments/lsp/client_message_handlers/document_highlight.py +36 -0
  11. python_fragments-0.8/fragments/lsp/client_message_handlers/document_symbols.py +52 -0
  12. python_fragments-0.8/fragments/lsp/client_message_handlers/folding_range.py +11 -0
  13. python_fragments-0.8/fragments/lsp/client_message_handlers/hover.py +32 -0
  14. python_fragments-0.8/fragments/lsp/client_message_handlers/inlay_hints.py +41 -0
  15. python_fragments-0.8/fragments/lsp/client_message_handlers/lifecycle.py +113 -0
  16. python_fragments-0.8/fragments/lsp/client_message_handlers/references.py +103 -0
  17. python_fragments-0.8/fragments/lsp/client_message_handlers/rename.py +62 -0
  18. python_fragments-0.8/fragments/lsp/client_message_handlers/semantic_tokens.py +38 -0
  19. python_fragments-0.8/fragments/lsp/client_message_handlers/signature_help.py +24 -0
  20. python_fragments-0.8/fragments/lsp/file_state.py +74 -0
  21. python_fragments-0.8/fragments/lsp/message_queue.py +123 -0
  22. python_fragments-0.8/fragments/lsp/pyright_notification_handlers/__init__.py +0 -0
  23. python_fragments-0.8/fragments/lsp/pyright_notification_handlers/capability.py +21 -0
  24. python_fragments-0.8/fragments/lsp/pyright_notification_handlers/configuration.py +25 -0
  25. python_fragments-0.8/fragments/lsp/pyright_notification_handlers/diagnostics.py +31 -0
  26. python_fragments-0.8/fragments/lsp/types.py +4 -0
  27. {python_fragments-0.5 → python_fragments-0.8}/pyproject.toml +3 -3
  28. {python_fragments-0.5 → python_fragments-0.8}/python_fragments.egg-info/PKG-INFO +2 -2
  29. python_fragments-0.8/python_fragments.egg-info/SOURCES.txt +43 -0
  30. {python_fragments-0.5 → python_fragments-0.8}/python_fragments.egg-info/entry_points.txt +1 -1
  31. {python_fragments-0.5 → python_fragments-0.8}/python_fragments.egg-info/requires.txt +1 -1
  32. {python_fragments-0.5 → python_fragments-0.8}/tests/test_grammar.py +3 -3
  33. {python_fragments-0.5 → python_fragments-0.8}/tests/test_source_map.py +50 -43
  34. python_fragments-0.5/fragments/lsp/completion.py +0 -79
  35. python_fragments-0.5/fragments/lsp/definition.py +0 -49
  36. python_fragments-0.5/fragments/lsp/file_state.py +0 -109
  37. python_fragments-0.5/fragments/lsp/hover.py +0 -44
  38. python_fragments-0.5/fragments/lsp/lifecycle.py +0 -114
  39. python_fragments-0.5/fragments/lsp/pyright.py +0 -94
  40. python_fragments-0.5/fragments/lsp/rename.py +0 -106
  41. python_fragments-0.5/fragments/lsp/semantic_tokens.py +0 -42
  42. python_fragments-0.5/fragments/lsp/server.py +0 -136
  43. python_fragments-0.5/python_fragments.egg-info/SOURCES.txt +0 -30
  44. python_fragments-0.5/tests/test_pyright.py +0 -126
  45. {python_fragments-0.5 → python_fragments-0.8}/README.md +0 -0
  46. {python_fragments-0.5 → python_fragments-0.8}/fragments/__init__.py +0 -0
  47. {python_fragments-0.5 → python_fragments-0.8}/fragments/cli.py +0 -0
  48. {python_fragments-0.5 → python_fragments-0.8}/fragments/html/__init__.py +0 -0
  49. {python_fragments-0.5 → python_fragments-0.8}/fragments/html/elements.py +0 -0
  50. {python_fragments-0.5 → python_fragments-0.8}/fragments/loader.py +0 -0
  51. {python_fragments-0.5 → python_fragments-0.8}/fragments/lsp/__init__.py +0 -0
  52. {python_fragments-0.5 → python_fragments-0.8}/fragments/source.py +0 -0
  53. {python_fragments-0.5 → python_fragments-0.8}/fragments/transpiler.py +0 -0
  54. {python_fragments-0.5 → python_fragments-0.8}/python_fragments.egg-info/dependency_links.txt +0 -0
  55. {python_fragments-0.5 → python_fragments-0.8}/python_fragments.egg-info/top_level.txt +0 -0
  56. {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.5
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: pygls>=2.0.0; extra == "lsp"
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, _ = source.eat_whitespace()
195
+ source, leading_whitespace = source.eat_whitespace()
196
196
  source, expression = expect_regex(source, INTERPOLATION_EXPRESSION, "expression")
197
- source, _ = source.eat_whitespace()
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())
@@ -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)))