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.
- modelable/__init__.py +1 -0
- modelable/__main__.py +3 -0
- modelable/_pydantic_py314_compat.py +31 -0
- modelable/cli.py +41 -0
- modelable/commands/__init__.py +1 -0
- modelable/commands/apicurio.py +84 -0
- modelable/commands/codegen.py +241 -0
- modelable/commands/common.py +43 -0
- modelable/commands/compile.py +237 -0
- modelable/commands/create.py +164 -0
- modelable/commands/diff.py +82 -0
- modelable/commands/graph.py +53 -0
- modelable/commands/llm.py +564 -0
- modelable/commands/lsp.py +15 -0
- modelable/commands/runtime.py +37 -0
- modelable/commands/scenario.py +104 -0
- modelable/commands/spec.py +197 -0
- modelable/commands/workspace.py +240 -0
- modelable/compat/__init__.py +11 -0
- modelable/compat/checker.py +179 -0
- modelable/compat/diff.py +169 -0
- modelable/compiler/__init__.py +3 -0
- modelable/compiler/compiler.py +19 -0
- modelable/compiler/workspace.py +346 -0
- modelable/diagnostics/__init__.py +3 -0
- modelable/diagnostics/model.py +27 -0
- modelable/emitters/__init__.py +0 -0
- modelable/emitters/base.py +22 -0
- modelable/emitters/csharp.py +245 -0
- modelable/emitters/dbt_yaml.py +290 -0
- modelable/emitters/diagnostics.py +25 -0
- modelable/emitters/fhir.py +694 -0
- modelable/emitters/fhir_validator.py +36 -0
- modelable/emitters/go.py +334 -0
- modelable/emitters/java.py +264 -0
- modelable/emitters/json_schema.py +458 -0
- modelable/emitters/markdown.py +252 -0
- modelable/emitters/odcs.py +355 -0
- modelable/emitters/openlineage.py +315 -0
- modelable/emitters/openmetadata.py +258 -0
- modelable/emitters/python.py +282 -0
- modelable/emitters/rust.py +643 -0
- modelable/emitters/shapes.py +261 -0
- modelable/emitters/sql.py +266 -0
- modelable/emitters/targets.py +141 -0
- modelable/emitters/typescript.py +352 -0
- modelable/expressions/__init__.py +0 -0
- modelable/expressions/cel.py +547 -0
- modelable/governance/__init__.py +3 -0
- modelable/governance/checker.py +271 -0
- modelable/governance/por.py +46 -0
- modelable/grammar/__init__.py +1 -0
- modelable/grammar/modelable.lark +257 -0
- modelable/graph/__init__.py +5 -0
- modelable/graph/export.py +442 -0
- modelable/llm/__init__.py +43 -0
- modelable/llm/chat.py +255 -0
- modelable/llm/config.py +87 -0
- modelable/llm/context.py +194 -0
- modelable/llm/engine.py +976 -0
- modelable/llm/importers.py +1077 -0
- modelable/llm/provenance.py +84 -0
- modelable/llm/providers.py +182 -0
- modelable/llm/qa.py +126 -0
- modelable/llm/recommendations.py +33 -0
- modelable/llm/redaction.py +19 -0
- modelable/llm/render.py +279 -0
- modelable/llm/update_plan.py +101 -0
- modelable/llm/validation_help.py +10 -0
- modelable/lsp/__init__.py +3 -0
- modelable/lsp/__main__.py +4 -0
- modelable/lsp/code_actions.py +210 -0
- modelable/lsp/completion.py +480 -0
- modelable/lsp/definition.py +343 -0
- modelable/lsp/diagnostics.py +31 -0
- modelable/lsp/document_symbols.py +197 -0
- modelable/lsp/federation.py +261 -0
- modelable/lsp/folding.py +33 -0
- modelable/lsp/formatting.py +64 -0
- modelable/lsp/highlight.py +30 -0
- modelable/lsp/hover.py +370 -0
- modelable/lsp/inlay_hints.py +158 -0
- modelable/lsp/references.py +511 -0
- modelable/lsp/rename.py +564 -0
- modelable/lsp/semantic_tokens.py +412 -0
- modelable/lsp/server.py +370 -0
- modelable/lsp/workspace.py +83 -0
- modelable/lsp/workspace_symbols.py +104 -0
- modelable/parser/__init__.py +94 -0
- modelable/parser/ir.py +451 -0
- modelable/parser/parse.py +47 -0
- modelable/parser/transformer.py +798 -0
- modelable/parser/wire.py +68 -0
- modelable/planner/__init__.py +0 -0
- modelable/planner/lineage.py +91 -0
- modelable/planner/planner.py +134 -0
- modelable/planner/plans.py +122 -0
- modelable/py.typed +0 -0
- modelable/registry/__init__.py +9 -0
- modelable/registry/apicurio.py +166 -0
- modelable/registry/base.py +18 -0
- modelable/registry/factory.py +18 -0
- modelable/registry/index.py +419 -0
- modelable/registry/local.py +26 -0
- modelable/registry/oci.py +22 -0
- modelable/registry/resolver.py +213 -0
- modelable/registry/schema.sql +119 -0
- modelable/registry/signature.py +26 -0
- modelable/release.py +125 -0
- modelable/runtime/__init__.py +5 -0
- modelable/runtime/adapter/__init__.py +17 -0
- modelable/runtime/adapter/base.py +18 -0
- modelable/runtime/adapter/postgres.py +82 -0
- modelable/specs/__init__.py +23 -0
- modelable/specs/tracking.py +220 -0
- modelable/validation/__init__.py +3 -0
- modelable/validation/semantic.py +659 -0
- modelable-1.0.0.dist-info/METADATA +61 -0
- modelable-1.0.0.dist-info/RECORD +122 -0
- modelable-1.0.0.dist-info/WHEEL +4 -0
- modelable-1.0.0.dist-info/entry_points.txt +2 -0
- modelable-1.0.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from modelable.diagnostics.model import Diagnostic
|
|
7
|
+
from modelable.lsp.workspace import LspWorkspaceIndex
|
|
8
|
+
from modelable.parser.parse import parse_text_to_ir
|
|
9
|
+
from modelable.registry.signature import compute_version_signature
|
|
10
|
+
|
|
11
|
+
_PINNED_IMPORT_PATTERN = re.compile(
|
|
12
|
+
r"^\s*import\s+domain\s+(?P<domain>[A-Za-z_][A-Za-z0-9_-]*)\s+"
|
|
13
|
+
r"from\s+registry\s+\"(?P<peer>[^\"]+)\""
|
|
14
|
+
r"(?:\s+at\s+(?P<ref_domain>[A-Za-z_][A-Za-z0-9_-]*)\."
|
|
15
|
+
r"(?P<ref_model>[A-Za-z_][A-Za-z0-9_-]*)\s*@\s*"
|
|
16
|
+
r"(?P<version>\d+)\#(?P<signature>[0-9a-fA-F]+))?"
|
|
17
|
+
)
|
|
18
|
+
_PINNED_REFERENCE_PATTERN = re.compile(
|
|
19
|
+
r"^\s*from\s+(?P<domain>[A-Za-z_][A-Za-z0-9_-]*)\."
|
|
20
|
+
r"(?P<model>[A-Za-z_][A-Za-z0-9_-]*)\s*@\s*"
|
|
21
|
+
r"(?P<version>\d+)\#(?P<signature>[0-9a-fA-F]+)\s+as\s+"
|
|
22
|
+
r"(?P<alias>[A-Za-z_][A-Za-z0-9_-]*)"
|
|
23
|
+
)
|
|
24
|
+
_PEERS_BLOCK_PATTERN = re.compile(r"peers\s*:\s*\[(?P<body>.*?)\]", re.DOTALL)
|
|
25
|
+
_PEER_ID_PATTERN = re.compile(r"id\s*:\s*\"(?P<id>[^\"]+)\"")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def build_import_diagnostics(index: LspWorkspaceIndex, uri: str) -> list[Diagnostic]:
|
|
29
|
+
source = index.documents.get(uri)
|
|
30
|
+
if source is None:
|
|
31
|
+
return []
|
|
32
|
+
|
|
33
|
+
diagnostics: list[Diagnostic] = []
|
|
34
|
+
declared_peers = _declared_peer_ids(index)
|
|
35
|
+
mirror_domains = set(mirror_domain_names(index))
|
|
36
|
+
mirror_signatures = _mirror_model_signatures(index)
|
|
37
|
+
|
|
38
|
+
for line_no, line in enumerate(source.text.splitlines()):
|
|
39
|
+
match = _PINNED_IMPORT_PATTERN.match(line)
|
|
40
|
+
if match is None:
|
|
41
|
+
match = _PINNED_REFERENCE_PATTERN.match(line)
|
|
42
|
+
if match is None:
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
domain_name = match.group("domain")
|
|
46
|
+
model_name = match.group("model")
|
|
47
|
+
version = int(match.group("version"))
|
|
48
|
+
signature = match.group("signature")
|
|
49
|
+
actual_signature = mirror_signatures.get((domain_name, model_name, version))
|
|
50
|
+
if actual_signature is None:
|
|
51
|
+
diagnostics.append(
|
|
52
|
+
Diagnostic(
|
|
53
|
+
code="FED",
|
|
54
|
+
message=(
|
|
55
|
+
f"pinned reference '{domain_name}.{model_name}@{version}#{signature}' "
|
|
56
|
+
"is not available in the local mirror cache"
|
|
57
|
+
),
|
|
58
|
+
severity="error",
|
|
59
|
+
path=uri,
|
|
60
|
+
line=line_no + 1,
|
|
61
|
+
column=match.start("signature") + 1,
|
|
62
|
+
end_line=line_no + 1,
|
|
63
|
+
end_column=match.end("signature") + 1,
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
continue
|
|
67
|
+
if signature.lower() != actual_signature.lower():
|
|
68
|
+
diagnostics.append(
|
|
69
|
+
Diagnostic(
|
|
70
|
+
code="FED",
|
|
71
|
+
message=(
|
|
72
|
+
f"pinned reference '{domain_name}.{model_name}@{version}#{signature}' "
|
|
73
|
+
f"does not match local mirror signature '{actual_signature}'"
|
|
74
|
+
),
|
|
75
|
+
severity="error",
|
|
76
|
+
path=uri,
|
|
77
|
+
line=line_no + 1,
|
|
78
|
+
column=match.start("signature") + 1,
|
|
79
|
+
end_line=line_no + 1,
|
|
80
|
+
end_column=match.end("signature") + 1,
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
domain_name = match.group("domain")
|
|
86
|
+
peer_id = match.group("peer")
|
|
87
|
+
|
|
88
|
+
if declared_peers is not None and peer_id not in declared_peers:
|
|
89
|
+
diagnostics.append(
|
|
90
|
+
Diagnostic(
|
|
91
|
+
code="FED",
|
|
92
|
+
message=f"import peer '{peer_id}' is not declared in workspace.mdl",
|
|
93
|
+
severity="warning",
|
|
94
|
+
path=uri,
|
|
95
|
+
line=line_no + 1,
|
|
96
|
+
column=match.start("peer") + 1,
|
|
97
|
+
end_line=line_no + 1,
|
|
98
|
+
end_column=match.end("peer") + 1,
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if domain_name not in mirror_domains:
|
|
103
|
+
diagnostics.append(
|
|
104
|
+
Diagnostic(
|
|
105
|
+
code="FED",
|
|
106
|
+
message=f"import domain '{domain_name}' is not available in the local mirror cache",
|
|
107
|
+
severity="error",
|
|
108
|
+
path=uri,
|
|
109
|
+
line=line_no + 1,
|
|
110
|
+
column=match.start("domain") + 1,
|
|
111
|
+
end_line=line_no + 1,
|
|
112
|
+
end_column=match.end("domain") + 1,
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
ref_domain = match.group("ref_domain")
|
|
117
|
+
if ref_domain is not None:
|
|
118
|
+
ref_model = match.group("ref_model")
|
|
119
|
+
version = int(match.group("version"))
|
|
120
|
+
signature = match.group("signature")
|
|
121
|
+
actual_signature = mirror_signatures.get((ref_domain, ref_model, version))
|
|
122
|
+
if actual_signature is None:
|
|
123
|
+
diagnostics.append(
|
|
124
|
+
Diagnostic(
|
|
125
|
+
code="FED",
|
|
126
|
+
message=(
|
|
127
|
+
f"pinned reference '{ref_domain}.{ref_model}@{version}#{signature}' "
|
|
128
|
+
"is not available in the local mirror cache"
|
|
129
|
+
),
|
|
130
|
+
severity="error",
|
|
131
|
+
path=uri,
|
|
132
|
+
line=line_no + 1,
|
|
133
|
+
column=match.start("signature") + 1,
|
|
134
|
+
end_line=line_no + 1,
|
|
135
|
+
end_column=match.end("signature") + 1,
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
continue
|
|
139
|
+
if signature.lower() != actual_signature.lower():
|
|
140
|
+
diagnostics.append(
|
|
141
|
+
Diagnostic(
|
|
142
|
+
code="FED",
|
|
143
|
+
message=(
|
|
144
|
+
f"pinned reference '{ref_domain}.{ref_model}@{version}#{signature}' "
|
|
145
|
+
f"does not match local mirror signature '{actual_signature}'"
|
|
146
|
+
),
|
|
147
|
+
severity="error",
|
|
148
|
+
path=uri,
|
|
149
|
+
line=line_no + 1,
|
|
150
|
+
column=match.start("signature") + 1,
|
|
151
|
+
end_line=line_no + 1,
|
|
152
|
+
end_column=match.end("signature") + 1,
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
return diagnostics
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def mirror_domain_names(index: LspWorkspaceIndex) -> list[str]:
|
|
160
|
+
names: set[str] = set()
|
|
161
|
+
for source in _mirror_sources(index):
|
|
162
|
+
for domain in source.domains:
|
|
163
|
+
names.add(domain.name)
|
|
164
|
+
return sorted(names)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def mirror_reference_names(index: LspWorkspaceIndex) -> list[tuple[str, str]]:
|
|
168
|
+
names: set[tuple[str, str]] = set()
|
|
169
|
+
for source in _mirror_sources(index):
|
|
170
|
+
for domain in source.domains:
|
|
171
|
+
for model_name in domain.models:
|
|
172
|
+
names.add((domain.name, model_name))
|
|
173
|
+
for projection_name in domain.projections:
|
|
174
|
+
names.add((domain.name, projection_name))
|
|
175
|
+
return sorted(names)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def mirror_model_versions(index: LspWorkspaceIndex) -> list[tuple[str, str, int]]:
|
|
179
|
+
versions: set[tuple[str, str, int]] = set()
|
|
180
|
+
for source in _mirror_sources(index):
|
|
181
|
+
for domain in source.domains:
|
|
182
|
+
for model_name, model_versions in domain.models.items():
|
|
183
|
+
for version in model_versions:
|
|
184
|
+
versions.add((domain.name, model_name, version.version))
|
|
185
|
+
return sorted(versions)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def mirror_field_names(index: LspWorkspaceIndex, domain_name: str, model_name: str, version: int) -> list[str]:
|
|
189
|
+
names: set[str] = set()
|
|
190
|
+
for source in _mirror_sources(index):
|
|
191
|
+
for domain in source.domains:
|
|
192
|
+
if domain.name != domain_name:
|
|
193
|
+
continue
|
|
194
|
+
versions = domain.models.get(model_name)
|
|
195
|
+
if versions is None:
|
|
196
|
+
continue
|
|
197
|
+
for item in versions:
|
|
198
|
+
if item.version != version:
|
|
199
|
+
continue
|
|
200
|
+
for field in item.fields:
|
|
201
|
+
names.add(field.name)
|
|
202
|
+
return sorted(names)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _declared_peer_ids(index: LspWorkspaceIndex) -> set[str] | None:
|
|
206
|
+
workspace_text = None
|
|
207
|
+
for source in index.documents.values():
|
|
208
|
+
if source.path is not None and source.path.name == "workspace.mdl":
|
|
209
|
+
workspace_text = source.text
|
|
210
|
+
break
|
|
211
|
+
if workspace_text is None:
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
match = _PEERS_BLOCK_PATTERN.search(workspace_text)
|
|
215
|
+
if match is None:
|
|
216
|
+
return set()
|
|
217
|
+
|
|
218
|
+
return {item.group("id") for item in _PEER_ID_PATTERN.finditer(match.group("body"))}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _mirror_sources(index: LspWorkspaceIndex):
|
|
222
|
+
root = _workspace_root(index)
|
|
223
|
+
if root is None:
|
|
224
|
+
return []
|
|
225
|
+
|
|
226
|
+
mirror_root = root / ".modelable" / "mirror"
|
|
227
|
+
if not mirror_root.exists():
|
|
228
|
+
return []
|
|
229
|
+
|
|
230
|
+
parsed_sources = []
|
|
231
|
+
for path in sorted(mirror_root.rglob("*.mdl"), key=lambda item: item.as_posix()):
|
|
232
|
+
try:
|
|
233
|
+
parsed_sources.append(parse_text_to_ir(path.read_text(encoding="utf-8"), path=path))
|
|
234
|
+
except Exception:
|
|
235
|
+
continue
|
|
236
|
+
return parsed_sources
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _mirror_model_signatures(index: LspWorkspaceIndex) -> dict[tuple[str, str, int], str]:
|
|
240
|
+
signatures: dict[tuple[str, str, int], str] = {}
|
|
241
|
+
for source in _mirror_sources(index):
|
|
242
|
+
for domain in source.domains:
|
|
243
|
+
for model_name, versions in domain.models.items():
|
|
244
|
+
for version in versions:
|
|
245
|
+
signatures[(domain.name, model_name, version.version)] = compute_version_signature(
|
|
246
|
+
domain.name,
|
|
247
|
+
model_name,
|
|
248
|
+
version,
|
|
249
|
+
)
|
|
250
|
+
return signatures
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _workspace_root(index: LspWorkspaceIndex) -> Path | None:
|
|
254
|
+
paths = [source.path for source in index.documents.values() if source.path is not None]
|
|
255
|
+
if not paths:
|
|
256
|
+
return None
|
|
257
|
+
|
|
258
|
+
workspace_files = [path for path in paths if path.name == "workspace.mdl"]
|
|
259
|
+
if workspace_files:
|
|
260
|
+
return workspace_files[0].parent
|
|
261
|
+
return paths[0].parent
|
modelable/lsp/folding.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from lsprotocol import types
|
|
4
|
+
|
|
5
|
+
from modelable.lsp.workspace import LspWorkspaceIndex
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def build_folding_ranges(index: LspWorkspaceIndex, uri: str) -> list[types.FoldingRange] | None:
|
|
9
|
+
source = index.documents.get(uri)
|
|
10
|
+
if source is None:
|
|
11
|
+
return None
|
|
12
|
+
return _compute_folding_ranges(source.text)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _compute_folding_ranges(text: str) -> list[types.FoldingRange]:
|
|
16
|
+
ranges: list[types.FoldingRange] = []
|
|
17
|
+
lines = text.splitlines()
|
|
18
|
+
stack: list[int] = []
|
|
19
|
+
for line_no, line in enumerate(lines):
|
|
20
|
+
for ch in line:
|
|
21
|
+
if ch == "{":
|
|
22
|
+
stack.append(line_no)
|
|
23
|
+
elif ch == "}" and stack:
|
|
24
|
+
start = stack.pop()
|
|
25
|
+
if start != line_no:
|
|
26
|
+
ranges.append(
|
|
27
|
+
types.FoldingRange(
|
|
28
|
+
start_line=start,
|
|
29
|
+
end_line=line_no,
|
|
30
|
+
kind=types.FoldingRangeKind.Region,
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
return ranges
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from lsprotocol import types
|
|
4
|
+
|
|
5
|
+
from modelable.lsp.workspace import LspWorkspaceIndex
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def build_document_formatting(
|
|
9
|
+
index: LspWorkspaceIndex,
|
|
10
|
+
uri: str,
|
|
11
|
+
tab_size: int,
|
|
12
|
+
insert_spaces: bool,
|
|
13
|
+
) -> list[types.TextEdit] | None:
|
|
14
|
+
source = index.documents.get(uri)
|
|
15
|
+
if source is None:
|
|
16
|
+
return None
|
|
17
|
+
|
|
18
|
+
formatted = _format_text(source.text, tab_size=max(tab_size, 1), insert_spaces=insert_spaces)
|
|
19
|
+
if formatted == source.text:
|
|
20
|
+
return []
|
|
21
|
+
|
|
22
|
+
source_lines = source.text.splitlines()
|
|
23
|
+
line_count = max(len(source_lines), 1)
|
|
24
|
+
end_line = line_count - 1
|
|
25
|
+
end_character = len(source_lines[-1]) if source_lines else 0
|
|
26
|
+
return [
|
|
27
|
+
types.TextEdit(
|
|
28
|
+
range=types.Range(
|
|
29
|
+
start=types.Position(line=0, character=0),
|
|
30
|
+
end=types.Position(line=end_line, character=end_character),
|
|
31
|
+
),
|
|
32
|
+
new_text=formatted,
|
|
33
|
+
)
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _format_text(text: str, tab_size: int, insert_spaces: bool) -> str:
|
|
38
|
+
had_trailing_newline = text.endswith("\n")
|
|
39
|
+
lines = text.splitlines()
|
|
40
|
+
if not lines:
|
|
41
|
+
return text
|
|
42
|
+
|
|
43
|
+
indent_unit = " " * tab_size if insert_spaces else "\t"
|
|
44
|
+
depth = 0
|
|
45
|
+
formatted_lines: list[str] = []
|
|
46
|
+
|
|
47
|
+
for line in lines:
|
|
48
|
+
stripped = line.strip()
|
|
49
|
+
if not stripped:
|
|
50
|
+
formatted_lines.append("")
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
leading_close = stripped.startswith("}")
|
|
54
|
+
indent_depth = max(depth - 1, 0) if leading_close else depth
|
|
55
|
+
formatted_lines.append(f"{indent_unit * indent_depth}{stripped}")
|
|
56
|
+
|
|
57
|
+
open_count = stripped.count("{")
|
|
58
|
+
close_count = stripped.count("}")
|
|
59
|
+
depth = max(depth + open_count - close_count, 0)
|
|
60
|
+
|
|
61
|
+
formatted = "\n".join(formatted_lines)
|
|
62
|
+
if had_trailing_newline:
|
|
63
|
+
formatted += "\n"
|
|
64
|
+
return formatted
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from lsprotocol import types
|
|
4
|
+
|
|
5
|
+
from modelable.lsp.references import build_references
|
|
6
|
+
from modelable.lsp.workspace import LspWorkspaceIndex
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def build_document_highlight(
|
|
10
|
+
index: LspWorkspaceIndex,
|
|
11
|
+
uri: str,
|
|
12
|
+
line: int,
|
|
13
|
+
character: int,
|
|
14
|
+
) -> list[types.DocumentHighlight] | None:
|
|
15
|
+
all_refs = build_references(index, uri, line, character, include_declaration=True)
|
|
16
|
+
if all_refs is None:
|
|
17
|
+
return None
|
|
18
|
+
|
|
19
|
+
usage_refs = build_references(index, uri, line, character, include_declaration=False) or []
|
|
20
|
+
usage_keys = {(loc.range.start.line, loc.range.start.character) for loc in usage_refs if loc.uri == uri}
|
|
21
|
+
|
|
22
|
+
highlights: list[types.DocumentHighlight] = []
|
|
23
|
+
for loc in all_refs:
|
|
24
|
+
if loc.uri != uri:
|
|
25
|
+
continue
|
|
26
|
+
key = (loc.range.start.line, loc.range.start.character)
|
|
27
|
+
kind = types.DocumentHighlightKind.Read if key in usage_keys else types.DocumentHighlightKind.Write
|
|
28
|
+
highlights.append(types.DocumentHighlight(range=loc.range, kind=kind))
|
|
29
|
+
|
|
30
|
+
return highlights if highlights else None
|