opencrbasic 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.
Files changed (117) hide show
  1. opencrbasic/__init__.py +3 -0
  2. opencrbasic/__main__.py +5 -0
  3. opencrbasic/analysis.py +184 -0
  4. opencrbasic/bindings.py +1034 -0
  5. opencrbasic/catalog.py +173 -0
  6. opencrbasic/checkconfig.py +144 -0
  7. opencrbasic/checker/__init__.py +977 -0
  8. opencrbasic/checker/cr3xx.py +3118 -0
  9. opencrbasic/checker/ess.py +81 -0
  10. opencrbasic/checker/expressions.py +3581 -0
  11. opencrbasic/checker/instr_data.py +1802 -0
  12. opencrbasic/checker/instr_measure.py +1547 -0
  13. opencrbasic/checker/instr_rules.py +3540 -0
  14. opencrbasic/checker/instructions.py +17 -0
  15. opencrbasic/checker/intervals.py +678 -0
  16. opencrbasic/checker/longconv.py +133 -0
  17. opencrbasic/checker/scan_time.py +580 -0
  18. opencrbasic/checker/storage.py +456 -0
  19. opencrbasic/checker/structs.py +240 -0
  20. opencrbasic/checker/structure.py +6213 -0
  21. opencrbasic/checker/symbols.py +609 -0
  22. opencrbasic/checker/terminals.py +249 -0
  23. opencrbasic/checker/unused.py +186 -0
  24. opencrbasic/checker/var_types.py +564 -0
  25. opencrbasic/checker/warnings.py +661 -0
  26. opencrbasic/checks.py +491 -0
  27. opencrbasic/cli.py +391 -0
  28. opencrbasic/commands.py +128 -0
  29. opencrbasic/compiler.py +320 -0
  30. opencrbasic/consteval.py +327 -0
  31. opencrbasic/data/background_calibration.json +4 -0
  32. opencrbasic/data/before_beginprog.json +99 -0
  33. opencrbasic/data/cr1000x/background_calibration.json +4 -0
  34. opencrbasic/data/cr1000x/before_beginprog.json +138 -0
  35. opencrbasic/data/cr1000x/ess_variables.json +1087 -0
  36. opencrbasic/data/cr1000x/fnotch.json +29 -0
  37. opencrbasic/data/cr1000x/functions.json +1337 -0
  38. opencrbasic/data/cr1000x/instructions.json +4836 -0
  39. opencrbasic/data/cr1000x/measure.json +121 -0
  40. opencrbasic/data/cr1000x/mode.json +111 -0
  41. opencrbasic/data/cr1000x/pipeline_tasks.json +88 -0
  42. opencrbasic/data/cr1000x/settings_fields.json +321 -0
  43. opencrbasic/data/cr1000x/status_members.json +318 -0
  44. opencrbasic/data/cr1000x/symbols.json +872 -0
  45. opencrbasic/data/cr1000x/terminals.json +225 -0
  46. opencrbasic/data/cr300/before_beginprog.json +42 -0
  47. opencrbasic/data/cr300/ess_variables.json +1087 -0
  48. opencrbasic/data/cr300/fnotch.json +22 -0
  49. opencrbasic/data/cr300/functions.json +1261 -0
  50. opencrbasic/data/cr300/instructions.json +2837 -0
  51. opencrbasic/data/cr300/measure.json +33 -0
  52. opencrbasic/data/cr300/mode.json +121 -0
  53. opencrbasic/data/cr300/settings_fields.json +259 -0
  54. opencrbasic/data/cr300/status_members.json +303 -0
  55. opencrbasic/data/cr300/symbols.json +584 -0
  56. opencrbasic/data/cr300/terminals.json +193 -0
  57. opencrbasic/data/cr350/before_beginprog.json +44 -0
  58. opencrbasic/data/cr350/ess_variables.json +1087 -0
  59. opencrbasic/data/cr350/fnotch.json +22 -0
  60. opencrbasic/data/cr350/functions.json +1262 -0
  61. opencrbasic/data/cr350/instructions.json +2848 -0
  62. opencrbasic/data/cr350/measure.json +35 -0
  63. opencrbasic/data/cr350/mode.json +121 -0
  64. opencrbasic/data/cr350/settings_fields.json +292 -0
  65. opencrbasic/data/cr350/status_members.json +303 -0
  66. opencrbasic/data/cr350/symbols.json +572 -0
  67. opencrbasic/data/cr350/terminals.json +190 -0
  68. opencrbasic/data/ess_variables.json +1087 -0
  69. opencrbasic/data/fnotch.json +34 -0
  70. opencrbasic/data/functions.json +1337 -0
  71. opencrbasic/data/instructions.json +4965 -0
  72. opencrbasic/data/measure.json +123 -0
  73. opencrbasic/data/mode.json +121 -0
  74. opencrbasic/data/notices_cr6.json +890 -0
  75. opencrbasic/data/pipeline_tasks.json +84 -0
  76. opencrbasic/data/settings_fields.json +322 -0
  77. opencrbasic/data/status_members.json +319 -0
  78. opencrbasic/data/status_table.json +684 -0
  79. opencrbasic/data/symbols.json +902 -0
  80. opencrbasic/data/terminals.json +195 -0
  81. opencrbasic/data/type_codes.json +249 -0
  82. opencrbasic/diagnostics.py +196 -0
  83. opencrbasic/editing.py +827 -0
  84. opencrbasic/formatting.py +310 -0
  85. opencrbasic/helpdocs.py +677 -0
  86. opencrbasic/hover.py +667 -0
  87. opencrbasic/inlay.py +169 -0
  88. opencrbasic/lexer.py +183 -0
  89. opencrbasic/lsp/__init__.py +5 -0
  90. opencrbasic/lsp/__main__.py +7 -0
  91. opencrbasic/lsp/config.py +202 -0
  92. opencrbasic/lsp/features.py +602 -0
  93. opencrbasic/lsp/server.py +741 -0
  94. opencrbasic/lsp/session.py +1189 -0
  95. opencrbasic/lsp/worker.py +128 -0
  96. opencrbasic/mode.py +143 -0
  97. opencrbasic/navigation.py +401 -0
  98. opencrbasic/parser.py +3264 -0
  99. opencrbasic/positions.py +274 -0
  100. opencrbasic/preprocess.py +269 -0
  101. opencrbasic/profiles.py +266 -0
  102. opencrbasic/provenance.py +17 -0
  103. opencrbasic/quickfix.py +331 -0
  104. opencrbasic/rename.py +662 -0
  105. opencrbasic/signature.py +33 -0
  106. opencrbasic/source.py +600 -0
  107. opencrbasic/sources.py +125 -0
  108. opencrbasic/syntax.py +337 -0
  109. opencrbasic/tables.py +2173 -0
  110. opencrbasic/tables_hook.py +14 -0
  111. opencrbasic/tdf.py +142 -0
  112. opencrbasic/timing.py +565 -0
  113. opencrbasic-1.0.0.dist-info/METADATA +223 -0
  114. opencrbasic-1.0.0.dist-info/RECORD +117 -0
  115. opencrbasic-1.0.0.dist-info/WHEEL +4 -0
  116. opencrbasic-1.0.0.dist-info/entry_points.txt +3 -0
  117. opencrbasic-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,602 @@
1
+ """Language-feature answers as LSP types. A protocol adapter over the core modules.
2
+
3
+ Each function is synchronous and meant for a worker thread (`asyncio.to_thread`): it calls
4
+ `editing`, `hover` or `navigation` and converts what they return. The core modules speak physical
5
+ 0-based lines and decoded (Latin-1) columns; `Ctx` converts both ways using the negotiated position
6
+ encoding and the configured file encoding (through the editor text of an open document, else
7
+ through its bytes), and nothing else. There are no CRBasic rules here.
8
+ """
9
+
10
+ import contextlib
11
+ from collections.abc import Callable, Mapping
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from lsprotocol import types
16
+
17
+ from .. import editing, formatting, inlay, navigation, positions, quickfix, rename
18
+ from .. import hover as hovering
19
+
20
+ LEGEND = types.SemanticTokensLegend(
21
+ token_types=list(navigation.TOKEN_TYPES), token_modifiers=list(navigation.TOKEN_MODIFIERS)
22
+ )
23
+
24
+ _COMPLETION_KIND = {
25
+ "instruction": types.CompletionItemKind.Function,
26
+ "function": types.CompletionItemKind.Function,
27
+ "symbol": types.CompletionItemKind.Constant,
28
+ "keyword": types.CompletionItemKind.Keyword,
29
+ "variable": types.CompletionItemKind.Variable,
30
+ "constant": types.CompletionItemKind.Constant,
31
+ "alias": types.CompletionItemKind.Variable,
32
+ "sub": types.CompletionItemKind.Function,
33
+ "user_function": types.CompletionItemKind.Function,
34
+ "table": types.CompletionItemKind.Class,
35
+ "type": types.CompletionItemKind.Struct,
36
+ "parameter": types.CompletionItemKind.Variable,
37
+ "field": types.CompletionItemKind.Field,
38
+ }
39
+ _SYMBOL_KIND = {
40
+ "sub": types.SymbolKind.Function,
41
+ "function": types.SymbolKind.Function,
42
+ "parameter": types.SymbolKind.Variable,
43
+ "datatable": types.SymbolKind.Class,
44
+ "output": types.SymbolKind.Field,
45
+ "consttable": types.SymbolKind.Class,
46
+ "const": types.SymbolKind.Constant,
47
+ "structuretype": types.SymbolKind.Struct,
48
+ "member": types.SymbolKind.Field,
49
+ "beginprog": types.SymbolKind.Module,
50
+ "scan": types.SymbolKind.Event,
51
+ "subscan": types.SymbolKind.Event,
52
+ "slowsequence": types.SymbolKind.Event,
53
+ "public": types.SymbolKind.Variable,
54
+ "dim": types.SymbolKind.Variable,
55
+ "static": types.SymbolKind.Variable,
56
+ "alias": types.SymbolKind.Variable,
57
+ }
58
+ _HIGHLIGHT_KIND = {
59
+ "text": types.DocumentHighlightKind.Text,
60
+ "read": types.DocumentHighlightKind.Read,
61
+ "write": types.DocumentHighlightKind.Write,
62
+ }
63
+ _FOLD_KIND = {"region": types.FoldingRangeKind.Region, "comment": types.FoldingRangeKind.Comment}
64
+
65
+
66
+ def _key(path: Path) -> str:
67
+ return str(path).lower()
68
+
69
+
70
+ class Ctx:
71
+ """What one request needs to convert positions: the encodings, the documents' compiler bytes
72
+ (a snapshot taken on the event loop), their URIs, the profile and the help provider.
73
+
74
+ Every conversion goes through here. A document whose bytes carry its editor text
75
+ (`positions.EditorBytes`) is mapped through that text, so a character the file encoding
76
+ replaced keeps its editor width; any other file is mapped through its bytes."""
77
+
78
+ def __init__(
79
+ self,
80
+ profile: str,
81
+ position_encoding: str,
82
+ file_encoding: str,
83
+ blobs: Mapping[Path, bytes],
84
+ uris: Mapping[Path, str],
85
+ help_provider: Any = None,
86
+ ):
87
+ self.profile = profile
88
+ self.encoding = position_encoding
89
+ self.file_encoding = file_encoding
90
+ self.help = help_provider
91
+ self._blobs = {_key(p): d for p, d in blobs.items()}
92
+ self._uris = {_key(p): u for p, u in uris.items()}
93
+ self._lines: dict[str, list[bytes] | None] = {}
94
+ self._editor: dict[str, list[str] | None] = {}
95
+
96
+ def data(self, path: Path) -> bytes | None:
97
+ found = self._blobs.get(_key(path))
98
+ if found is not None:
99
+ return found
100
+ try:
101
+ return Path(path).read_bytes()
102
+ except OSError:
103
+ return None
104
+
105
+ def lines(self, path: Path) -> list[bytes] | None:
106
+ key = _key(path)
107
+ if key not in self._lines:
108
+ data = self.data(path)
109
+ self._lines[key] = None if data is None else positions.physical_lines(data)
110
+ return self._lines[key]
111
+
112
+ def editor_lines(self, path: Path) -> list[str] | None:
113
+ """The editor text lines of an open document, or None when the document has no editor
114
+ text (a file read from disk)."""
115
+ key = _key(path)
116
+ if key not in self._editor:
117
+ found = self._blobs.get(key)
118
+ text = getattr(found, "text", None)
119
+ self._editor[key] = None if text is None else positions.text_lines(text)
120
+ return self._editor[key]
121
+
122
+ def uri(self, path: Path) -> str:
123
+ return self._uris.get(_key(path)) or Path(path).absolute().as_uri()
124
+
125
+ def decoded(self, path: Path, position: types.Position) -> tuple[int, int] | None:
126
+ """LSP position -> (0-based line, decoded column); a line past the end clamps to the last."""
127
+ lines = self.lines(path)
128
+ if not lines:
129
+ return None
130
+ line = min(max(position.line, 0), len(lines) - 1)
131
+ editor = self.editor_lines(path)
132
+ if editor is not None:
133
+ return line, positions.compiler_column(
134
+ editor[line], position.character, encoding=self.encoding, file_encoding=self.file_encoding
135
+ )
136
+ _, col = positions.decoded_position(
137
+ lines, line, position.character, encoding=self.encoding, editor_encoding=self.file_encoding
138
+ )
139
+ return line, col
140
+
141
+ def position(self, path: Path, line: int, col: int | None) -> types.Position:
142
+ """(0-based line, decoded column or None for the line end) -> LSP position. A line past
143
+ the end of the file clamps to its last line."""
144
+ lines = self.lines(path) or [b""]
145
+ line = min(max(line, 0), len(lines) - 1)
146
+ col = len(lines[line]) if col is None else col
147
+ editor = self.editor_lines(path)
148
+ if editor is not None:
149
+ return types.Position(
150
+ line,
151
+ positions.editor_character(editor[line], col, encoding=self.encoding, file_encoding=self.file_encoding),
152
+ )
153
+ out_line, character = positions.lsp_position(
154
+ lines, line + 1, col, encoding=self.encoding, editor_encoding=self.file_encoding
155
+ )
156
+ return types.Position(out_line, character)
157
+
158
+ def range(self, path: Path, span: editing.Span) -> types.Range:
159
+ return types.Range(
160
+ self.position(path, span.start_line, span.start_col), self.position(path, span.end_line, span.end_col)
161
+ )
162
+
163
+ def columns(self, path: Path, line: int, start: int, end: int) -> types.Range:
164
+ return types.Range(self.position(path, line, start), self.position(path, line, end))
165
+
166
+
167
+ def _guard(ana: Any):
168
+ """Core calls on one program run one at a time (the modules cache on the program and the
169
+ feature calls are milliseconds)."""
170
+ return ana.guard if ana is not None else contextlib.nullcontext()
171
+
172
+
173
+ def _memo(ana: Any, key: str, make: Callable[[], Any]) -> Any:
174
+ return ana.cached(key, make) if ana is not None else make()
175
+
176
+
177
+ def _text(ctx: Ctx, path: Path) -> str | None:
178
+ data = ctx.data(path)
179
+ return None if data is None else data.decode("latin-1")
180
+
181
+
182
+ # ---- completion, hover, signature help (text based; the program supplies names) -------------
183
+
184
+
185
+ def completion(ctx: Ctx, ana: Any, result: Any, path: Path, position: types.Position) -> types.CompletionList:
186
+ text, at = _text(ctx, path), ctx.decoded(path, position)
187
+ if text is None or at is None:
188
+ return types.CompletionList(is_incomplete=False, items=[])
189
+ program = getattr(result, "program", None)
190
+ with _guard(ana):
191
+ declared = (
192
+ _memo(ana, "declared", lambda: editing.declared_names(program, ctx.profile))
193
+ if program is not None
194
+ else None
195
+ )
196
+ found = editing.completion_candidates(result, text, at[0], at[1], ctx.profile, declared=declared)
197
+ where = ctx.range(path, found.replace)
198
+ items = []
199
+ for n, item in enumerate(found.items):
200
+ detail = item.detail or item.kind
201
+ if item.param_count is not None:
202
+ detail += f" ({item.param_count} parameter{'' if item.param_count == 1 else 's'})"
203
+ items.append(
204
+ types.CompletionItem(
205
+ label=item.label,
206
+ kind=_COMPLETION_KIND.get(item.kind),
207
+ detail=detail,
208
+ sort_text=f"{n:06d}",
209
+ text_edit=types.TextEdit(where, item.label),
210
+ )
211
+ )
212
+ # no program yet: the names and members are missing, so the client must ask again
213
+ return types.CompletionList(
214
+ is_incomplete=bool(getattr(found, "is_incomplete", False)) or result is None, items=items
215
+ )
216
+
217
+
218
+ def hover(ctx: Ctx, ana: Any, result: Any, path: Path, position: types.Position) -> types.Hover | None:
219
+ text, at = _text(ctx, path), ctx.decoded(path, position)
220
+ if text is None or at is None:
221
+ return None
222
+ program = getattr(result, "program", None)
223
+ with _guard(ana):
224
+ declared = (
225
+ _memo(ana, "hover_declarations", lambda: hovering.declarations(program)) if program is not None else None
226
+ )
227
+ found = hovering.hover(result, text, at[0], at[1], ctx.profile, ctx.help, declared=declared)
228
+ if found is None:
229
+ return None
230
+ return types.Hover(
231
+ contents=types.MarkupContent(types.MarkupKind.Markdown, found.markdown),
232
+ range=ctx.columns(path, found.line, found.start_column, found.end_column),
233
+ )
234
+
235
+
236
+ def signature_help(ctx: Ctx, path: Path, position: types.Position) -> types.SignatureHelp | None:
237
+ text, at = _text(ctx, path), ctx.decoded(path, position)
238
+ if text is None or at is None:
239
+ return None
240
+ info = None
241
+ for call in hovering.call_stack(text, at[0], at[1]): # innermost first
242
+ info = hovering.signature_help(call.name, call.active, ctx.profile, ctx.help)
243
+ if info is not None:
244
+ break
245
+ if info is None:
246
+ return None
247
+
248
+ def offsets(span: tuple[int, int]) -> tuple[int, int]: # label offsets are UTF-16 units
249
+ return (positions.units(info.label[: span[0]], "utf-16"), positions.units(info.label[: span[1]], "utf-16"))
250
+
251
+ parameters = [
252
+ types.ParameterInformation(
253
+ label=offsets(p.label_span),
254
+ documentation=types.MarkupContent(types.MarkupKind.Markdown, p.documentation) if p.documentation else None,
255
+ )
256
+ for p in info.parameters
257
+ ]
258
+ signature = types.SignatureInformation(
259
+ label=info.label,
260
+ documentation=types.MarkupContent(types.MarkupKind.Markdown, info.documentation)
261
+ if info.documentation
262
+ else None,
263
+ parameters=parameters,
264
+ active_parameter=info.active_parameter,
265
+ )
266
+ return types.SignatureHelp(signatures=[signature], active_signature=0, active_parameter=info.active_parameter)
267
+
268
+
269
+ # ---- inlay hints (LSP-09c) ---------------------------------------------------------------------
270
+
271
+
272
+ def inlay_hints(ctx: Ctx, ana: Any, result: Any, path: Path, start_line: int, end_line: int) -> list[types.InlayHint]:
273
+ """Parameter-name hints on lines `start_line..end_line` (0-based, inclusive). The whole file's
274
+ hints are computed once per program and kept; a request only filters them."""
275
+ if result is None or ctx.help is None:
276
+ return []
277
+ with _guard(ana):
278
+ every = _memo(
279
+ ana,
280
+ f"inlay:{_key(path)}:{getattr(ctx.help, 'directory', '')}",
281
+ lambda: inlay.parameter_hints(result, path, 0, 1 << 30, ctx.profile, ctx.help),
282
+ )
283
+ return [
284
+ types.InlayHint(
285
+ position=ctx.position(path, h.line, h.column),
286
+ label=h.label,
287
+ kind=types.InlayHintKind.Parameter,
288
+ padding_right=h.padding_right,
289
+ )
290
+ for h in every
291
+ if start_line <= h.line <= end_line
292
+ ]
293
+
294
+
295
+ # ---- outline, folding, links -----------------------------------------------------------------
296
+
297
+
298
+ def _symbol(ctx: Ctx, path: Path, sym: editing.Symbol) -> types.DocumentSymbol:
299
+ full, header = ctx.range(path, sym.range), ctx.range(path, sym.selection)
300
+ inside = (full.start.line, full.start.character) <= (header.start.line, header.start.character) and (
301
+ header.end.line,
302
+ header.end.character,
303
+ ) <= (full.end.line, full.end.character)
304
+ return types.DocumentSymbol(
305
+ name=sym.name or sym.kind,
306
+ kind=_SYMBOL_KIND.get(sym.kind, types.SymbolKind.Variable),
307
+ range=full,
308
+ selection_range=header if inside else full,
309
+ detail=sym.detail or None,
310
+ children=[_symbol(ctx, path, c) for c in sym.children] or None,
311
+ )
312
+
313
+
314
+ def document_symbols(ctx: Ctx, ana: Any, result: Any, path: Path) -> list[types.DocumentSymbol]:
315
+ if result is None:
316
+ return []
317
+ with _guard(ana):
318
+ found = editing.document_symbols(result, path, ctx.profile)
319
+ return [_symbol(ctx, path, s) for s in found]
320
+
321
+
322
+ def folding_ranges(ctx: Ctx, path: Path) -> list[types.FoldingRange]:
323
+ text = _text(ctx, path)
324
+ if text is None:
325
+ return []
326
+ return [
327
+ types.FoldingRange(start_line=f.start_line, end_line=f.end_line, kind=_FOLD_KIND.get(f.kind))
328
+ for f in editing.folding_ranges(text, ctx.profile)
329
+ ]
330
+
331
+
332
+ def _same(a: Path, b: Path) -> bool:
333
+ return _key(Path(a).absolute()) == _key(Path(b).absolute())
334
+
335
+
336
+ def document_links(ctx: Ctx, ana: Any, result: Any, path: Path) -> list[types.DocumentLink]:
337
+ if result is None:
338
+ return []
339
+ with _guard(ana):
340
+ links = _memo(ana, "include_links", lambda: editing.include_links(result))
341
+ return [
342
+ types.DocumentLink(range=ctx.range(path, link.range), target=Path(link.target).absolute().as_uri())
343
+ for link in links
344
+ if link.target is not None and _same(link.file, path)
345
+ ]
346
+
347
+
348
+ # ---- navigation -------------------------------------------------------------------------------
349
+
350
+
351
+ def _navigation(ana: Any, result: Any) -> Any:
352
+ if result is None:
353
+ return None
354
+ with _guard(ana):
355
+ return _memo(ana, "navigation", lambda: navigation.Navigation(result))
356
+
357
+
358
+ def _locations(ctx: Ctx, found: list[navigation.Location]) -> list[types.Location]:
359
+ return [
360
+ types.Location(ctx.uri(loc.path), ctx.columns(loc.path, loc.line, loc.start, loc.end))
361
+ for loc in found
362
+ if loc.path is not None
363
+ ]
364
+
365
+
366
+ def definition(ctx: Ctx, ana: Any, result: Any, path: Path, position: types.Position) -> list[types.Location]:
367
+ nav, at = _navigation(ana, result), ctx.decoded(path, position)
368
+ if nav is None or at is None:
369
+ return []
370
+ with _guard(ana):
371
+ found = nav.definition(path, *at)
372
+ return _locations(ctx, found)
373
+
374
+
375
+ def references(
376
+ ctx: Ctx, ana: Any, result: Any, path: Path, position: types.Position, include_declaration: bool
377
+ ) -> list[types.Location]:
378
+ nav, at = _navigation(ana, result), ctx.decoded(path, position)
379
+ if nav is None or at is None:
380
+ return []
381
+ with _guard(ana):
382
+ found = nav.references(path, at[0], at[1], include_declaration)
383
+ return _locations(ctx, found)
384
+
385
+
386
+ def _navigations(analyses: list) -> tuple[list, contextlib.ExitStack]:
387
+ """The `Navigation` of each (Analysed, result) pair, and a stack holding every program's
388
+ core-call guard (taken in the given order; the caller passes them sorted by entry path)."""
389
+ stack = contextlib.ExitStack()
390
+ navs = []
391
+ try:
392
+ for ana, result in analyses:
393
+ nav = _navigation(ana, result)
394
+ if nav is not None:
395
+ stack.enter_context(_guard(ana))
396
+ navs.append(nav)
397
+ except BaseException:
398
+ stack.close()
399
+ raise
400
+ return navs, stack
401
+
402
+
403
+ def declaration_files(ctx: Ctx, analyses: list, path: Path, position: types.Position) -> set[Path]:
404
+ """The files of the declarations the name at the position resolves to in any of `analyses`
405
+ ((Analysed, result) pairs): the files whose readers `references_across` must also see."""
406
+ at = ctx.decoded(path, position)
407
+ if at is None:
408
+ return set()
409
+ navs, stack = _navigations(analyses)
410
+ with stack:
411
+ found = navigation.declaration_sites(navs, path, *at)
412
+ return {Path(s.path) for s in found if s.path is not None}
413
+
414
+
415
+ def references_across(
416
+ ctx: Ctx, analyses: list, path: Path, position: types.Position, include_declaration: bool
417
+ ) -> list[types.Location]:
418
+ """References over several analyses ((Analysed, result) pairs; LSP-R4a)."""
419
+ at = ctx.decoded(path, position)
420
+ if at is None:
421
+ return []
422
+ navs, stack = _navigations(analyses)
423
+ with stack:
424
+ found = navigation.references_across(navs, path, at[0], at[1], include_declaration)
425
+ return _locations(ctx, found)
426
+
427
+
428
+ def document_highlights(
429
+ ctx: Ctx, ana: Any, result: Any, path: Path, position: types.Position
430
+ ) -> list[types.DocumentHighlight]:
431
+ nav, at = _navigation(ana, result), ctx.decoded(path, position)
432
+ if nav is None or at is None:
433
+ return []
434
+ with _guard(ana):
435
+ found = nav.document_highlights(path, *at)
436
+ return [
437
+ types.DocumentHighlight(ctx.columns(path, h.line, h.start, h.end), _HIGHLIGHT_KIND.get(h.kind)) for h in found
438
+ ]
439
+
440
+
441
+ def semantic_tokens(ctx: Ctx, ana: Any, result: Any, path: Path) -> types.SemanticTokens:
442
+ nav, lines = _navigation(ana, result), ctx.lines(path)
443
+ data: list[int] = []
444
+ if nav is not None and lines:
445
+ kinds = {name: i for i, name in enumerate(navigation.TOKEN_TYPES)}
446
+ mods = {name: 1 << i for i, name in enumerate(navigation.TOKEN_MODIFIERS)}
447
+ last_line = last_start = 0
448
+ with _guard(ana):
449
+ found = nav.semantic_tokens(path)
450
+ for tok in found:
451
+ if tok.line >= len(lines):
452
+ continue # a stale program: the line is gone
453
+ begin = ctx.position(path, tok.line, tok.start)
454
+ end = ctx.position(path, tok.line, tok.start + tok.length)
455
+ length = end.character - begin.character
456
+ delta = begin.line - last_line
457
+ if length <= 0 or delta < 0 or (not delta and begin.character < last_start):
458
+ continue
459
+ data += [
460
+ delta,
461
+ begin.character if delta else begin.character - last_start,
462
+ length,
463
+ kinds[tok.type],
464
+ sum(mods[m] for m in tok.modifiers),
465
+ ]
466
+ last_line, last_start = begin.line, begin.character
467
+ return types.SemanticTokens(data=data)
468
+
469
+
470
+ # ---- formatting (LSP-07-wire) -----------------------------------------------------------------
471
+
472
+
473
+ def _indent(options: types.FormattingOptions) -> str:
474
+ width = max(1, min(int(options.tab_size or 1), 16))
475
+ return " " * width if options.insert_spaces else "\t"
476
+
477
+
478
+ def formatting_edits(
479
+ ctx: Ctx, path: Path, options: types.FormattingOptions, where: types.Range | None = None
480
+ ) -> list[types.TextEdit]:
481
+ """Indentation edits for the document, or for the lines `where` touches (a range that ends at
482
+ character 0 of a later line does not include that line). Text-only: the refusal policy
483
+ (`formatting.format_refusal`) is the caller's."""
484
+ text = _text(ctx, path)
485
+ if text is None:
486
+ return []
487
+ indent = _indent(options)
488
+ if where is None:
489
+ found = formatting.format_indentation(text, ctx.profile, indent)
490
+ else:
491
+ end = where.end
492
+ if end.character == 0 and end.line > where.start.line:
493
+ end = types.Position(end.line - 1, 0)
494
+ first, last = ctx.decoded(path, where.start), ctx.decoded(path, end)
495
+ if first is None or last is None:
496
+ return []
497
+ found = formatting.format_range(text, first[0], last[0], ctx.profile, indent)
498
+ return [types.TextEdit(ctx.columns(path, e.line, e.start, e.end), e.new_text) for e in found]
499
+
500
+
501
+ # ---- rename (LSP-07-wire) ---------------------------------------------------------------------
502
+
503
+
504
+ def prepare_rename(
505
+ ctx: Ctx, results: list, path: Path, position: types.Position, source: Any = None
506
+ ) -> tuple[types.PrepareRenamePlaceholder | None, rename.Refusal | None, frozenset]:
507
+ """(placeholder, refusal, files): `files` are the files of the declaration and its references
508
+ (`PrepareResult.files`), empty on a refusal."""
509
+ at = ctx.decoded(path, position)
510
+ if at is None:
511
+ return None, rename.Refusal("no_range", "the document cannot be read"), frozenset()
512
+ found = rename.prepare_rename(results, path, *at, source=source)
513
+ if not found.ok:
514
+ return None, found.refusal, frozenset()
515
+ assert found.path is not None # an ok PrepareResult carries its range
516
+ return (
517
+ types.PrepareRenamePlaceholder(
518
+ range=ctx.columns(found.path, found.line, found.start, found.end), placeholder=found.placeholder
519
+ ),
520
+ None,
521
+ found.files,
522
+ )
523
+
524
+
525
+ def rename_edit(
526
+ ctx: Ctx,
527
+ results: list,
528
+ path: Path,
529
+ position: types.Position,
530
+ new_name: str,
531
+ versions: Mapping[Path, int],
532
+ source: Any = None,
533
+ ) -> tuple[types.WorkspaceEdit | None, rename.Refusal | None, dict]:
534
+ """(edit, refusal, file hashes). The edit is `documentChanges` with the document version for
535
+ every open document and `None` for a closed one. The hashes are `rename`'s content hashes.
536
+ `source`: the `SourceProvider` `rename` checks freshness against (default: the analysed
537
+ overlays on the disk)."""
538
+ at = ctx.decoded(path, position)
539
+ if at is None:
540
+ return None, rename.Refusal("no_range", "the document cannot be read"), {}
541
+ found = rename.rename(results, path, at[0], at[1], new_name, source=source)
542
+ if not found.ok:
543
+ return None, found.refusal, {}
544
+ changes = []
545
+ for target, edits in sorted(found.edits.items(), key=lambda kv: str(kv[0])):
546
+ changes.append(
547
+ types.TextDocumentEdit(
548
+ types.OptionalVersionedTextDocumentIdentifier(ctx.uri(target), versions.get(target)),
549
+ [types.TextEdit(ctx.columns(target, e.line, e.start, e.end), e.new_text) for e in edits],
550
+ )
551
+ )
552
+ return types.WorkspaceEdit(document_changes=changes), None, dict(found.versions)
553
+
554
+
555
+ # ---- quick fixes (LSP-07-wire) ----------------------------------------------------------------
556
+
557
+
558
+ def code_actions(
559
+ ctx: Ctx, sources: list[tuple[Path, int, Any]], path: Path, diagnostics: list[types.Diagnostic], version: int | None
560
+ ) -> list[types.CodeAction]:
561
+ """Quick fixes for the diagnostics of the request that this server published. `sources`:
562
+ (entry path, result token, `AnalysisResult`) of the entries that contributed diagnostics to
563
+ `path`. A request diagnostic names its analysis diagnostic in `data` (`{"entry", "result",
564
+ "index"}`, M6); one from another result, or without `data`, gets no fix. The same fix from
565
+ several entries is offered once."""
566
+ actions: list[types.CodeAction] = []
567
+ seen: set = set()
568
+ for lsp_diag in diagnostics:
569
+ data = lsp_diag.data if isinstance(lsp_diag.data, dict) else {}
570
+ if lsp_diag.source != "opencrbasic" or not data:
571
+ continue
572
+ for entry, token, result in sources:
573
+ if data.get("entry") != str(entry) or data.get("result") != token:
574
+ continue
575
+ index = data.get("index")
576
+ if not isinstance(index, int) or not 0 <= index < len(result.diagnostics):
577
+ continue
578
+ diag = result.diagnostics[index]
579
+ if diag.path is not None and _same(diag.path, path):
580
+ for act in quickfix.code_actions(result, path, diag, ctx.profile):
581
+ if any(not _same(e.path, path) for e in act.edits):
582
+ continue
583
+ key = (lsp_diag.range.start.line, act.title, act.edits)
584
+ if key in seen:
585
+ continue
586
+ seen.add(key)
587
+ edits = [types.TextEdit(ctx.columns(path, e.line, e.start, e.end), e.new_text) for e in act.edits]
588
+ actions.append(
589
+ types.CodeAction(
590
+ title=act.title,
591
+ kind=types.CodeActionKind.QuickFix,
592
+ diagnostics=[lsp_diag],
593
+ edit=types.WorkspaceEdit(
594
+ document_changes=[
595
+ types.TextDocumentEdit(
596
+ types.OptionalVersionedTextDocumentIdentifier(ctx.uri(path), version), edits
597
+ )
598
+ ]
599
+ ),
600
+ )
601
+ )
602
+ return actions