python-fragments 0.1__py3-none-any.whl

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.
fragments/__init__.py ADDED
File without changes
fragments/ast_nodes.py ADDED
@@ -0,0 +1,237 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Sequence
3
+
4
+
5
+ @dataclass(slots=True)
6
+ class ASTModule:
7
+ source_start: int = field(compare=False)
8
+ source_end: int = field(compare=False)
9
+
10
+ children: list["ASTPython | ASTFragment"]
11
+
12
+ transpiled_content: str = field(init=False)
13
+ transpiled_start: int = field(init=False)
14
+ transpiled_end: int = field(init=False)
15
+
16
+ __template__: str = "from fragments.html.elements import el, sequence, comment\n{}"
17
+
18
+ def transpile(self, transpiled_start: int = 0) -> None:
19
+ self.transpiled_start = transpiled_start
20
+ transpiled_start += len(self.__template__) - 2
21
+ for child in self.children:
22
+ child.transpile(transpiled_start)
23
+ transpiled_start = child.transpiled_end
24
+
25
+ children: str = "".join(child.transpiled_content for child in self.children)
26
+ self.transpiled_content = self.__template__.format(children)
27
+ self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
28
+
29
+
30
+ @dataclass(slots=True)
31
+ class ASTPython:
32
+ source_start: int = field(compare=False)
33
+ source_end: int = field(compare=False)
34
+ content: str
35
+
36
+ transpiled_content: str = field(init=False)
37
+ transpiled_start: int = field(init=False)
38
+ transpiled_end: int = field(init=False)
39
+
40
+ def transpile(self, transpiled_start: int) -> None:
41
+ self.transpiled_start = transpiled_start
42
+ self.transpiled_content = self.content
43
+ self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
44
+
45
+
46
+ @dataclass(slots=True)
47
+ class ASTFragment:
48
+ source_start: int = field(compare=False)
49
+ source_end: int = field(compare=False)
50
+
51
+ children: list["ASTHTMLElement | ASTHTMLComment | ASTHTMLText | ASTInterpolation"]
52
+
53
+ transpiled_content: str = field(init=False)
54
+ transpiled_start: int = field(init=False)
55
+ transpiled_end: int = field(init=False)
56
+
57
+ __template__: str = """sequence([{}])"""
58
+
59
+ def transpile(self, transpiled_start: int) -> None:
60
+ self.transpiled_start = transpiled_start
61
+ transpiled_start += 10
62
+ for child in self.children:
63
+ child.transpile(transpiled_start)
64
+ transpiled_start = child.transpiled_end + 1
65
+ transpiled_start -= 1
66
+
67
+ self.transpiled_content = self.__template__.format(",".join(child.transpiled_content for child in self.children))
68
+ self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
69
+
70
+
71
+ @dataclass(slots=True)
72
+ class ASTHTMLElement:
73
+ source_start: int = field(compare=False)
74
+ source_end: int = field(compare=False)
75
+
76
+ name: str
77
+ attributes: dict[str, "ASTHTMLAttribute"]
78
+ if_attribute: "ASTInterpolation | None"
79
+ for_attribute: "ASTInterpolation | None"
80
+ children: Sequence["ASTHTMLElement | ASTHTMLText | ASTInterpolation"]
81
+ one_line: bool
82
+
83
+ transpiled_content: str = field(init=False)
84
+ transpiled_start: int = field(init=False)
85
+ transpiled_end: int = field(init=False)
86
+
87
+ __for_template__: str = """sequence([{} for {}])"""
88
+ __if_template__: str = """{} if {} else ''"""
89
+ __element_template__: str = """el("{}",[{}],oneline={},{})"""
90
+ __component_template__: str = """{}([{}],{})"""
91
+
92
+ def transpile(self, transpiled_start: int) -> None:
93
+ self.transpiled_start = transpiled_start
94
+
95
+ if self.for_attribute is not None:
96
+ transpiled_start += 10
97
+
98
+ if self.name[0].capitalize() == self.name[0]:
99
+ self._transpile_component_call(transpiled_start)
100
+ else:
101
+ self._transpile_element_call(transpiled_start)
102
+
103
+ if self.if_attribute is not None:
104
+ self.if_attribute.transpile(self.transpiled_end + 4)
105
+ self.transpiled_content = self.__if_template__.format(self.transpiled_content, self.if_attribute.transpiled_content)
106
+ self.transpiled_end = self.if_attribute.transpiled_end + 8
107
+ elif self.for_attribute is not None:
108
+ self.for_attribute.transpile(self.transpiled_end + 5)
109
+ self.transpiled_content = self.__for_template__.format(self.transpiled_content, self.for_attribute.transpiled_content)
110
+ self.transpiled_end = self.for_attribute.transpiled_end + 2
111
+
112
+ def _transpile_element_call(self, start: int) -> None:
113
+ transpiled_start = start + len(self.name) + 7
114
+ for child in self.children:
115
+ child.transpile(transpiled_start)
116
+ transpiled_start = child.transpiled_end + 1
117
+ transpiled_start -= 1
118
+ children = ",".join(child.transpiled_content for child in self.children)
119
+
120
+ oneline_offset = len(str(self.one_line))
121
+
122
+ transpiled_start += 10 + oneline_offset + 1
123
+
124
+ for attribute in self.attributes.values():
125
+ attribute.transpile(transpiled_start)
126
+ transpiled_start = attribute.transpiled_end + 1
127
+ transpiled_start -= 1
128
+
129
+ attributes = ",".join(attribute.transpiled_content for attribute in self.attributes.values())
130
+ self.transpiled_content = self.__element_template__.format(self.name, children, self.one_line, attributes)
131
+ self.transpiled_end = start + len(self.transpiled_content)
132
+
133
+ def _transpile_component_call(self, start: int) -> None:
134
+ transpiled_start = start + len(self.name) + 2
135
+ for child in self.children:
136
+ child.transpile(transpiled_start)
137
+ transpiled_start = child.transpiled_end + 1
138
+ transpiled_start -= 1
139
+ children = ",".join(child.transpiled_content for child in self.children)
140
+
141
+ if len(self.attributes) == 0:
142
+ self.transpiled_content = self.__component_template__.format(self.name, children, "")
143
+ self.transpiled_end = start + len(self.transpiled_content)
144
+ return
145
+
146
+ transpiled_start += 2
147
+ for attribute in self.attributes.values():
148
+ attribute.transpile(transpiled_start)
149
+ transpiled_start = attribute.transpiled_end + 1
150
+
151
+ attributes = ",".join(attribute.transpiled_content for attribute in self.attributes.values())
152
+ self.transpiled_content = self.__component_template__.format(self.name, children, attributes)
153
+ self.transpiled_end = start + len(self.transpiled_content)
154
+
155
+
156
+ @dataclass(slots=True)
157
+ class ASTHTMLComment:
158
+ source_start: int = field(compare=False)
159
+ source_end: int = field(compare=False)
160
+
161
+ content: str
162
+
163
+ transpiled_content: str = field(init=False)
164
+ transpiled_start: int = field(init=False)
165
+ transpiled_end: int = field(init=False)
166
+
167
+ __template__: str = """comment("{}")"""
168
+
169
+ def transpile(self, transpiled_start: int) -> None:
170
+ self.transpiled_start = transpiled_start
171
+ self.transpiled_content = self.__template__.format(self.content)
172
+ self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
173
+
174
+
175
+ @dataclass(slots=True)
176
+ class ASTHTMLAttribute:
177
+ source_start: int = field(compare=False)
178
+ source_end: int = field(compare=False)
179
+
180
+ name: str
181
+ value: str | None
182
+ interpolation: "ASTInterpolation | None"
183
+
184
+ transpiled_content: str = field(init=False)
185
+ transpiled_start: int = field(init=False)
186
+ transpiled_end: int = field(init=False)
187
+
188
+ __value_template__: str = '{}="{}"'
189
+ __interpolation_template__: str = "{}={}"
190
+
191
+ def transpile(self, transpiled_start: int) -> None:
192
+ self.transpiled_start = transpiled_start
193
+
194
+ if self.value is not None:
195
+ self.transpiled_content = self.__value_template__.format(self.name, self.value)
196
+ self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
197
+ return
198
+
199
+ assert self.interpolation is not None
200
+ self.interpolation.transpile(self.transpiled_start + len(self.name) + 1)
201
+ self.transpiled_content = self.__interpolation_template__.format(self.name, self.interpolation.transpiled_content)
202
+ self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
203
+
204
+
205
+ @dataclass(slots=True)
206
+ class ASTHTMLText:
207
+ source_start: int = field(compare=False)
208
+ source_end: int = field(compare=False)
209
+
210
+ text: str
211
+
212
+ transpiled_content: str = field(init=False)
213
+ transpiled_start: int = field(init=False)
214
+ transpiled_end: int = field(init=False)
215
+
216
+ __template__: str = '"{}"'
217
+
218
+ def transpile(self, transpiled_start: int) -> None:
219
+ self.transpiled_content = self.__template__.format(self.text)
220
+ self.transpiled_start = transpiled_start
221
+ self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
222
+
223
+
224
+ @dataclass(slots=True)
225
+ class ASTInterpolation:
226
+ source_start: int = field(compare=False)
227
+ source_end: int = field(compare=False)
228
+ expression: str
229
+
230
+ transpiled_content: str = field(init=False)
231
+ transpiled_start: int = field(init=False)
232
+ transpiled_end: int = field(init=False)
233
+
234
+ def transpile(self, transpiled_start: int) -> None:
235
+ self.transpiled_content = self.expression
236
+ self.transpiled_start = transpiled_start
237
+ self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
fragments/cli.py ADDED
@@ -0,0 +1,20 @@
1
+ import argparse
2
+
3
+ from fragments import transpiler
4
+
5
+
6
+ def parse_args() -> argparse.Namespace:
7
+ parser = argparse.ArgumentParser("fragments", description="Fragments transpilation CLI tool")
8
+ parser.add_argument("--input", type=str, required=True)
9
+ return parser.parse_args()
10
+
11
+
12
+ def main():
13
+ args = parse_args()
14
+ with open(args.input, "r") as f:
15
+ source = f.read()
16
+ print(transpiler.transpile(source))
17
+
18
+
19
+ if __name__ == "__main__":
20
+ main()
fragments/grammar.py ADDED
@@ -0,0 +1,206 @@
1
+ from re import Match
2
+ import re
3
+
4
+ from fragments.ast_nodes import ASTFragment, ASTHTMLAttribute, ASTHTMLComment, ASTHTMLElement, ASTHTMLText, ASTInterpolation, ASTModule, ASTPython
5
+ from fragments.source import Source
6
+
7
+ PYTHON = r"([\s\S]*?)(?=<>)|[\s\S]*$"
8
+ IDENTIFIER = r"[a-zA-Z_][a-zA-Z0-9_]*"
9
+ STRING_CONTENTS = r"(.*?)(?=\")"
10
+ INTERPOLATION_EXPRESSION = r"(.*?)(?= }})"
11
+ HTML_IDENTIFIER = r"[a-zA-Z][a-zA-Z0-9_-]*"
12
+ HTML_TEXT = r"(.*?)(?=<)"
13
+
14
+
15
+ class ParsingError(Exception):
16
+ def __init__(self, message: str, source_start: int) -> None:
17
+ super().__init__(message)
18
+ self.source_start: int = source_start
19
+
20
+
21
+ def optional_regex(source: Source, pattern: str) -> tuple[Source, str | None]:
22
+ """Expect the source to start with the given pattern, return the source after the pattern and the found match (if any)."""
23
+ result: Match[str] | None = re.match(pattern, source.remaining())
24
+ if result is None:
25
+ return source, None
26
+ return source.eat(result.end()), result.group(0)
27
+
28
+
29
+ def expect_regex(source: Source, pattern: str, label: str) -> tuple[Source, str]:
30
+ """Expect the source to start with the given pattern, return the source after the pattern and the found match."""
31
+ source, result = optional_regex(source, pattern)
32
+ if result is None:
33
+ raise ParsingError(f"Expected {label}", source.offset)
34
+ return source, result
35
+
36
+
37
+ def expect_string(source: Source, string: str) -> Source:
38
+ """Expect the source to start with the given string, return the source after the string."""
39
+ if not source.remaining().startswith(string):
40
+ raise ParsingError(f"Expected {string}", source.offset)
41
+ return source.eat(len(string))
42
+
43
+
44
+ def expect_module(source: Source) -> tuple[Source, ASTModule]:
45
+ """The top level of the recursive descent grammar."""
46
+ children: list["ASTPython | ASTFragment"] = []
47
+ source_start: int = source.offset
48
+ while not source.at_end():
49
+ if source.remaining().startswith("<>"):
50
+ source, fragment = expect_fragment(source)
51
+ children.append(fragment)
52
+ else:
53
+ source, python = expect_python(source)
54
+ children.append(python)
55
+ return source, ASTModule(source_start, source.offset, children)
56
+
57
+
58
+ def expect_python(source: Source) -> tuple[Source, ASTPython]:
59
+ """Vanilla Python code."""
60
+ source_start: int = source.offset
61
+ source, python = expect_regex(source, PYTHON, "python source")
62
+ return source, ASTPython(source_start, source.offset, python)
63
+
64
+
65
+ def expect_fragment(source: Source) -> tuple[Source, ASTFragment]:
66
+ """A fragment - the top level of things created by this library."""
67
+ source_start: int = source.offset
68
+ source = expect_string(source, "<>")
69
+
70
+ children: list[ASTHTMLElement | ASTHTMLComment | ASTHTMLText | ASTInterpolation] = []
71
+ while not source.remaining().startswith("</>"):
72
+ source, _ = source.eat_whitespace()
73
+ if source.remaining().startswith("</>"):
74
+ break
75
+ source, child = expect_expression(source)
76
+ children.append(child)
77
+
78
+ source, _ = source.eat_whitespace()
79
+ source = expect_string(source, "</>")
80
+ source_end: int = source.offset
81
+
82
+ return source, ASTFragment(source_start, source_end, children)
83
+
84
+
85
+ def expect_expression(source: Source) -> tuple[Source, ASTHTMLElement | ASTHTMLComment | ASTHTMLText | ASTInterpolation]:
86
+ """Any HTML / functional block that might appear as part of the fragment."""
87
+ if source.remaining().startswith("<!--"):
88
+ source, html_comment = expect_html_comment(source)
89
+ return source, html_comment
90
+
91
+ if source.remaining().startswith("<"):
92
+ source, html_element = expect_html_element(source)
93
+ return source, html_element
94
+
95
+ if source.remaining().startswith("{{"):
96
+ source, interpolation = expect_interpolation(source)
97
+ return source, interpolation
98
+
99
+ source, html_text = expect_html_text(source)
100
+ return source, html_text
101
+
102
+
103
+ def expect_html_comment(source: Source) -> tuple[Source, ASTHTMLComment]:
104
+ source_start = source.offset
105
+ source = expect_string(source, "<!--")
106
+ source, content = expect_regex(source, r"[\s\S]*?(?=-->)", "comment content")
107
+ source = expect_string(source, "-->")
108
+ return source, ASTHTMLComment(source_start, source.offset, content)
109
+
110
+
111
+ def expect_html_element(source: Source) -> tuple[Source, ASTHTMLElement]:
112
+ """An HTML element that may appear in the fragment tree."""
113
+ element_source_start = source.offset
114
+ source = expect_string(source, "<")
115
+ source, _ = source.eat_whitespace()
116
+ source, name = expect_regex(source, HTML_IDENTIFIER, "element name")
117
+ source, _ = source.eat_whitespace()
118
+
119
+ attributes: dict[str, ASTHTMLAttribute] = {}
120
+ while not (source.remaining().startswith(">") or source.remaining().startswith("/>")):
121
+ attribute_source_start = source.offset
122
+ source, attribute_name = expect_regex(source, HTML_IDENTIFIER, "attribute name")
123
+ if not source.remaining().startswith("="):
124
+ attribute = ASTHTMLAttribute(attribute_source_start, source.offset, attribute_name, None, None)
125
+ attributes[attribute_name] = attribute
126
+ continue
127
+ source = expect_string(source, "=")
128
+
129
+ if source.remaining().startswith('"'):
130
+ source = expect_string(source, '"')
131
+ source, attribute_value = expect_regex(source, STRING_CONTENTS, "attribute value")
132
+ source = expect_string(source, '"')
133
+ attribute = ASTHTMLAttribute(attribute_source_start, source.offset, attribute_name, attribute_value, None)
134
+ attributes[attribute_name] = attribute
135
+ elif source.remaining().startswith("{{"):
136
+ source, interpolation = expect_interpolation(source)
137
+ attribute = ASTHTMLAttribute(attribute_source_start, source.offset, attribute_name, None, interpolation)
138
+ attributes[attribute_name] = attribute
139
+
140
+ source, _ = source.eat_whitespace()
141
+
142
+ if_attribute = attributes.pop("if") if "if" in attributes else None
143
+ for_attribute = attributes.pop("for") if "for" in attributes else None
144
+
145
+ if source.remaining().startswith("/>"):
146
+ source = expect_string(source, "/>")
147
+ html_element = ASTHTMLElement(
148
+ element_source_start,
149
+ source.offset,
150
+ name,
151
+ attributes,
152
+ if_attribute.interpolation if if_attribute is not None else None,
153
+ for_attribute.interpolation if for_attribute is not None else None,
154
+ [],
155
+ True,
156
+ )
157
+ return source, html_element
158
+
159
+ source = expect_string(source, ">")
160
+ source, _ = source.eat_whitespace()
161
+
162
+ children: list[ASTHTMLElement | ASTHTMLComment | ASTHTMLText | ASTInterpolation] = []
163
+ while not source.remaining().startswith("</"):
164
+ source, child = expect_expression(source)
165
+ children.append(child)
166
+ source, _ = source.eat_whitespace()
167
+
168
+ source = expect_string(source, "</")
169
+ source, _ = source.eat_whitespace()
170
+ source, closing_name = expect_regex(source, IDENTIFIER, "element name")
171
+
172
+ if name != closing_name:
173
+ raise ParsingError(f"Element closed ({closing_name!r}) is not the same as currently opened element ({name!r})", source.offset)
174
+
175
+ source, _ = source.eat_whitespace()
176
+ source = expect_string(source, ">")
177
+
178
+ html_element = ASTHTMLElement(
179
+ element_source_start,
180
+ source.offset,
181
+ name,
182
+ attributes,
183
+ if_attribute.interpolation if if_attribute is not None else None,
184
+ for_attribute.interpolation if for_attribute is not None else None,
185
+ children,
186
+ False,
187
+ )
188
+ return source, html_element
189
+
190
+
191
+ def expect_interpolation(source: Source) -> tuple[Source, ASTInterpolation]:
192
+ """An interpolation block."""
193
+ source_start = source.offset
194
+ source = expect_string(source, "{{")
195
+ source, _ = source.eat_whitespace()
196
+ source, expression = expect_regex(source, INTERPOLATION_EXPRESSION, "expression")
197
+ source, _ = source.eat_whitespace()
198
+ source = expect_string(source, "}}")
199
+ return source, ASTInterpolation(source_start, source.offset, expression)
200
+
201
+
202
+ def expect_html_text(source: Source) -> tuple[Source, ASTHTMLText]:
203
+ """Text as the child of an HTML expression."""
204
+ source_start = source.offset
205
+ source, text = expect_regex(source, HTML_TEXT, "text")
206
+ return source, ASTHTMLText(source_start, source.offset, text)
File without changes
@@ -0,0 +1,54 @@
1
+ from typing import Any
2
+
3
+
4
+ def sequence(children: list[str]) -> str:
5
+ return "".join(children)
6
+
7
+
8
+ def el(
9
+ name: str,
10
+ children: list[str],
11
+ oneline: bool,
12
+ **attributes: dict[str, Any],
13
+ ) -> str:
14
+ tag_contents = [
15
+ name,
16
+ classes_to_string(attributes.pop("classes")) if "classes" in attributes else None,
17
+ style_to_string(attributes.pop("style")) if "style" in attributes else None,
18
+ attributes_to_string(attributes) if attributes is not None else None,
19
+ ]
20
+ tag_contents = [item for item in tag_contents if item is not None]
21
+ tag_contents_string = " ".join(tag_contents)
22
+
23
+ if oneline:
24
+ return f"""<{tag_contents_string} />"""
25
+
26
+ children_string = "".join(str(child) for child in children)
27
+
28
+ return f"""<{tag_contents_string}>{children_string}</{name}>"""
29
+
30
+
31
+ def comment(content: str) -> str:
32
+ return f"<!-- {content} -->"
33
+
34
+
35
+ def attributes_to_string(attributes: dict[str, Any]) -> str:
36
+ return " ".join("=".join([key, f'"{value}"']) if value is not None else key for key, value in attributes.items())
37
+
38
+
39
+ def classes_to_string(classes: list[str] | str) -> str:
40
+ if isinstance(classes, list):
41
+ inner = " ".join(classes)
42
+ else:
43
+ inner = classes
44
+
45
+ return f'class="{inner}"'
46
+
47
+
48
+ def style_to_string(style: dict[str, str] | str) -> str:
49
+ if isinstance(style, dict):
50
+ inner = ";".join(": ".join(pair) for pair in style.items())
51
+ else:
52
+ inner = style
53
+
54
+ return f'style="{inner}"'
fragments/loader.py ADDED
@@ -0,0 +1,32 @@
1
+ import importlib.abc
2
+ import importlib.machinery
3
+ import sys
4
+
5
+ from fragments import transpiler
6
+
7
+
8
+ class TranspilingLoader(importlib.machinery.SourceFileLoader):
9
+ def get_data(self, path: str) -> bytes:
10
+ if not path.endswith(".py"):
11
+ return super().get_data(path)
12
+
13
+ source = super().get_data(path).decode("utf-8")
14
+
15
+ if "<>" not in source:
16
+ return source.encode("utf-8")
17
+
18
+ transpiled = transpiler.transpile(source)
19
+ return transpiled.encode("utf-8")
20
+
21
+
22
+ class TranspilingFinder(importlib.abc.MetaPathFinder):
23
+ def find_spec(self, fullname: str, path, target=None):
24
+ spec = importlib.machinery.PathFinder.find_spec(fullname, path)
25
+ if spec is None or not isinstance(spec.loader, importlib.machinery.SourceFileLoader):
26
+ return None
27
+ assert spec.origin is not None
28
+ spec.loader = TranspilingLoader(fullname, spec.origin)
29
+ return spec
30
+
31
+
32
+ sys.meta_path.insert(0, TranspilingFinder())
File without changes
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ from lsprotocol import types
4
+
5
+ from fragments.lsp.server import FragmentsServer, _converter, _remap_text_edits, server
6
+
7
+
8
+ @server.feature(types.TEXT_DOCUMENT_COMPLETION, types.CompletionOptions(trigger_characters=["."]))
9
+ async def completion(language_server: FragmentsServer, params: types.CompletionParams) -> types.CompletionList | None:
10
+ if language_server._pyright is None or params.text_document.uri not in language_server._files:
11
+ return None
12
+ state = language_server._files[params.text_document.uri]
13
+
14
+ context = None
15
+ if params.context is not None:
16
+ context = {
17
+ "triggerKind": params.context.trigger_kind.value,
18
+ "triggerCharacter": params.context.trigger_character,
19
+ }
20
+
21
+ if state is None:
22
+ result = await language_server._pyright.request(
23
+ "textDocument/completion",
24
+ {
25
+ "textDocument": {"uri": params.text_document.uri},
26
+ "position": {"line": params.position.line, "character": params.position.character},
27
+ "context": context,
28
+ },
29
+ )
30
+ raw_result = result.get("result")
31
+ if not raw_result:
32
+ return None
33
+ if isinstance(raw_result, list):
34
+ return types.CompletionList(is_incomplete=False, items=_converter.structure(raw_result, list[types.CompletionItem]))
35
+ return _converter.structure(raw_result, types.CompletionList)
36
+
37
+ transpiled_position = state.original_to_transpiled_position(params.position)
38
+ if transpiled_position is None:
39
+ return None
40
+
41
+ result = await language_server._pyright.request(
42
+ "textDocument/completion",
43
+ {
44
+ "textDocument": {"uri": params.text_document.uri},
45
+ "position": {"line": transpiled_position.line, "character": transpiled_position.character},
46
+ "context": context,
47
+ },
48
+ )
49
+
50
+ raw_result = result.get("result")
51
+ if not raw_result:
52
+ return None
53
+
54
+ if isinstance(raw_result, list):
55
+ raw_items, is_incomplete = raw_result, False
56
+ else:
57
+ raw_items = raw_result.get("items", [])
58
+ is_incomplete = raw_result.get("isIncomplete", False)
59
+
60
+ remapped_items: list[types.CompletionItem] = []
61
+ for item in _converter.structure(raw_items, list[types.CompletionItem]):
62
+ if item.text_edit is not None:
63
+ if isinstance(item.text_edit, types.InsertReplaceEdit):
64
+ insert = state.transpiled_to_original_range(item.text_edit.insert)
65
+ replace = state.transpiled_to_original_range(item.text_edit.replace)
66
+ if insert is None or replace is None:
67
+ continue
68
+ item.text_edit.insert = insert
69
+ item.text_edit.replace = replace
70
+ else:
71
+ remapped = state.transpiled_to_original_range(item.text_edit.range)
72
+ if remapped is None:
73
+ continue
74
+ item.text_edit.range = remapped
75
+ if item.additional_text_edits:
76
+ item.additional_text_edits = _remap_text_edits(item.additional_text_edits, state)
77
+ remapped_items.append(item)
78
+
79
+ return types.CompletionList(is_incomplete=is_incomplete, items=remapped_items)
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ from lsprotocol import types
4
+
5
+ from fragments.lsp.server import FragmentsServer, _converter, server
6
+
7
+
8
+ @server.feature(types.TEXT_DOCUMENT_DEFINITION)
9
+ async def definition(language_server: FragmentsServer, params: types.DefinitionParams) -> list[types.Location] | None:
10
+ if language_server._pyright is None or params.text_document.uri not in language_server._files:
11
+ return None
12
+ state = language_server._files[params.text_document.uri]
13
+
14
+ if state is None:
15
+ result = await language_server._pyright.request(
16
+ "textDocument/definition",
17
+ {
18
+ "textDocument": {"uri": params.text_document.uri},
19
+ "position": {"line": params.position.line, "character": params.position.character},
20
+ },
21
+ )
22
+ else:
23
+ transpiled_position = state.original_to_transpiled_position(params.position)
24
+ if transpiled_position is None:
25
+ return None
26
+ result = await language_server._pyright.request(
27
+ "textDocument/definition",
28
+ {
29
+ "textDocument": {"uri": params.text_document.uri},
30
+ "position": {"line": transpiled_position.line, "character": transpiled_position.character},
31
+ },
32
+ )
33
+
34
+ raw_result = result.get("result")
35
+ if not raw_result:
36
+ return None
37
+
38
+ raw_locations = [raw_result] if isinstance(raw_result, dict) else raw_result
39
+ locations: list[types.Location] = []
40
+ for location in _converter.structure(raw_locations, list[types.Location]):
41
+ target_state = language_server._files.get(location.uri)
42
+ if target_state is not None:
43
+ remapped = target_state.transpiled_to_original_range(location.range)
44
+ if remapped is None:
45
+ continue
46
+ location.range = remapped
47
+ locations.append(location)
48
+
49
+ return locations or None