modelable 1.0.0__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.

Potentially problematic release.


This version of modelable might be problematic. Click here for more details.

Files changed (122) hide show
  1. modelable/__init__.py +1 -0
  2. modelable/__main__.py +3 -0
  3. modelable/_pydantic_py314_compat.py +31 -0
  4. modelable/cli.py +41 -0
  5. modelable/commands/__init__.py +1 -0
  6. modelable/commands/apicurio.py +84 -0
  7. modelable/commands/codegen.py +241 -0
  8. modelable/commands/common.py +43 -0
  9. modelable/commands/compile.py +237 -0
  10. modelable/commands/create.py +164 -0
  11. modelable/commands/diff.py +82 -0
  12. modelable/commands/graph.py +53 -0
  13. modelable/commands/llm.py +564 -0
  14. modelable/commands/lsp.py +15 -0
  15. modelable/commands/runtime.py +37 -0
  16. modelable/commands/scenario.py +104 -0
  17. modelable/commands/spec.py +197 -0
  18. modelable/commands/workspace.py +240 -0
  19. modelable/compat/__init__.py +11 -0
  20. modelable/compat/checker.py +179 -0
  21. modelable/compat/diff.py +169 -0
  22. modelable/compiler/__init__.py +3 -0
  23. modelable/compiler/compiler.py +19 -0
  24. modelable/compiler/workspace.py +346 -0
  25. modelable/diagnostics/__init__.py +3 -0
  26. modelable/diagnostics/model.py +27 -0
  27. modelable/emitters/__init__.py +0 -0
  28. modelable/emitters/base.py +22 -0
  29. modelable/emitters/csharp.py +245 -0
  30. modelable/emitters/dbt_yaml.py +290 -0
  31. modelable/emitters/diagnostics.py +25 -0
  32. modelable/emitters/fhir.py +694 -0
  33. modelable/emitters/fhir_validator.py +36 -0
  34. modelable/emitters/go.py +334 -0
  35. modelable/emitters/java.py +264 -0
  36. modelable/emitters/json_schema.py +458 -0
  37. modelable/emitters/markdown.py +252 -0
  38. modelable/emitters/odcs.py +355 -0
  39. modelable/emitters/openlineage.py +315 -0
  40. modelable/emitters/openmetadata.py +258 -0
  41. modelable/emitters/python.py +282 -0
  42. modelable/emitters/rust.py +643 -0
  43. modelable/emitters/shapes.py +261 -0
  44. modelable/emitters/sql.py +266 -0
  45. modelable/emitters/targets.py +141 -0
  46. modelable/emitters/typescript.py +352 -0
  47. modelable/expressions/__init__.py +0 -0
  48. modelable/expressions/cel.py +547 -0
  49. modelable/governance/__init__.py +3 -0
  50. modelable/governance/checker.py +271 -0
  51. modelable/governance/por.py +46 -0
  52. modelable/grammar/__init__.py +1 -0
  53. modelable/grammar/modelable.lark +257 -0
  54. modelable/graph/__init__.py +5 -0
  55. modelable/graph/export.py +442 -0
  56. modelable/llm/__init__.py +43 -0
  57. modelable/llm/chat.py +255 -0
  58. modelable/llm/config.py +87 -0
  59. modelable/llm/context.py +194 -0
  60. modelable/llm/engine.py +976 -0
  61. modelable/llm/importers.py +1077 -0
  62. modelable/llm/provenance.py +84 -0
  63. modelable/llm/providers.py +182 -0
  64. modelable/llm/qa.py +126 -0
  65. modelable/llm/recommendations.py +33 -0
  66. modelable/llm/redaction.py +19 -0
  67. modelable/llm/render.py +279 -0
  68. modelable/llm/update_plan.py +101 -0
  69. modelable/llm/validation_help.py +10 -0
  70. modelable/lsp/__init__.py +3 -0
  71. modelable/lsp/__main__.py +4 -0
  72. modelable/lsp/code_actions.py +210 -0
  73. modelable/lsp/completion.py +480 -0
  74. modelable/lsp/definition.py +343 -0
  75. modelable/lsp/diagnostics.py +31 -0
  76. modelable/lsp/document_symbols.py +197 -0
  77. modelable/lsp/federation.py +261 -0
  78. modelable/lsp/folding.py +33 -0
  79. modelable/lsp/formatting.py +64 -0
  80. modelable/lsp/highlight.py +30 -0
  81. modelable/lsp/hover.py +370 -0
  82. modelable/lsp/inlay_hints.py +158 -0
  83. modelable/lsp/references.py +511 -0
  84. modelable/lsp/rename.py +564 -0
  85. modelable/lsp/semantic_tokens.py +412 -0
  86. modelable/lsp/server.py +370 -0
  87. modelable/lsp/workspace.py +83 -0
  88. modelable/lsp/workspace_symbols.py +104 -0
  89. modelable/parser/__init__.py +94 -0
  90. modelable/parser/ir.py +451 -0
  91. modelable/parser/parse.py +47 -0
  92. modelable/parser/transformer.py +798 -0
  93. modelable/parser/wire.py +68 -0
  94. modelable/planner/__init__.py +0 -0
  95. modelable/planner/lineage.py +91 -0
  96. modelable/planner/planner.py +134 -0
  97. modelable/planner/plans.py +122 -0
  98. modelable/py.typed +0 -0
  99. modelable/registry/__init__.py +9 -0
  100. modelable/registry/apicurio.py +166 -0
  101. modelable/registry/base.py +18 -0
  102. modelable/registry/factory.py +18 -0
  103. modelable/registry/index.py +419 -0
  104. modelable/registry/local.py +26 -0
  105. modelable/registry/oci.py +22 -0
  106. modelable/registry/resolver.py +213 -0
  107. modelable/registry/schema.sql +119 -0
  108. modelable/registry/signature.py +26 -0
  109. modelable/release.py +125 -0
  110. modelable/runtime/__init__.py +5 -0
  111. modelable/runtime/adapter/__init__.py +17 -0
  112. modelable/runtime/adapter/base.py +18 -0
  113. modelable/runtime/adapter/postgres.py +82 -0
  114. modelable/specs/__init__.py +23 -0
  115. modelable/specs/tracking.py +220 -0
  116. modelable/validation/__init__.py +3 -0
  117. modelable/validation/semantic.py +659 -0
  118. modelable-1.0.0.dist-info/METADATA +61 -0
  119. modelable-1.0.0.dist-info/RECORD +122 -0
  120. modelable-1.0.0.dist-info/WHEEL +4 -0
  121. modelable-1.0.0.dist-info/entry_points.txt +2 -0
  122. modelable-1.0.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,343 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ from lsprotocol import types
6
+
7
+ from modelable.llm.context import parse_model_ref
8
+ from modelable.lsp.workspace import LspWorkspaceIndex
9
+ from modelable.registry.resolver import resolve_model_ref
10
+
11
+ _QUALIFIED_REF_PATTERN = re.compile(
12
+ r"(?P<domain>[A-Za-z_][A-Za-z0-9_]*)\.(?P<name>[A-Za-z_][A-Za-z0-9_]*)\s*@\s*(?P<version>\d+)"
13
+ )
14
+ _REF_TYPE_PATTERN = re.compile(r"ref\s*<\s*(?P<domain>[A-Za-z_][A-Za-z0-9_]*)\.(?P<name>[A-Za-z_][A-Za-z0-9_]*)\s*>")
15
+ _FIELD_REF_PATTERN = re.compile(r"(?P<alias>[A-Za-z_][A-Za-z0-9_]*)\.(?P<field>[A-Za-z_][A-Za-z0-9_]*)")
16
+ _DECL_PATTERN = re.compile(
17
+ r"^\s*(?P<kind>entity|aggregate|event|value|projection)\s+"
18
+ r"(?P<name>[A-Za-z_][A-Za-z0-9_]*)\s*@\s*(?P<version>\d+)"
19
+ )
20
+ _DOMAIN_PATTERN = re.compile(r'^\s*domain\s+(?:"(?P<quoted>[^"]+)"|(?P<name>[A-Za-z_][A-Za-z0-9_]*))')
21
+ _WORD_PATTERN = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
22
+ _MODEL_FIELD_PATTERN = re.compile(
23
+ r"^\s*(?:@[A-Za-z_][A-Za-z0-9_]*(?:\([^)]*\))?\s+)*(?P<name>[A-Za-z_][A-Za-z0-9_]*)\??\s*:"
24
+ )
25
+ _PROJECTION_FIELD_PATTERN = re.compile(
26
+ r"^\s*(?:@[A-Za-z_][A-Za-z0-9_]*(?:\([^)]*\))?\s+)*(?P<name>[A-Za-z_][A-Za-z0-9_]*)\s*(?:<-|=)"
27
+ )
28
+
29
+
30
+ _MODEL_DECL_KINDS = {"entity", "aggregate", "event", "value"}
31
+
32
+
33
+ def build_definition(
34
+ index: LspWorkspaceIndex, uri: str, line: int, character: int
35
+ ) -> types.Location | list[types.Location] | None:
36
+ source = index.documents.get(uri)
37
+ workspace = index.workspace
38
+ if source is None or workspace is None:
39
+ return None
40
+
41
+ lines = source.text.splitlines()
42
+ if line < 0 or line >= len(lines):
43
+ return None
44
+ text_line = lines[line]
45
+
46
+ for match in _QUALIFIED_REF_PATTERN.finditer(text_line):
47
+ if _contains(match.start(), match.end(), character):
48
+ ref = f"{match.group('domain')}.{match.group('name')}@{match.group('version')}"
49
+ location = _definition_for_qualified_ref(workspace, ref)
50
+ if location is not None:
51
+ return location
52
+
53
+ for match in _REF_TYPE_PATTERN.finditer(text_line):
54
+ if _contains(match.start(), match.end(), character):
55
+ location = _definition_for_unversioned_ref(workspace, match.group("domain"), match.group("name"))
56
+ if location is not None:
57
+ return location
58
+
59
+ for match in _FIELD_REF_PATTERN.finditer(text_line):
60
+ if _contains(match.start(), match.end(), character):
61
+ location = _definition_for_field_reference(
62
+ workspace,
63
+ source.text,
64
+ line,
65
+ match.group("alias"),
66
+ match.group("field"),
67
+ )
68
+ if location is not None:
69
+ return location
70
+
71
+ word = _word_at(text_line, character)
72
+ if word is None:
73
+ return None
74
+
75
+ scope = _current_scope(source.text, line)
76
+ if scope is None:
77
+ return None
78
+ domain_name, kind, name, version = scope
79
+
80
+ if word == name:
81
+ return _definition_for_decl(workspace, domain_name, kind, name, version)
82
+
83
+ if kind == "model":
84
+ return _definition_for_model_field(
85
+ workspace,
86
+ domain_name,
87
+ name,
88
+ version,
89
+ word,
90
+ )
91
+ return _definition_for_projection_field(
92
+ workspace,
93
+ domain_name,
94
+ name,
95
+ version,
96
+ word,
97
+ )
98
+
99
+
100
+ def _definition_for_qualified_ref(workspace, ref: str) -> types.Location | None:
101
+ model_ref = parse_model_ref(ref)
102
+ domain = next((d for d in workspace.mdl.domains if d.name == model_ref.domain), None)
103
+ if domain is None:
104
+ return None
105
+ if model_ref.name in domain.models:
106
+ return _definition_for_decl(workspace, model_ref.domain, "model", model_ref.name, model_ref.version)
107
+ if model_ref.name in domain.projections:
108
+ return _definition_for_decl(workspace, model_ref.domain, "projection", model_ref.name, model_ref.version)
109
+ return None
110
+
111
+
112
+ def _definition_for_unversioned_ref(workspace, domain_name: str, name: str) -> types.Location | None:
113
+ domain = next((d for d in workspace.mdl.domains if d.name == domain_name), None)
114
+ if domain is None:
115
+ return None
116
+ if name in domain.models:
117
+ latest = max(domain.models[name], key=lambda v: v.version)
118
+ return _definition_for_decl(workspace, domain_name, "model", name, latest.version)
119
+ if name in domain.projections:
120
+ latest = max(domain.projections[name], key=lambda v: v.version)
121
+ return _definition_for_decl(workspace, domain_name, "projection", name, latest.version)
122
+ return None
123
+
124
+
125
+ def _definition_for_field_reference(
126
+ workspace,
127
+ text: str,
128
+ line: int,
129
+ alias: str,
130
+ field_name: str,
131
+ ) -> types.Location | None:
132
+ scope = _current_scope(text, line)
133
+ if scope is None:
134
+ return None
135
+ domain_name, kind, name, version = scope
136
+ if kind == "model":
137
+ return _definition_for_model_field(workspace, domain_name, name, version, field_name)
138
+
139
+ domain = next((d for d in workspace.mdl.domains if d.name == domain_name), None)
140
+ if domain is None:
141
+ return None
142
+ versions = domain.projections.get(name)
143
+ if not versions:
144
+ return None
145
+ projection_version = next((item for item in versions if item.version == version), None)
146
+ if projection_version is None:
147
+ return None
148
+
149
+ for source_ref in [projection_version.source, *projection_version.joins]:
150
+ if source_ref.alias != alias:
151
+ continue
152
+ try:
153
+ resolved = resolve_model_ref(
154
+ workspace.mdl,
155
+ source_ref.model,
156
+ source_ref.version,
157
+ )
158
+ except LookupError:
159
+ break
160
+ return _definition_for_source_field(
161
+ workspace,
162
+ resolved.domain_name,
163
+ resolved.model_name,
164
+ resolved.version.version,
165
+ field_name,
166
+ )
167
+
168
+ return _definition_for_projection_field(workspace, domain_name, name, version, field_name)
169
+
170
+
171
+ def _definition_for_decl(
172
+ workspace,
173
+ domain_name: str,
174
+ kind: str,
175
+ name: str,
176
+ version: int,
177
+ ) -> types.Location | None:
178
+ for source in workspace.sources:
179
+ location = _find_decl_location(source.uri, source.text, domain_name, kind, name, version)
180
+ if location is not None:
181
+ return location
182
+ return None
183
+
184
+
185
+ def _definition_for_model_field(
186
+ workspace,
187
+ domain_name: str,
188
+ model_name: str,
189
+ version: int,
190
+ field_name: str,
191
+ ) -> types.Location | None:
192
+ return _definition_for_source_field(workspace, domain_name, model_name, version, field_name)
193
+
194
+
195
+ def _definition_for_source_field(
196
+ workspace,
197
+ domain_name: str,
198
+ model_name: str,
199
+ version: int,
200
+ field_name: str,
201
+ ) -> types.Location | None:
202
+ for source in workspace.sources:
203
+ for kind in ("model", "projection"):
204
+ location = _find_field_location(
205
+ source.uri,
206
+ source.text,
207
+ domain_name,
208
+ kind,
209
+ model_name,
210
+ version,
211
+ field_name,
212
+ )
213
+ if location is not None:
214
+ return location
215
+ return None
216
+
217
+
218
+ def _definition_for_projection_field(
219
+ workspace,
220
+ domain_name: str,
221
+ projection_name: str,
222
+ version: int,
223
+ field_name: str,
224
+ ) -> types.Location | None:
225
+ return _definition_for_source_field(workspace, domain_name, projection_name, version, field_name)
226
+
227
+
228
+ def _find_decl_location(
229
+ uri: str,
230
+ text: str,
231
+ domain_name: str,
232
+ kind: str,
233
+ name: str,
234
+ version: int,
235
+ ) -> types.Location | None:
236
+ current_domain: str | None = None
237
+ lines = text.splitlines()
238
+ for line_no, line in enumerate(lines):
239
+ domain_match = _DOMAIN_PATTERN.match(line)
240
+ if domain_match:
241
+ current_domain = domain_match.group("quoted") or domain_match.group("name")
242
+ continue
243
+ decl_match = _DECL_PATTERN.match(line)
244
+ if not decl_match or current_domain != domain_name:
245
+ continue
246
+ decl_kind = decl_match.group("kind")
247
+ if kind == "model":
248
+ if decl_kind not in _MODEL_DECL_KINDS:
249
+ continue
250
+ elif decl_kind != kind:
251
+ continue
252
+ if decl_match.group("name") != name:
253
+ continue
254
+ if int(decl_match.group("version")) != version:
255
+ continue
256
+ return types.Location(
257
+ uri=uri,
258
+ range=types.Range(
259
+ start=types.Position(line=line_no, character=decl_match.start("name")),
260
+ end=types.Position(line=line_no, character=decl_match.end("name")),
261
+ ),
262
+ )
263
+ return None
264
+
265
+
266
+ def _find_field_location(
267
+ uri: str,
268
+ text: str,
269
+ domain_name: str,
270
+ kind: str,
271
+ name: str,
272
+ version: int,
273
+ field_name: str,
274
+ ) -> types.Location | None:
275
+ current_domain: str | None = None
276
+ active = False
277
+ lines = text.splitlines()
278
+ pattern = _MODEL_FIELD_PATTERN if kind == "model" else _PROJECTION_FIELD_PATTERN
279
+
280
+ for line_no, line in enumerate(lines):
281
+ domain_match = _DOMAIN_PATTERN.match(line)
282
+ if domain_match:
283
+ current_domain = domain_match.group("quoted") or domain_match.group("name")
284
+ active = False
285
+ continue
286
+ decl_match = _DECL_PATTERN.match(line)
287
+ if decl_match and current_domain == domain_name:
288
+ decl_kind = decl_match.group("kind")
289
+ active = (
290
+ (decl_kind in _MODEL_DECL_KINDS if kind == "model" else decl_kind == kind)
291
+ and decl_match.group("name") == name
292
+ and int(decl_match.group("version")) == version
293
+ )
294
+ continue
295
+ if not active:
296
+ continue
297
+ field_match = pattern.match(line)
298
+ if field_match and field_match.group("name") == field_name:
299
+ return types.Location(
300
+ uri=uri,
301
+ range=types.Range(
302
+ start=types.Position(line=line_no, character=field_match.start("name")),
303
+ end=types.Position(line=line_no, character=field_match.end("name")),
304
+ ),
305
+ )
306
+ if line.strip() == "}":
307
+ active = False
308
+ return None
309
+
310
+
311
+ def _current_scope(text: str, line: int) -> tuple[str, str, str, int] | None:
312
+ lines = text.splitlines()
313
+ current_domain: str | None = None
314
+ current_kind: str | None = None
315
+ current_name: str | None = None
316
+ current_version: int | None = None
317
+ for item in lines[: line + 1]:
318
+ domain_match = _DOMAIN_PATTERN.match(item)
319
+ if domain_match:
320
+ current_domain = domain_match.group("quoted") or domain_match.group("name")
321
+ current_kind = None
322
+ current_name = None
323
+ current_version = None
324
+ continue
325
+ decl_match = _DECL_PATTERN.match(item)
326
+ if decl_match and current_domain is not None:
327
+ current_kind = "model" if decl_match.group("kind") != "projection" else "projection"
328
+ current_name = decl_match.group("name")
329
+ current_version = int(decl_match.group("version"))
330
+ if current_domain and current_kind and current_name and current_version is not None:
331
+ return current_domain, current_kind, current_name, current_version
332
+ return None
333
+
334
+
335
+ def _word_at(text_line: str, character: int) -> str | None:
336
+ for match in _WORD_PATTERN.finditer(text_line):
337
+ if _contains(match.start(), match.end(), character):
338
+ return match.group(0)
339
+ return None
340
+
341
+
342
+ def _contains(start: int, end: int, character: int) -> bool:
343
+ return start <= character <= max(end - 1, start)
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ from lsprotocol import types
4
+
5
+ from modelable.diagnostics.model import Diagnostic
6
+
7
+
8
+ def to_lsp_diagnostic(diagnostic: Diagnostic) -> types.Diagnostic:
9
+ severity_map = {
10
+ "error": types.DiagnosticSeverity.Error,
11
+ "warning": types.DiagnosticSeverity.Warning,
12
+ "information": types.DiagnosticSeverity.Information,
13
+ }
14
+ line = max((diagnostic.line or 1) - 1, 0)
15
+ column = max((diagnostic.column or 1) - 1, 0)
16
+ end_line = max((diagnostic.end_line or diagnostic.line or 1) - 1, 0)
17
+ end_column = max((diagnostic.end_column or diagnostic.column or 1) - 1, 0)
18
+ return types.Diagnostic(
19
+ message=diagnostic.message,
20
+ severity=severity_map[diagnostic.severity],
21
+ source="modelable",
22
+ code=diagnostic.code,
23
+ range=types.Range(
24
+ start=types.Position(line=line, character=column),
25
+ end=types.Position(line=end_line, character=end_column),
26
+ ),
27
+ )
28
+
29
+
30
+ def to_lsp_diagnostics(diagnostics: list[Diagnostic]) -> list[types.Diagnostic]:
31
+ return [to_lsp_diagnostic(diagnostic) for diagnostic in diagnostics]
@@ -0,0 +1,197 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ from lsprotocol import types
6
+
7
+ from modelable.lsp.workspace import LspWorkspaceIndex
8
+
9
+ _DOMAIN_PATTERN = re.compile(r'^\s*domain\s+(?:"(?P<quoted>[^"]+)"|(?P<name>[A-Za-z_][A-Za-z0-9_]*))')
10
+ _DECL_PATTERN = re.compile(
11
+ r"^\s*(?P<kind>entity|aggregate|event|value|projection)\s+"
12
+ r"(?P<name>[A-Za-z_][A-Za-z0-9_]*)\s*@\s*(?P<version>\d+)"
13
+ )
14
+ _FIELD_PATTERN = re.compile(r"^\s*(?:@[A-Za-z_][A-Za-z0-9_]*(?:\([^)]*\))?\s+)*(?P<name>[A-Za-z_][A-Za-z0-9_]*)\??\s*:")
15
+ _PROJECTION_FIELD_PATTERN = re.compile(
16
+ r"^\s*(?:@[A-Za-z_][A-Za-z0-9_]*(?:\([^)]*\))?\s+)*(?P<name>[A-Za-z_][A-Za-z0-9_]*)\s*(?:<-|=)"
17
+ )
18
+
19
+
20
+ def build_document_symbols(
21
+ index: LspWorkspaceIndex,
22
+ uri: str,
23
+ ) -> list[types.DocumentSymbol] | None:
24
+ source = index.documents.get(uri)
25
+ if source is None or index.workspace is None:
26
+ return None
27
+
28
+ lines = source.text.splitlines()
29
+ symbols: list[types.DocumentSymbol] = []
30
+ current_domain: types.DocumentSymbol | None = None
31
+ current_decl: types.DocumentSymbol | None = None
32
+ current_domain_name: str | None = None
33
+ current_decl_name: str | None = None
34
+ current_decl_kind: str | None = None
35
+ current_decl_version: int | None = None
36
+
37
+ for line_no, line in enumerate(lines):
38
+ domain_match = _DOMAIN_PATTERN.match(line)
39
+ if domain_match:
40
+ current_domain_name = domain_match.group("quoted") or domain_match.group("name")
41
+ current_domain = _make_domain_symbol(
42
+ lines,
43
+ line_no,
44
+ current_domain_name,
45
+ )
46
+ symbols.append(current_domain)
47
+ current_decl = None
48
+ current_decl_name = None
49
+ current_decl_kind = None
50
+ current_decl_version = None
51
+ continue
52
+
53
+ decl_match = _DECL_PATTERN.match(line)
54
+ if decl_match and current_domain is not None and current_domain_name is not None:
55
+ current_decl_name = decl_match.group("name")
56
+ current_decl_kind = decl_match.group("kind")
57
+ current_decl_version = int(decl_match.group("version"))
58
+ current_decl = _make_decl_symbol(
59
+ lines,
60
+ line_no,
61
+ current_domain_name,
62
+ current_decl_kind,
63
+ current_decl_name,
64
+ current_decl_version,
65
+ )
66
+ if current_domain.children is None:
67
+ current_domain.children = [current_decl]
68
+ else:
69
+ current_domain.children = [*current_domain.children, current_decl]
70
+ continue
71
+
72
+ if current_decl is None:
73
+ continue
74
+
75
+ field_match = _FIELD_PATTERN.match(line) or _PROJECTION_FIELD_PATTERN.match(line)
76
+ if field_match:
77
+ field_symbol = _make_field_symbol(
78
+ line_no,
79
+ field_match.start("name"),
80
+ field_match.end("name"),
81
+ field_match.group("name"),
82
+ line,
83
+ )
84
+ if current_decl.children is None:
85
+ current_decl.children = [field_symbol]
86
+ else:
87
+ current_decl.children = [*current_decl.children, field_symbol]
88
+
89
+ return symbols
90
+
91
+
92
+ def _make_domain_symbol(
93
+ lines: list[str],
94
+ line_no: int,
95
+ name: str,
96
+ ) -> types.DocumentSymbol:
97
+ return types.DocumentSymbol(
98
+ name=name,
99
+ kind=types.SymbolKind.Module,
100
+ detail="domain",
101
+ range=types.Range(
102
+ start=types.Position(line=line_no, character=0),
103
+ end=types.Position(
104
+ line=_block_end_line(lines, line_no), character=len(lines[_block_end_line(lines, line_no)])
105
+ ),
106
+ ),
107
+ selection_range=types.Range(
108
+ start=types.Position(line=line_no, character=_domain_name_start(lines[line_no])),
109
+ end=types.Position(line=line_no, character=_domain_name_end(lines[line_no])),
110
+ ),
111
+ )
112
+
113
+
114
+ def _make_decl_symbol(
115
+ lines: list[str],
116
+ line_no: int,
117
+ domain_name: str,
118
+ kind: str,
119
+ name: str,
120
+ version: int,
121
+ ) -> types.DocumentSymbol:
122
+ detail = f"{kind} @{version}"
123
+ return types.DocumentSymbol(
124
+ name=name,
125
+ kind=types.SymbolKind.Class,
126
+ detail=detail,
127
+ range=types.Range(
128
+ start=types.Position(line=line_no, character=0),
129
+ end=types.Position(
130
+ line=_block_end_line(lines, line_no), character=len(lines[_block_end_line(lines, line_no)])
131
+ ),
132
+ ),
133
+ selection_range=types.Range(
134
+ start=types.Position(line=line_no, character=_decl_name_start(lines[line_no])),
135
+ end=types.Position(line=line_no, character=_decl_name_end(lines[line_no])),
136
+ ),
137
+ )
138
+
139
+
140
+ def _make_field_symbol(
141
+ line_no: int,
142
+ start: int,
143
+ end: int,
144
+ name: str,
145
+ line: str,
146
+ ) -> types.DocumentSymbol:
147
+ return types.DocumentSymbol(
148
+ name=name,
149
+ kind=types.SymbolKind.Field,
150
+ detail=line.strip(),
151
+ range=types.Range(
152
+ start=types.Position(line=line_no, character=0),
153
+ end=types.Position(line=line_no, character=len(line)),
154
+ ),
155
+ selection_range=types.Range(
156
+ start=types.Position(line=line_no, character=start),
157
+ end=types.Position(line=line_no, character=end),
158
+ ),
159
+ )
160
+
161
+
162
+ def _block_end_line(lines: list[str], start_line: int) -> int:
163
+ depth = 0
164
+ for line_no in range(start_line, len(lines)):
165
+ depth += lines[line_no].count("{")
166
+ depth -= lines[line_no].count("}")
167
+ if line_no > start_line and depth <= 0:
168
+ return line_no
169
+ return max(len(lines) - 1, start_line)
170
+
171
+
172
+ def _domain_name_start(line: str) -> int:
173
+ match = _DOMAIN_PATTERN.match(line)
174
+ if match is None:
175
+ return 0
176
+ return match.start("quoted") if match.group("quoted") is not None else match.start("name")
177
+
178
+
179
+ def _domain_name_end(line: str) -> int:
180
+ match = _DOMAIN_PATTERN.match(line)
181
+ if match is None:
182
+ return 0
183
+ return match.end("quoted") if match.group("quoted") is not None else match.end("name")
184
+
185
+
186
+ def _decl_name_start(line: str) -> int:
187
+ match = _DECL_PATTERN.match(line)
188
+ if match is None:
189
+ return 0
190
+ return match.start("name")
191
+
192
+
193
+ def _decl_name_end(line: str) -> int:
194
+ match = _DECL_PATTERN.match(line)
195
+ if match is None:
196
+ return 0
197
+ return match.end("name")