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,370 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import contextlib
5
+ import inspect
6
+ from pathlib import Path
7
+
8
+ from lsprotocol import types
9
+ from pygls import feature_manager
10
+ from pygls.lsp.server import LanguageServer
11
+ from pygls.protocol import json_rpc
12
+
13
+ from modelable.lsp.code_actions import build_code_actions
14
+ from modelable.lsp.completion import build_completion
15
+ from modelable.lsp.definition import build_definition
16
+ from modelable.lsp.diagnostics import to_lsp_diagnostics
17
+ from modelable.lsp.document_symbols import build_document_symbols
18
+ from modelable.lsp.federation import build_import_diagnostics
19
+ from modelable.lsp.folding import build_folding_ranges
20
+ from modelable.lsp.formatting import build_document_formatting
21
+ from modelable.lsp.highlight import build_document_highlight
22
+ from modelable.lsp.hover import build_hover
23
+ from modelable.lsp.inlay_hints import build_inlay_hints
24
+ from modelable.lsp.references import build_references
25
+ from modelable.lsp.rename import build_prepare_rename, build_rename
26
+ from modelable.lsp.semantic_tokens import build_semantic_tokens, semantic_tokens_legend
27
+ from modelable.lsp.workspace import LspWorkspaceIndex, uri_to_path
28
+ from modelable.lsp.workspace_symbols import build_workspace_symbols
29
+
30
+ feature_manager.asyncio.iscoroutinefunction = inspect.iscoroutinefunction
31
+ json_rpc.asyncio.iscoroutinefunction = inspect.iscoroutinefunction
32
+
33
+ _DEBOUNCE_DELAY = 0.2
34
+
35
+
36
+ class ModelableLanguageServer(LanguageServer):
37
+ def __init__(self) -> None:
38
+ super().__init__("modelable-lsp", "0.1.0")
39
+ self.index = LspWorkspaceIndex()
40
+ self._indexes: dict[Path, LspWorkspaceIndex] = {}
41
+ self._debounce_tasks: dict[str, asyncio.Task] = {}
42
+ self._root_uri: str | None = None
43
+ self._scanned_dirs: set[Path] = set()
44
+
45
+ def index_for(self, uri: str) -> LspWorkspaceIndex:
46
+ path = uri_to_path(uri)
47
+ if path is not None:
48
+ # Keep index routing stable even when no workspace.mdl exists:
49
+ # use the file's parent directory as the effective root.
50
+ root = _find_workspace_root(path) or path.parent
51
+ if root not in self._indexes:
52
+ self._indexes[root] = LspWorkspaceIndex()
53
+ return self._indexes[root]
54
+ return self.index
55
+
56
+
57
+ server = ModelableLanguageServer()
58
+
59
+
60
+ @server.feature(types.INITIALIZE)
61
+ def initialize(ls: ModelableLanguageServer, params: types.InitializeParams) -> types.InitializeResult:
62
+ ls._root_uri = params.root_uri
63
+ return _build_initialize_result()
64
+
65
+
66
+ def _build_initialize_result() -> types.InitializeResult:
67
+ return types.InitializeResult(
68
+ capabilities=types.ServerCapabilities(
69
+ text_document_sync=types.TextDocumentSyncOptions(
70
+ open_close=True,
71
+ change=types.TextDocumentSyncKind.Incremental,
72
+ save=types.SaveOptions(include_text=True),
73
+ ),
74
+ hover_provider=True,
75
+ definition_provider=True,
76
+ references_provider=True,
77
+ document_symbol_provider=True,
78
+ workspace_symbol_provider=types.WorkspaceSymbolOptions(resolve_provider=False),
79
+ document_formatting_provider=True,
80
+ code_action_provider=types.CodeActionOptions(
81
+ code_action_kinds=[types.CodeActionKind.QuickFix],
82
+ resolve_provider=False,
83
+ ),
84
+ rename_provider=types.RenameOptions(prepare_provider=True),
85
+ semantic_tokens_provider=types.SemanticTokensOptions(
86
+ legend=semantic_tokens_legend(),
87
+ full=True,
88
+ range=False,
89
+ ),
90
+ document_highlight_provider=True,
91
+ folding_range_provider=True,
92
+ inlay_hint_provider=types.InlayHintOptions(resolve_provider=False),
93
+ completion_provider=types.CompletionOptions(
94
+ trigger_characters=["@", "."],
95
+ ),
96
+ workspace=types.WorkspaceOptions(
97
+ workspace_folders=types.WorkspaceFoldersServerCapabilities(
98
+ supported=True,
99
+ change_notifications=True,
100
+ )
101
+ ),
102
+ )
103
+ )
104
+
105
+
106
+ @server.feature(types.INITIALIZED)
107
+ def initialized(ls: ModelableLanguageServer, _params: types.InitializedParams) -> None:
108
+ scan_paths: list[Path] = []
109
+ for folder in (ls.workspace.folders or {}).values():
110
+ folder_path = uri_to_path(folder.uri)
111
+ if folder_path is not None and (folder_path / "workspace.mdl").exists():
112
+ scan_paths.append(folder_path)
113
+ if not scan_paths and ls._root_uri:
114
+ root_path = uri_to_path(ls._root_uri)
115
+ if root_path is not None and (root_path / "workspace.mdl").exists():
116
+ scan_paths.append(root_path)
117
+ loaded_uris: list[str] = []
118
+ for path in scan_paths:
119
+ index = _get_index_for_root(ls, path)
120
+ loaded_uris.extend(_scan_and_load_path(ls, path, index))
121
+ for uri in loaded_uris:
122
+ _publish_document_diagnostics(ls, uri, ls.index_for(uri))
123
+
124
+
125
+ @server.feature(types.WORKSPACE_DID_CHANGE_WATCHED_FILES)
126
+ def did_change_watched_files(ls: ModelableLanguageServer, params: types.DidChangeWatchedFilesParams) -> None:
127
+ for change in params.changes:
128
+ uri = change.uri
129
+ index = ls.index_for(uri)
130
+ if change.type == types.FileChangeType.Deleted:
131
+ index.remove_document(uri)
132
+ else:
133
+ path = uri_to_path(uri)
134
+ if path is not None and path.exists():
135
+ with contextlib.suppress(Exception):
136
+ index.load_background_document(uri, path.read_text(encoding="utf-8"))
137
+ _publish_document_diagnostics(ls, uri, index)
138
+
139
+
140
+ @server.feature(types.TEXT_DOCUMENT_DID_OPEN)
141
+ def did_open(ls: ModelableLanguageServer, params: types.DidOpenTextDocumentParams) -> None:
142
+ uri = params.text_document.uri
143
+ path = uri_to_path(uri)
144
+ if path is not None:
145
+ scan_root = _find_workspace_root(path) or path.parent
146
+ if scan_root not in ls._scanned_dirs:
147
+ index = _get_index_for_root(ls, scan_root)
148
+ _scan_and_load_path(ls, scan_root, index)
149
+ index = ls.index_for(uri)
150
+ index.upsert_document(uri, params.text_document.text)
151
+ _publish_document_diagnostics(ls, uri, index)
152
+
153
+
154
+ @server.feature(types.TEXT_DOCUMENT_DID_CHANGE)
155
+ async def did_change(ls: ModelableLanguageServer, params: types.DidChangeTextDocumentParams) -> None:
156
+ document = ls.workspace.get_text_document(params.text_document.uri)
157
+ uri = document.uri
158
+ ls.index_for(uri).upsert_document(uri, document.source)
159
+
160
+ existing = ls._debounce_tasks.get(uri)
161
+ if existing is not None and not existing.done():
162
+ existing.cancel()
163
+
164
+ async def _delayed_publish() -> None:
165
+ await asyncio.sleep(_DEBOUNCE_DELAY)
166
+ _publish_document_diagnostics(ls, uri, ls.index_for(uri))
167
+
168
+ ls._debounce_tasks[uri] = asyncio.ensure_future(_delayed_publish())
169
+
170
+
171
+ @server.feature(types.TEXT_DOCUMENT_DID_CLOSE)
172
+ def did_close(ls: ModelableLanguageServer, params: types.DidCloseTextDocumentParams) -> None:
173
+ uri = params.text_document.uri
174
+ existing = ls._debounce_tasks.pop(uri, None)
175
+ if existing is not None and not existing.done():
176
+ existing.cancel()
177
+ ls.index_for(uri).close_document(uri)
178
+ ls.text_document_publish_diagnostics(types.PublishDiagnosticsParams(uri=uri, diagnostics=[]))
179
+
180
+
181
+ @server.feature(types.TEXT_DOCUMENT_HOVER)
182
+ def hover(ls: ModelableLanguageServer, params: types.HoverParams) -> types.Hover | None:
183
+ return build_hover(
184
+ ls.index_for(params.text_document.uri),
185
+ params.text_document.uri,
186
+ params.position.line,
187
+ params.position.character,
188
+ )
189
+
190
+
191
+ @server.feature(types.TEXT_DOCUMENT_DEFINITION)
192
+ def definition(
193
+ ls: ModelableLanguageServer, params: types.DefinitionParams
194
+ ) -> types.Location | list[types.Location] | None:
195
+ return build_definition(
196
+ ls.index_for(params.text_document.uri),
197
+ params.text_document.uri,
198
+ params.position.line,
199
+ params.position.character,
200
+ )
201
+
202
+
203
+ @server.feature(types.TEXT_DOCUMENT_REFERENCES)
204
+ def references(ls: ModelableLanguageServer, params: types.ReferenceParams) -> list[types.Location] | None:
205
+ return build_references(
206
+ ls.index_for(params.text_document.uri),
207
+ params.text_document.uri,
208
+ params.position.line,
209
+ params.position.character,
210
+ params.context.include_declaration,
211
+ )
212
+
213
+
214
+ @server.feature(types.TEXT_DOCUMENT_COMPLETION)
215
+ def completion(ls: ModelableLanguageServer, params: types.CompletionParams) -> types.CompletionList:
216
+ return build_completion(
217
+ ls.index_for(params.text_document.uri),
218
+ params.text_document.uri,
219
+ params.position.line,
220
+ params.position.character,
221
+ )
222
+
223
+
224
+ @server.feature(types.TEXT_DOCUMENT_DOCUMENT_SYMBOL)
225
+ def document_symbol(
226
+ ls: ModelableLanguageServer, params: types.DocumentSymbolParams
227
+ ) -> list[types.DocumentSymbol] | None:
228
+ return build_document_symbols(ls.index_for(params.text_document.uri), params.text_document.uri)
229
+
230
+
231
+ @server.feature(types.WORKSPACE_SYMBOL)
232
+ def workspace_symbol(
233
+ ls: ModelableLanguageServer, params: types.WorkspaceSymbolParams
234
+ ) -> list[types.WorkspaceSymbol] | None:
235
+ results: list[types.WorkspaceSymbol] = []
236
+ seen_roots: set[int] = set()
237
+ for index in [ls.index, *ls._indexes.values()]:
238
+ if id(index) not in seen_roots:
239
+ seen_roots.add(id(index))
240
+ found = build_workspace_symbols(index, params.query)
241
+ if found:
242
+ results.extend(found)
243
+ return results or None
244
+
245
+
246
+ @server.feature(types.TEXT_DOCUMENT_FORMATTING)
247
+ def document_formatting(
248
+ ls: ModelableLanguageServer, params: types.DocumentFormattingParams
249
+ ) -> list[types.TextEdit] | None:
250
+ return build_document_formatting(
251
+ ls.index_for(params.text_document.uri),
252
+ params.text_document.uri,
253
+ params.options.tab_size,
254
+ params.options.insert_spaces,
255
+ )
256
+
257
+
258
+ @server.feature(types.TEXT_DOCUMENT_CODE_ACTION)
259
+ def code_action(ls: ModelableLanguageServer, params: types.CodeActionParams) -> list[types.CodeAction] | None:
260
+ return build_code_actions(
261
+ ls.index_for(params.text_document.uri),
262
+ params.text_document.uri,
263
+ params.range.start.line,
264
+ params.range.start.character,
265
+ list(params.context.diagnostics),
266
+ )
267
+
268
+
269
+ @server.feature(types.TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL)
270
+ def semantic_tokens_full(
271
+ ls: ModelableLanguageServer, params: types.SemanticTokensParams
272
+ ) -> types.SemanticTokens | None:
273
+ source = ls.index_for(params.text_document.uri).documents.get(params.text_document.uri)
274
+ if source is None:
275
+ return types.SemanticTokens(data=[])
276
+ return build_semantic_tokens(source.text)
277
+
278
+
279
+ @server.feature(types.TEXT_DOCUMENT_PREPARE_RENAME)
280
+ def prepare_rename(ls: ModelableLanguageServer, params: types.PrepareRenameParams) -> types.Range | None:
281
+ return build_prepare_rename(
282
+ ls.index_for(params.text_document.uri),
283
+ params.text_document.uri,
284
+ params.position.line,
285
+ params.position.character,
286
+ )
287
+
288
+
289
+ @server.feature(types.TEXT_DOCUMENT_RENAME)
290
+ def rename(ls: ModelableLanguageServer, params: types.RenameParams) -> types.WorkspaceEdit | None:
291
+ return build_rename(
292
+ ls.index_for(params.text_document.uri),
293
+ params.text_document.uri,
294
+ params.position.line,
295
+ params.position.character,
296
+ params.new_name,
297
+ )
298
+
299
+
300
+ @server.feature(types.TEXT_DOCUMENT_DOCUMENT_HIGHLIGHT)
301
+ def document_highlight(
302
+ ls: ModelableLanguageServer, params: types.DocumentHighlightParams
303
+ ) -> list[types.DocumentHighlight] | None:
304
+ return build_document_highlight(
305
+ ls.index_for(params.text_document.uri),
306
+ params.text_document.uri,
307
+ params.position.line,
308
+ params.position.character,
309
+ )
310
+
311
+
312
+ @server.feature(types.TEXT_DOCUMENT_FOLDING_RANGE)
313
+ def folding_range(ls: ModelableLanguageServer, params: types.FoldingRangeParams) -> list[types.FoldingRange] | None:
314
+ return build_folding_ranges(ls.index_for(params.text_document.uri), params.text_document.uri)
315
+
316
+
317
+ @server.feature(types.TEXT_DOCUMENT_INLAY_HINT)
318
+ def inlay_hint(ls: ModelableLanguageServer, params: types.InlayHintParams) -> list[types.InlayHint] | None:
319
+ return build_inlay_hints(ls.index_for(params.text_document.uri), params.text_document.uri, params.range)
320
+
321
+
322
+ def _find_workspace_root(file_path: Path) -> Path | None:
323
+ """Walk up the ancestor chain to find the nearest directory containing workspace.mdl."""
324
+ directory = file_path.parent
325
+ while True:
326
+ if (directory / "workspace.mdl").exists():
327
+ return directory
328
+ parent = directory.parent
329
+ if parent == directory:
330
+ return None
331
+ directory = parent
332
+
333
+
334
+ def _get_index_for_root(ls: ModelableLanguageServer, root: Path) -> LspWorkspaceIndex:
335
+ if root not in ls._indexes:
336
+ ls._indexes[root] = LspWorkspaceIndex()
337
+ return ls._indexes[root]
338
+
339
+
340
+ def _scan_and_load_path(ls: ModelableLanguageServer, folder_path: Path, index: LspWorkspaceIndex) -> list[str]:
341
+ ls._scanned_dirs.add(folder_path)
342
+ loaded: list[str] = []
343
+ for mdl_file in sorted(folder_path.rglob("*.mdl")):
344
+ file_uri = mdl_file.as_uri()
345
+ ls._scanned_dirs.add(mdl_file.parent)
346
+ try:
347
+ index.load_background_document(file_uri, mdl_file.read_text(encoding="utf-8"))
348
+ loaded.append(file_uri)
349
+ except Exception:
350
+ pass
351
+ return loaded
352
+
353
+
354
+ def _publish_document_diagnostics(ls: ModelableLanguageServer, uri: str, index: LspWorkspaceIndex) -> None:
355
+ workspace = index.workspace
356
+ if workspace is None:
357
+ ls.text_document_publish_diagnostics(types.PublishDiagnosticsParams(uri=uri, diagnostics=[]))
358
+ return
359
+ diagnostics = []
360
+ for diagnostic in workspace.errors:
361
+ if diagnostic.path == uri or diagnostic.path == "<workspace>":
362
+ diagnostics.append(diagnostic)
363
+ diagnostics.extend(build_import_diagnostics(index, uri))
364
+ ls.text_document_publish_diagnostics(
365
+ types.PublishDiagnosticsParams(uri=uri, diagnostics=to_lsp_diagnostics(diagnostics))
366
+ )
367
+
368
+
369
+ def main() -> None:
370
+ server.start_io()
@@ -0,0 +1,83 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path
5
+ from urllib.parse import unquote, urlparse
6
+
7
+ from modelable.compiler.workspace import (
8
+ Workspace,
9
+ WorkspaceDocumentSource,
10
+ load_workspace_from_sources,
11
+ )
12
+
13
+
14
+ @dataclass
15
+ class LspWorkspaceIndex:
16
+ documents: dict[str, WorkspaceDocumentSource] = field(default_factory=dict)
17
+ workspace: Workspace | None = None
18
+ _user_opened: set[str] = field(default_factory=set, init=False, repr=False)
19
+
20
+ def upsert_document(self, uri: str, text: str) -> Workspace | None:
21
+ self._user_opened.add(uri)
22
+ source = WorkspaceDocumentSource(
23
+ path=uri_to_path(uri),
24
+ uri=uri,
25
+ text=text,
26
+ )
27
+ current = self.documents.get(uri)
28
+ if current is not None and current.text == text:
29
+ return self.workspace
30
+ self.documents[uri] = source
31
+ return self.rebuild()
32
+
33
+ def load_background_document(self, uri: str, text: str) -> None:
34
+ """Load a document from disk without marking it as user-opened."""
35
+ if uri in self._user_opened:
36
+ return
37
+ source = WorkspaceDocumentSource(path=uri_to_path(uri), uri=uri, text=text)
38
+ current = self.documents.get(uri)
39
+ if current is not None and current.text == text:
40
+ return
41
+ self.documents[uri] = source
42
+ self.rebuild()
43
+
44
+ def close_document(self, uri: str) -> Workspace | None:
45
+ """Called when the user closes a tab — revert to on-disk content if available."""
46
+ self._user_opened.discard(uri)
47
+ path = uri_to_path(uri)
48
+ if path is not None and path.exists():
49
+ try:
50
+ text = path.read_text(encoding="utf-8")
51
+ source = WorkspaceDocumentSource(path=path, uri=uri, text=text)
52
+ self.documents[uri] = source
53
+ return self.rebuild()
54
+ except Exception:
55
+ pass
56
+ return self.remove_document(uri)
57
+
58
+ def remove_document(self, uri: str) -> Workspace | None:
59
+ self._user_opened.discard(uri)
60
+ if uri in self.documents:
61
+ del self.documents[uri]
62
+ return self.rebuild()
63
+ return self.workspace
64
+
65
+ def rebuild(self) -> Workspace | None:
66
+ if not self.documents:
67
+ self.workspace = None
68
+ return None
69
+ ordered_sources = [self.documents[uri] for uri in sorted(self.documents)]
70
+ self.workspace = load_workspace_from_sources(ordered_sources)
71
+ return self.workspace
72
+
73
+
74
+ def uri_to_path(uri: str) -> Path | None:
75
+ parsed = urlparse(uri)
76
+ if parsed.scheme != "file":
77
+ return None
78
+ path = unquote(parsed.path)
79
+ if parsed.netloc:
80
+ path = f"//{parsed.netloc}{path}"
81
+ elif len(path) >= 3 and path.startswith("/") and path[2] == ":":
82
+ path = path[1:]
83
+ return Path(path)
@@ -0,0 +1,104 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from lsprotocol import types
6
+
7
+ from modelable.lsp.document_symbols import build_document_symbols
8
+ from modelable.lsp.workspace import LspWorkspaceIndex
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class _WorkspaceSymbolEntry:
13
+ name: str
14
+ kind: types.SymbolKind
15
+ uri: str
16
+ start_line: int
17
+ start_character: int
18
+ end_line: int
19
+ end_character: int
20
+ container_name: str | None
21
+
22
+
23
+ def build_workspace_symbols(
24
+ index: LspWorkspaceIndex,
25
+ query: str,
26
+ ) -> list[types.WorkspaceSymbol] | None:
27
+ workspace = index.workspace
28
+ if workspace is None:
29
+ return None
30
+
31
+ entries: list[_WorkspaceSymbolEntry] = []
32
+ lowered_query = query.strip().lower()
33
+
34
+ for source in workspace.sources:
35
+ symbols = build_document_symbols(index, source.uri)
36
+ if not symbols:
37
+ continue
38
+ entries.extend(_flatten_symbols(source.uri, symbols))
39
+
40
+ filtered = [
41
+ entry
42
+ for entry in entries
43
+ if not lowered_query
44
+ or lowered_query in entry.name.lower()
45
+ or (entry.container_name is not None and lowered_query in entry.container_name.lower())
46
+ ]
47
+ filtered.sort(key=lambda item: (item.name.lower(), item.container_name or "", item.uri))
48
+ return [
49
+ types.WorkspaceSymbol(
50
+ name=entry.name,
51
+ kind=entry.kind,
52
+ location=types.Location(
53
+ uri=entry.uri,
54
+ range=types.Range(
55
+ start=types.Position(line=entry.start_line, character=entry.start_character),
56
+ end=types.Position(line=entry.end_line, character=entry.end_character),
57
+ ),
58
+ ),
59
+ container_name=entry.container_name,
60
+ )
61
+ for entry in filtered
62
+ ]
63
+
64
+
65
+ def _flatten_symbols(uri: str, symbols: list[types.DocumentSymbol]) -> list[_WorkspaceSymbolEntry]:
66
+ entries: list[_WorkspaceSymbolEntry] = []
67
+ for symbol in symbols:
68
+ entries.append(
69
+ _WorkspaceSymbolEntry(
70
+ name=symbol.name,
71
+ kind=symbol.kind,
72
+ uri=uri,
73
+ start_line=symbol.selection_range.start.line,
74
+ start_character=symbol.selection_range.start.character,
75
+ end_line=symbol.selection_range.end.line,
76
+ end_character=symbol.selection_range.end.character,
77
+ container_name=None,
78
+ )
79
+ )
80
+ for child in symbol.children or []:
81
+ entries.extend(_flatten_child(uri, symbol.name, child))
82
+ return entries
83
+
84
+
85
+ def _flatten_child(
86
+ uri: str,
87
+ container_name: str,
88
+ symbol: types.DocumentSymbol,
89
+ ) -> list[_WorkspaceSymbolEntry]:
90
+ entries = [
91
+ _WorkspaceSymbolEntry(
92
+ name=symbol.name,
93
+ kind=symbol.kind,
94
+ uri=uri,
95
+ start_line=symbol.selection_range.start.line,
96
+ start_character=symbol.selection_range.start.character,
97
+ end_line=symbol.selection_range.end.line,
98
+ end_character=symbol.selection_range.end.character,
99
+ container_name=container_name,
100
+ )
101
+ ]
102
+ for child in symbol.children or []:
103
+ entries.extend(_flatten_child(uri, symbol.name, child))
104
+ return entries
@@ -0,0 +1,94 @@
1
+ from modelable.parser.ir import (
2
+ AccessBlock,
3
+ AccessGrant,
4
+ AiConfig,
5
+ AnnClassification,
6
+ AnnDeprecated,
7
+ AnnKey,
8
+ AnnOwner,
9
+ AnnPii,
10
+ AnnServer,
11
+ AnnWire,
12
+ ArrayType,
13
+ AutoProjectionDecl,
14
+ AutoProjectionTarget,
15
+ BindingDef,
16
+ ChangeKind,
17
+ ClassificationLevel,
18
+ ComputedMapping,
19
+ DecimalType,
20
+ DirectMapping,
21
+ DomainDef,
22
+ EnumType,
23
+ FieldDef,
24
+ FieldMapping,
25
+ GenerateTarget,
26
+ JoinRef,
27
+ MapType,
28
+ MdlFile,
29
+ ModelKind,
30
+ ModelVersion,
31
+ NamedType,
32
+ ObjectType,
33
+ PrimitiveType,
34
+ ProjectionField,
35
+ ProjectionVersion,
36
+ RefType,
37
+ SourceRef,
38
+ ValidationError,
39
+ VersionExact,
40
+ VersionMin,
41
+ VersionRange,
42
+ WireTargetHint,
43
+ WorkspaceDef,
44
+ )
45
+ from modelable.parser.parse import parse_file, parse_file_to_ir, parse_text, parse_text_to_ir
46
+
47
+ __all__ = [
48
+ "AccessBlock",
49
+ "AccessGrant",
50
+ "AiConfig",
51
+ "AnnClassification",
52
+ "AnnDeprecated",
53
+ "AnnKey",
54
+ "AnnOwner",
55
+ "AnnPii",
56
+ "AnnServer",
57
+ "AnnWire",
58
+ "ArrayType",
59
+ "AutoProjectionDecl",
60
+ "AutoProjectionTarget",
61
+ "BindingDef",
62
+ "ChangeKind",
63
+ "ClassificationLevel",
64
+ "ComputedMapping",
65
+ "DecimalType",
66
+ "DirectMapping",
67
+ "DomainDef",
68
+ "EnumType",
69
+ "FieldDef",
70
+ "FieldMapping",
71
+ "GenerateTarget",
72
+ "JoinRef",
73
+ "MapType",
74
+ "MdlFile",
75
+ "ModelKind",
76
+ "ModelVersion",
77
+ "NamedType",
78
+ "ObjectType",
79
+ "PrimitiveType",
80
+ "ProjectionField",
81
+ "ProjectionVersion",
82
+ "RefType",
83
+ "SourceRef",
84
+ "ValidationError",
85
+ "VersionExact",
86
+ "VersionMin",
87
+ "VersionRange",
88
+ "WireTargetHint",
89
+ "WorkspaceDef",
90
+ "parse_file",
91
+ "parse_file_to_ir",
92
+ "parse_text",
93
+ "parse_text_to_ir",
94
+ ]