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.
- opencrbasic/__init__.py +3 -0
- opencrbasic/__main__.py +5 -0
- opencrbasic/analysis.py +184 -0
- opencrbasic/bindings.py +1034 -0
- opencrbasic/catalog.py +173 -0
- opencrbasic/checkconfig.py +144 -0
- opencrbasic/checker/__init__.py +977 -0
- opencrbasic/checker/cr3xx.py +3118 -0
- opencrbasic/checker/ess.py +81 -0
- opencrbasic/checker/expressions.py +3581 -0
- opencrbasic/checker/instr_data.py +1802 -0
- opencrbasic/checker/instr_measure.py +1547 -0
- opencrbasic/checker/instr_rules.py +3540 -0
- opencrbasic/checker/instructions.py +17 -0
- opencrbasic/checker/intervals.py +678 -0
- opencrbasic/checker/longconv.py +133 -0
- opencrbasic/checker/scan_time.py +580 -0
- opencrbasic/checker/storage.py +456 -0
- opencrbasic/checker/structs.py +240 -0
- opencrbasic/checker/structure.py +6213 -0
- opencrbasic/checker/symbols.py +609 -0
- opencrbasic/checker/terminals.py +249 -0
- opencrbasic/checker/unused.py +186 -0
- opencrbasic/checker/var_types.py +564 -0
- opencrbasic/checker/warnings.py +661 -0
- opencrbasic/checks.py +491 -0
- opencrbasic/cli.py +391 -0
- opencrbasic/commands.py +128 -0
- opencrbasic/compiler.py +320 -0
- opencrbasic/consteval.py +327 -0
- opencrbasic/data/background_calibration.json +4 -0
- opencrbasic/data/before_beginprog.json +99 -0
- opencrbasic/data/cr1000x/background_calibration.json +4 -0
- opencrbasic/data/cr1000x/before_beginprog.json +138 -0
- opencrbasic/data/cr1000x/ess_variables.json +1087 -0
- opencrbasic/data/cr1000x/fnotch.json +29 -0
- opencrbasic/data/cr1000x/functions.json +1337 -0
- opencrbasic/data/cr1000x/instructions.json +4836 -0
- opencrbasic/data/cr1000x/measure.json +121 -0
- opencrbasic/data/cr1000x/mode.json +111 -0
- opencrbasic/data/cr1000x/pipeline_tasks.json +88 -0
- opencrbasic/data/cr1000x/settings_fields.json +321 -0
- opencrbasic/data/cr1000x/status_members.json +318 -0
- opencrbasic/data/cr1000x/symbols.json +872 -0
- opencrbasic/data/cr1000x/terminals.json +225 -0
- opencrbasic/data/cr300/before_beginprog.json +42 -0
- opencrbasic/data/cr300/ess_variables.json +1087 -0
- opencrbasic/data/cr300/fnotch.json +22 -0
- opencrbasic/data/cr300/functions.json +1261 -0
- opencrbasic/data/cr300/instructions.json +2837 -0
- opencrbasic/data/cr300/measure.json +33 -0
- opencrbasic/data/cr300/mode.json +121 -0
- opencrbasic/data/cr300/settings_fields.json +259 -0
- opencrbasic/data/cr300/status_members.json +303 -0
- opencrbasic/data/cr300/symbols.json +584 -0
- opencrbasic/data/cr300/terminals.json +193 -0
- opencrbasic/data/cr350/before_beginprog.json +44 -0
- opencrbasic/data/cr350/ess_variables.json +1087 -0
- opencrbasic/data/cr350/fnotch.json +22 -0
- opencrbasic/data/cr350/functions.json +1262 -0
- opencrbasic/data/cr350/instructions.json +2848 -0
- opencrbasic/data/cr350/measure.json +35 -0
- opencrbasic/data/cr350/mode.json +121 -0
- opencrbasic/data/cr350/settings_fields.json +292 -0
- opencrbasic/data/cr350/status_members.json +303 -0
- opencrbasic/data/cr350/symbols.json +572 -0
- opencrbasic/data/cr350/terminals.json +190 -0
- opencrbasic/data/ess_variables.json +1087 -0
- opencrbasic/data/fnotch.json +34 -0
- opencrbasic/data/functions.json +1337 -0
- opencrbasic/data/instructions.json +4965 -0
- opencrbasic/data/measure.json +123 -0
- opencrbasic/data/mode.json +121 -0
- opencrbasic/data/notices_cr6.json +890 -0
- opencrbasic/data/pipeline_tasks.json +84 -0
- opencrbasic/data/settings_fields.json +322 -0
- opencrbasic/data/status_members.json +319 -0
- opencrbasic/data/status_table.json +684 -0
- opencrbasic/data/symbols.json +902 -0
- opencrbasic/data/terminals.json +195 -0
- opencrbasic/data/type_codes.json +249 -0
- opencrbasic/diagnostics.py +196 -0
- opencrbasic/editing.py +827 -0
- opencrbasic/formatting.py +310 -0
- opencrbasic/helpdocs.py +677 -0
- opencrbasic/hover.py +667 -0
- opencrbasic/inlay.py +169 -0
- opencrbasic/lexer.py +183 -0
- opencrbasic/lsp/__init__.py +5 -0
- opencrbasic/lsp/__main__.py +7 -0
- opencrbasic/lsp/config.py +202 -0
- opencrbasic/lsp/features.py +602 -0
- opencrbasic/lsp/server.py +741 -0
- opencrbasic/lsp/session.py +1189 -0
- opencrbasic/lsp/worker.py +128 -0
- opencrbasic/mode.py +143 -0
- opencrbasic/navigation.py +401 -0
- opencrbasic/parser.py +3264 -0
- opencrbasic/positions.py +274 -0
- opencrbasic/preprocess.py +269 -0
- opencrbasic/profiles.py +266 -0
- opencrbasic/provenance.py +17 -0
- opencrbasic/quickfix.py +331 -0
- opencrbasic/rename.py +662 -0
- opencrbasic/signature.py +33 -0
- opencrbasic/source.py +600 -0
- opencrbasic/sources.py +125 -0
- opencrbasic/syntax.py +337 -0
- opencrbasic/tables.py +2173 -0
- opencrbasic/tables_hook.py +14 -0
- opencrbasic/tdf.py +142 -0
- opencrbasic/timing.py +565 -0
- opencrbasic-1.0.0.dist-info/METADATA +223 -0
- opencrbasic-1.0.0.dist-info/RECORD +117 -0
- opencrbasic-1.0.0.dist-info/WHEEL +4 -0
- opencrbasic-1.0.0.dist-info/entry_points.txt +3 -0
- 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
|