robotcode-language-server 2.6.2__py3-none-any.whl → 2.8.0.dev548__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.
- robotcode/language_server/__version__.py +1 -1
- robotcode/language_server/common/parts/diagnostics.py +50 -17
- robotcode/language_server/common/parts/documents.py +34 -31
- robotcode/language_server/common/parts/workspace.py +57 -55
- robotcode/language_server/common/protocol.py +3 -1
- robotcode/language_server/robotframework/configuration.py +3 -0
- robotcode/language_server/robotframework/parts/code_action_documentation.py +440 -129
- robotcode/language_server/robotframework/parts/code_action_helper_mixin.py +17 -2
- robotcode/language_server/robotframework/parts/code_action_quick_fixes.py +6 -25
- robotcode/language_server/robotframework/parts/code_action_refactor.py +8 -6
- robotcode/language_server/robotframework/parts/code_lens.py +3 -4
- robotcode/language_server/robotframework/parts/completion.py +193 -21
- robotcode/language_server/robotframework/parts/debugging_utils.py +75 -6
- robotcode/language_server/robotframework/parts/diagnostics.py +15 -3
- robotcode/language_server/robotframework/parts/documents_cache.py +5 -1
- robotcode/language_server/robotframework/parts/hover.py +103 -21
- robotcode/language_server/robotframework/parts/http_server.py +17 -7
- robotcode/language_server/robotframework/parts/inline_value.py +69 -7
- robotcode/language_server/robotframework/parts/keywords_treeview.py +151 -53
- robotcode/language_server/robotframework/parts/model_variables.py +331 -0
- robotcode/language_server/robotframework/parts/references.py +47 -31
- robotcode/language_server/robotframework/parts/rename.py +1 -2
- robotcode/language_server/robotframework/parts/robot_workspace.py +229 -25
- robotcode/language_server/robotframework/parts/selection_range.py +26 -13
- robotcode/language_server/robotframework/parts/semantic_tokens.py +414 -113
- robotcode/language_server/robotframework/parts/signature_help.py +162 -70
- robotcode/language_server/robotframework/protocol.py +1 -0
- {robotcode_language_server-2.6.2.dist-info → robotcode_language_server-2.8.0.dev548.dist-info}/METADATA +7 -6
- {robotcode_language_server-2.6.2.dist-info → robotcode_language_server-2.8.0.dev548.dist-info}/RECORD +31 -30
- {robotcode_language_server-2.6.2.dist-info → robotcode_language_server-2.8.0.dev548.dist-info}/WHEEL +1 -1
- {robotcode_language_server-2.6.2.dist-info → robotcode_language_server-2.8.0.dev548.dist-info}/entry_points.txt +0 -0
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
|
-
from typing import TYPE_CHECKING, Any, List, Optional
|
|
2
|
+
from typing import TYPE_CHECKING, Any, Callable, List, Mapping, Optional, Tuple
|
|
3
3
|
|
|
4
4
|
from robotcode.core.lsp.types import TextDocumentIdentifier
|
|
5
|
+
from robotcode.core.text_document import TextDocument
|
|
5
6
|
from robotcode.core.uri import Uri
|
|
6
7
|
from robotcode.core.utils.dataclasses import CamelSnakeMixin
|
|
7
8
|
from robotcode.core.utils.logging import LoggingDescriptor
|
|
8
9
|
from robotcode.jsonrpc2.protocol import rpc_method
|
|
9
|
-
from robotcode.robot.diagnostics.
|
|
10
|
+
from robotcode.robot.diagnostics.entities import LibraryEntry
|
|
11
|
+
from robotcode.robot.diagnostics.library_doc import KeywordDoc, LibraryDoc
|
|
12
|
+
from robotcode.robot.diagnostics.namespace import Namespace
|
|
13
|
+
from robotcode.robot.utils.markdown_docs import LinkResolver, ReferenceTarget
|
|
10
14
|
|
|
15
|
+
from .code_action_documentation import DocumentationTarget, keyword_reference_targets, link_documentation
|
|
11
16
|
from .protocol_part import RobotLanguageServerProtocolPart
|
|
12
17
|
|
|
13
18
|
if TYPE_CHECKING:
|
|
@@ -72,7 +77,7 @@ class GetDocumentationUrl(CamelSnakeMixin):
|
|
|
72
77
|
keyword_id: Optional[str] = None
|
|
73
78
|
|
|
74
79
|
|
|
75
|
-
class RobotKeywordsTreeViewPart(RobotLanguageServerProtocolPart
|
|
80
|
+
class RobotKeywordsTreeViewPart(RobotLanguageServerProtocolPart):
|
|
76
81
|
_logger = LoggingDescriptor()
|
|
77
82
|
|
|
78
83
|
def __init__(self, parent: "RobotLanguageServerProtocol") -> None:
|
|
@@ -96,50 +101,117 @@ class RobotKeywordsTreeViewPart(RobotLanguageServerProtocolPart, ModelHelper):
|
|
|
96
101
|
result = []
|
|
97
102
|
|
|
98
103
|
for _k, v in namespace.libraries.items():
|
|
104
|
+
documentation, keywords = (
|
|
105
|
+
self._import_documentation(v, True, document, namespace) if not no_documentation else (None, None)
|
|
106
|
+
)
|
|
99
107
|
result.append(
|
|
100
108
|
DocumentImport(
|
|
101
109
|
name=v.name,
|
|
102
110
|
alias=v.alias,
|
|
103
111
|
id=str(hash(v)),
|
|
104
112
|
type="library",
|
|
105
|
-
documentation=
|
|
106
|
-
keywords=
|
|
107
|
-
Keyword(
|
|
108
|
-
l.name,
|
|
109
|
-
str(hash(l)),
|
|
110
|
-
l.parameter_signature(),
|
|
111
|
-
l.to_markdown(add_signature=False) if not no_documentation else None,
|
|
112
|
-
)
|
|
113
|
-
for l in v.library_doc.keywords.values()
|
|
114
|
-
]
|
|
115
|
-
if not no_documentation
|
|
116
|
-
else None,
|
|
113
|
+
documentation=documentation,
|
|
114
|
+
keywords=keywords,
|
|
117
115
|
)
|
|
118
116
|
)
|
|
119
117
|
for _k, v in namespace.resources.items():
|
|
118
|
+
documentation, keywords = (
|
|
119
|
+
self._import_documentation(v, False, document, namespace) if not no_documentation else (None, None)
|
|
120
|
+
)
|
|
120
121
|
result.append(
|
|
121
122
|
DocumentImport(
|
|
122
123
|
name=v.name,
|
|
123
124
|
alias=None,
|
|
124
125
|
id=str(hash(v)),
|
|
125
126
|
type="resource",
|
|
126
|
-
documentation=
|
|
127
|
-
keywords=
|
|
128
|
-
Keyword(
|
|
129
|
-
l.name,
|
|
130
|
-
str(hash(l)),
|
|
131
|
-
l.parameter_signature(),
|
|
132
|
-
l.to_markdown(add_signature=False) if not no_documentation else None,
|
|
133
|
-
)
|
|
134
|
-
for l in v.library_doc.keywords.values()
|
|
135
|
-
]
|
|
136
|
-
if not no_documentation
|
|
137
|
-
else None,
|
|
127
|
+
documentation=documentation,
|
|
128
|
+
keywords=keywords,
|
|
138
129
|
)
|
|
139
130
|
)
|
|
140
131
|
|
|
141
132
|
return result
|
|
142
133
|
|
|
134
|
+
@property
|
|
135
|
+
def _links(self) -> bool:
|
|
136
|
+
return self.parent.robot_initialization_options.documentation_viewer_links
|
|
137
|
+
|
|
138
|
+
def _import_documentation(
|
|
139
|
+
self, entry: LibraryEntry, is_library: bool, document: TextDocument, namespace: Namespace
|
|
140
|
+
) -> Tuple[str, List[Keyword]]:
|
|
141
|
+
"""The tooltips of an import and of its keywords."""
|
|
142
|
+
library_doc = entry.library_doc
|
|
143
|
+
target = (
|
|
144
|
+
self.parent.robot_code_action_documentation.entry_target(entry, is_library, document, namespace)
|
|
145
|
+
if self._links
|
|
146
|
+
else None
|
|
147
|
+
)
|
|
148
|
+
link_resolver = self._link_resolver(document, namespace, target, library_doc)
|
|
149
|
+
return (
|
|
150
|
+
self._documentation(
|
|
151
|
+
lambda link_resolver: library_doc.to_markdown(add_signature=False, link_resolver=link_resolver),
|
|
152
|
+
target,
|
|
153
|
+
link_resolver,
|
|
154
|
+
),
|
|
155
|
+
self._keywords(list(library_doc.keywords.values()), target, link_resolver),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def _link_resolver(
|
|
159
|
+
self,
|
|
160
|
+
document: TextDocument,
|
|
161
|
+
namespace: Namespace,
|
|
162
|
+
target: Optional[DocumentationTarget],
|
|
163
|
+
library_doc: LibraryDoc,
|
|
164
|
+
) -> Optional[LinkResolver]:
|
|
165
|
+
# one for the tooltips of an import and all of its keywords
|
|
166
|
+
if target is None:
|
|
167
|
+
return None
|
|
168
|
+
return self.parent.robot_code_action_documentation.link_resolver(document, namespace, target, library_doc)
|
|
169
|
+
|
|
170
|
+
def _keywords(
|
|
171
|
+
self,
|
|
172
|
+
keywords: List[KeywordDoc],
|
|
173
|
+
target: Optional[DocumentationTarget],
|
|
174
|
+
link_resolver: Optional[LinkResolver],
|
|
175
|
+
) -> List[Keyword]:
|
|
176
|
+
"""The keywords of a library, a resource file or the document with their tooltips."""
|
|
177
|
+
# once for all keywords, they share their library and its documentation format
|
|
178
|
+
targets = keyword_reference_targets(keywords[0], self._links) if keywords else None
|
|
179
|
+
return [self._keyword(kw, target, link_resolver, targets) for kw in keywords]
|
|
180
|
+
|
|
181
|
+
def _keyword(
|
|
182
|
+
self,
|
|
183
|
+
kw: KeywordDoc,
|
|
184
|
+
target: Optional[DocumentationTarget],
|
|
185
|
+
link_resolver: Optional[LinkResolver],
|
|
186
|
+
targets: Optional[Mapping[str, ReferenceTarget]],
|
|
187
|
+
) -> Keyword:
|
|
188
|
+
return Keyword(
|
|
189
|
+
kw.name,
|
|
190
|
+
str(hash(kw)),
|
|
191
|
+
kw.parameter_signature(),
|
|
192
|
+
self._documentation(
|
|
193
|
+
lambda link_resolver: kw.to_markdown(
|
|
194
|
+
add_signature=False, link_resolver=link_resolver, reference_targets=targets
|
|
195
|
+
),
|
|
196
|
+
target,
|
|
197
|
+
link_resolver,
|
|
198
|
+
heading=False,
|
|
199
|
+
),
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
def _documentation(
|
|
203
|
+
self,
|
|
204
|
+
render: Callable[[Optional[LinkResolver]], str],
|
|
205
|
+
target: Optional[DocumentationTarget],
|
|
206
|
+
link_resolver: Optional[LinkResolver],
|
|
207
|
+
*,
|
|
208
|
+
heading: bool = True,
|
|
209
|
+
) -> str:
|
|
210
|
+
"""A tooltip; with links, linked into the Documentation Viewer at the page of `target`."""
|
|
211
|
+
if not self._links:
|
|
212
|
+
return render(None)
|
|
213
|
+
return link_documentation(render, target, None, heading=heading, link_resolver=link_resolver)
|
|
214
|
+
|
|
143
215
|
@rpc_method(name="robot/keywordsview/getDocumentKeywords", param_type=GetDocumentKeywordsParams, threaded=True)
|
|
144
216
|
@_logger.call
|
|
145
217
|
def _get_document_keywords(
|
|
@@ -155,31 +227,27 @@ class RobotKeywordsTreeViewPart(RobotLanguageServerProtocolPart, ModelHelper):
|
|
|
155
227
|
|
|
156
228
|
namespace = self.parent.documents_cache.get_namespace(document)
|
|
157
229
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
str(hash(l)),
|
|
162
|
-
l.parameter_signature(),
|
|
163
|
-
l.to_markdown(add_signature=False) if not no_documentation else None,
|
|
164
|
-
)
|
|
165
|
-
for l in namespace.library_doc.keywords.values()
|
|
166
|
-
]
|
|
230
|
+
keywords = list(namespace.library_doc.keywords.values())
|
|
231
|
+
if no_documentation:
|
|
232
|
+
return [Keyword(l.name, str(hash(l)), l.parameter_signature(), None) for l in keywords]
|
|
167
233
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
234
|
+
target = (
|
|
235
|
+
self.parent.robot_code_action_documentation.document_target(document, namespace) if self._links else None
|
|
236
|
+
)
|
|
237
|
+
return self._keywords(keywords, target, self._link_resolver(document, namespace, target, namespace.library_doc))
|
|
238
|
+
|
|
239
|
+
def _documentation_target(
|
|
171
240
|
self,
|
|
172
241
|
text_document: TextDocumentIdentifier,
|
|
173
|
-
import_id: Optional[str]
|
|
174
|
-
keyword_id: Optional[str]
|
|
175
|
-
|
|
176
|
-
**kwargs: Any,
|
|
177
|
-
) -> Optional[str]:
|
|
242
|
+
import_id: Optional[str],
|
|
243
|
+
keyword_id: Optional[str],
|
|
244
|
+
) -> Optional[DocumentationTarget]:
|
|
178
245
|
document = self.parent.documents.get(text_document.uri)
|
|
179
246
|
if document is None:
|
|
180
247
|
return None
|
|
181
248
|
|
|
182
249
|
namespace = self.parent.documents_cache.get_namespace(document)
|
|
250
|
+
documentation = self.parent.robot_code_action_documentation
|
|
183
251
|
|
|
184
252
|
keyword_name = None
|
|
185
253
|
|
|
@@ -189,25 +257,55 @@ class RobotKeywordsTreeViewPart(RobotLanguageServerProtocolPart, ModelHelper):
|
|
|
189
257
|
if keyword is not None:
|
|
190
258
|
keyword_name = keyword.name
|
|
191
259
|
|
|
192
|
-
return
|
|
193
|
-
str(document.uri.to_path().name), (), document, namespace, keyword_name
|
|
194
|
-
)
|
|
260
|
+
return documentation.document_target(document, namespace, keyword_name)
|
|
195
261
|
|
|
262
|
+
is_library = True
|
|
196
263
|
entry = next((l for l in namespace.libraries.values() if str(hash(l)) == import_id), None)
|
|
197
264
|
if entry is None:
|
|
265
|
+
is_library = False
|
|
198
266
|
entry = next((l for l in namespace.resources.values() if str(hash(l)) == import_id), None)
|
|
199
267
|
|
|
200
|
-
if
|
|
268
|
+
if entry is None:
|
|
269
|
+
return None
|
|
270
|
+
|
|
271
|
+
if keyword_id:
|
|
201
272
|
keyword = next((l for l in entry.library_doc.keywords.values() if str(hash(l)) == keyword_id), None)
|
|
202
273
|
if keyword is not None:
|
|
203
274
|
keyword_name = keyword.name
|
|
204
275
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
276
|
+
return documentation.entry_target(entry, is_library, document, namespace, keyword_name)
|
|
277
|
+
|
|
278
|
+
@rpc_method(name="robot/keywordsview/getDocumentationTarget", param_type=GetDocumentationUrl, threaded=True)
|
|
279
|
+
@_logger.call
|
|
280
|
+
def _get_documentation_target(
|
|
281
|
+
self,
|
|
282
|
+
text_document: TextDocumentIdentifier,
|
|
283
|
+
import_id: Optional[str] = None,
|
|
284
|
+
keyword_id: Optional[str] = None,
|
|
285
|
+
*args: Any,
|
|
286
|
+
**kwargs: Any,
|
|
287
|
+
) -> Optional[DocumentationTarget]:
|
|
288
|
+
return self._documentation_target(text_document, import_id, keyword_id)
|
|
289
|
+
|
|
290
|
+
@rpc_method(name="robot/keywordsview/getDocumentationUrl", param_type=GetDocumentationUrl, threaded=True)
|
|
291
|
+
@_logger.call
|
|
292
|
+
def _get_documentation_url(
|
|
293
|
+
self,
|
|
294
|
+
text_document: TextDocumentIdentifier,
|
|
295
|
+
import_id: Optional[str] = None,
|
|
296
|
+
keyword_id: Optional[str] = None,
|
|
297
|
+
*args: Any,
|
|
298
|
+
**kwargs: Any,
|
|
299
|
+
) -> Optional[str]:
|
|
300
|
+
target = self._documentation_target(text_document, import_id, keyword_id)
|
|
301
|
+
if target is None:
|
|
302
|
+
return None
|
|
303
|
+
|
|
304
|
+
document = self.parent.documents.get(text_document.uri)
|
|
305
|
+
if document is None:
|
|
306
|
+
return None
|
|
209
307
|
|
|
210
|
-
return
|
|
308
|
+
return self.parent.robot_code_action_documentation.build_url(target, document)
|
|
211
309
|
|
|
212
310
|
@rpc_method(
|
|
213
311
|
name="robot/keywordsview/getLibraryDocumentation", param_type=GetLibraryDocumentationParams, threaded=True
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"""Model-path variable extraction shared by selection range, inline values,
|
|
2
|
+
and debug evaluatable expressions.
|
|
3
|
+
|
|
4
|
+
A faithful, self-contained port of the legacy
|
|
5
|
+
`ModelHelper.iter_variables_from_token` / `iter_expression_variables_from_token`
|
|
6
|
+
candidate semantics, operating on the `SemanticToken`s of a
|
|
7
|
+
`SemanticStatement`:
|
|
8
|
+
|
|
9
|
+
- variable occurrences are scanned from the token *values* of the cells the
|
|
10
|
+
legacy tokenizer type gate admits (arguments, names, keyword cells — not
|
|
11
|
+
comments or error tokens), in the same order legacy yields them: outer
|
|
12
|
+
variables before nested ones, the extended-syntax base name before the
|
|
13
|
+
full inner span;
|
|
14
|
+
- resolution goes through `SemanticModel.find_variable()` (no `ModelHelper`,
|
|
15
|
+
no `Namespace` re-resolution on the model path);
|
|
16
|
+
- bare-``$var`` expression references come from the pre-computed
|
|
17
|
+
`PYTHON_VARIABLE_REF` sub-tokens of `CONDITION` cells.
|
|
18
|
+
|
|
19
|
+
Consumers differ only in how they use the candidates: selection range takes
|
|
20
|
+
the first candidate whose range contains the cursor; inline values and debug
|
|
21
|
+
extraction re-resolve the primary candidate of each occurrence at the
|
|
22
|
+
debugger's stopped location and keep only found definitions.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from typing import Iterator, List, Optional
|
|
27
|
+
|
|
28
|
+
from robotcode.core.lsp.types import Position, Range
|
|
29
|
+
from robotcode.robot.diagnostics.semantic_analyzer.enums import NodeKind, TokenKind
|
|
30
|
+
from robotcode.robot.diagnostics.semantic_analyzer.model import SemanticModel
|
|
31
|
+
from robotcode.robot.diagnostics.semantic_analyzer.nodes import SemanticStatement, SemanticToken
|
|
32
|
+
from robotcode.robot.diagnostics.semantic_analyzer.variable_tokenizer import (
|
|
33
|
+
_MATCH_EXTENDED,
|
|
34
|
+
_build_python_expression_sub_tokens,
|
|
35
|
+
)
|
|
36
|
+
from robotcode.robot.utils.variables import contains_variable, is_number_literal, search_variable
|
|
37
|
+
|
|
38
|
+
# Statement cells the legacy `tokenize_variables` walk actually scans — its
|
|
39
|
+
# type gate allows `Token.ALLOW_VARIABLES` (arguments, names, test-case and
|
|
40
|
+
# keyword definition names) plus KEYWORD, ASSIGN, and OPTION cells;
|
|
41
|
+
# `Token.VARIABLE` cells pass through whole. Comments, error tokens, setting
|
|
42
|
+
# names, control-flow words, and structural tokens are never scanned.
|
|
43
|
+
_SCANNABLE_KINDS = frozenset(
|
|
44
|
+
{
|
|
45
|
+
TokenKind.ARGUMENT,
|
|
46
|
+
TokenKind.TAG,
|
|
47
|
+
TokenKind.CONDITION,
|
|
48
|
+
TokenKind.NAMED_ARGUMENT_NAME,
|
|
49
|
+
TokenKind.NAMED_ARGUMENT_VALUE,
|
|
50
|
+
TokenKind.PARAMETER,
|
|
51
|
+
TokenKind.OPTION,
|
|
52
|
+
TokenKind.OPTION_NAME,
|
|
53
|
+
TokenKind.OPTION_VALUE,
|
|
54
|
+
TokenKind.TEST_NAME,
|
|
55
|
+
TokenKind.KEYWORD_NAME,
|
|
56
|
+
TokenKind.KEYWORD,
|
|
57
|
+
TokenKind.KEYWORD_INNER,
|
|
58
|
+
TokenKind.BDD_PREFIX,
|
|
59
|
+
TokenKind.NAMESPACE,
|
|
60
|
+
TokenKind.IMPORT_NAME,
|
|
61
|
+
TokenKind.VARIABLE,
|
|
62
|
+
TokenKind.VARIABLE_NOT_FOUND,
|
|
63
|
+
TokenKind.VARIABLE_NAME,
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def scannable_statement_tokens(stmt: SemanticStatement) -> List[SemanticToken]:
|
|
69
|
+
"""The top-level tokens of a statement that the legacy variable iteration
|
|
70
|
+
would scan (see the `_SCANNABLE_KINDS` gate above)."""
|
|
71
|
+
return [token for token in stmt.tokens if token.kind in _SCANNABLE_KINDS]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class ModelVariableCandidate:
|
|
76
|
+
"""One selectable/resolvable variable occurrence candidate."""
|
|
77
|
+
|
|
78
|
+
range: Range
|
|
79
|
+
lookup_name: str
|
|
80
|
+
primary: bool
|
|
81
|
+
"""True for the first candidate of an occurrence — the one whose lookup
|
|
82
|
+
name mirrors the legacy resolution (extended syntax resolves the base)."""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# Statements whose variable-family cells come from `Token.VARIABLE` in the
|
|
86
|
+
# AST (definition names, FOR/EXCEPT/VAR targets) — outside the legacy
|
|
87
|
+
# tokenizer's type gate. Assign targets in keyword calls / inline IF are
|
|
88
|
+
# `Token.ASSIGN` and tokenize normally.
|
|
89
|
+
_UNGATED_STMT_KINDS = frozenset({NodeKind.VARIABLE_DEF, NodeKind.FOR_HEADER, NodeKind.EXCEPT_HEADER})
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def iter_model_variable_candidates(
|
|
93
|
+
token: SemanticToken,
|
|
94
|
+
model: SemanticModel,
|
|
95
|
+
resolve_line: int,
|
|
96
|
+
resolve_col: Optional[int] = None,
|
|
97
|
+
stmt_kind: Optional[NodeKind] = None,
|
|
98
|
+
) -> Iterator[ModelVariableCandidate]:
|
|
99
|
+
"""Yield the regular variable candidates of one statement token in legacy
|
|
100
|
+
order (`iter_variables_from_token`).
|
|
101
|
+
|
|
102
|
+
``resolve_line`` / ``resolve_col`` form the position used for
|
|
103
|
+
visibility-aware lookups (the cursor position for selection range, the
|
|
104
|
+
debugger's stopped location for inline values). ``stmt_kind`` is the
|
|
105
|
+
containing statement's kind — it decides whether a variable-family cell
|
|
106
|
+
is a `Token.VARIABLE` (processed whole, outside the legacy tokenizer's
|
|
107
|
+
type gate) or a tokenized `Token.ASSIGN`.
|
|
108
|
+
"""
|
|
109
|
+
if token.value and token.col_offset is not None:
|
|
110
|
+
ungated = stmt_kind in _UNGATED_STMT_KINDS and token.kind in (
|
|
111
|
+
TokenKind.VARIABLE,
|
|
112
|
+
TokenKind.VARIABLE_NOT_FOUND,
|
|
113
|
+
TokenKind.VARIABLE_NAME,
|
|
114
|
+
)
|
|
115
|
+
yield from _scan_value(token.value, token.line, token.col_offset, ungated, model, resolve_line, resolve_col)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def iter_condition_ref_candidates(token: SemanticToken) -> Iterator[ModelVariableCandidate]:
|
|
119
|
+
"""Yield the bare ``$var`` expression-reference candidates of a
|
|
120
|
+
`CONDITION` cell from its pre-computed `PYTHON_VARIABLE_REF` sub-tokens
|
|
121
|
+
(legacy `iter_expression_variables_from_token`). Used by inline values
|
|
122
|
+
and debug extraction, but not by selection range."""
|
|
123
|
+
if token.kind is not TokenKind.CONDITION:
|
|
124
|
+
return
|
|
125
|
+
for sub in token.sub_tokens or []:
|
|
126
|
+
if sub.kind is TokenKind.PYTHON_VARIABLE_REF:
|
|
127
|
+
yield from _expression_ref_candidate(sub)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _scan_value(
|
|
131
|
+
value: str,
|
|
132
|
+
line: int,
|
|
133
|
+
col_offset: int,
|
|
134
|
+
ungated: bool,
|
|
135
|
+
model: SemanticModel,
|
|
136
|
+
resolve_line: int,
|
|
137
|
+
resolve_col: Optional[int],
|
|
138
|
+
) -> Iterator[ModelVariableCandidate]:
|
|
139
|
+
"""Scan a value fragment for variable occurrences, mirroring the legacy
|
|
140
|
+
`tokenize_variables` walk (outer occurrence first, then its index-access
|
|
141
|
+
content and the rest of the fragment)."""
|
|
142
|
+
pos = 0
|
|
143
|
+
while pos < len(value):
|
|
144
|
+
matcher = search_variable(value[pos:], "$@&%", ignore_errors=True)
|
|
145
|
+
base = matcher.base
|
|
146
|
+
if base is None:
|
|
147
|
+
if ungated and pos == 0 and len(value) >= 2 and value[0] in "$@&%" and value[1] == "{":
|
|
148
|
+
# Unclosed variable at the start of the cell (`${aaa`):
|
|
149
|
+
# legacy yields the whole raw token
|
|
150
|
+
# (`strip_variable_token` leaves unparseable values untouched).
|
|
151
|
+
yield ModelVariableCandidate(
|
|
152
|
+
range=_make_range(line, col_offset, len(value)),
|
|
153
|
+
lookup_name=value,
|
|
154
|
+
primary=True,
|
|
155
|
+
)
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
var_start = pos + matcher.start
|
|
159
|
+
name = matcher.name or ""
|
|
160
|
+
if not name:
|
|
161
|
+
# `${}` / `@{}`: legacy yields the whole unstripped remainder once
|
|
162
|
+
# and stops scanning the token.
|
|
163
|
+
span = value[var_start : pos + matcher.end] or value[var_start:]
|
|
164
|
+
yield ModelVariableCandidate(
|
|
165
|
+
range=_make_range(line, col_offset + var_start, len(span)),
|
|
166
|
+
lookup_name=span,
|
|
167
|
+
primary=True,
|
|
168
|
+
)
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
yield from _occurrence_candidates(
|
|
172
|
+
name, base, line, col_offset + var_start, ungated, model, resolve_line, resolve_col
|
|
173
|
+
)
|
|
174
|
+
if ungated:
|
|
175
|
+
# An ungated cell is processed as a single whole occurrence.
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
# Continue right after the variable's closing brace: index-access
|
|
179
|
+
# content (`${x}[${i}]`) and the remaining text are scanned like the
|
|
180
|
+
# legacy rest-recursion.
|
|
181
|
+
pos = var_start + len(name)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _occurrence_candidates(
|
|
185
|
+
name: str,
|
|
186
|
+
base: str,
|
|
187
|
+
line: int,
|
|
188
|
+
col_offset: int,
|
|
189
|
+
ungated: bool,
|
|
190
|
+
model: SemanticModel,
|
|
191
|
+
resolve_line: int,
|
|
192
|
+
resolve_col: Optional[int],
|
|
193
|
+
) -> Iterator[ModelVariableCandidate]:
|
|
194
|
+
"""Candidates of one well-formed variable occurrence ``name`` (e.g.
|
|
195
|
+
``${obj.attr}``) whose ``$`` sits at ``col_offset``.
|
|
196
|
+
|
|
197
|
+
Mirrors the legacy resolution order: raw lookup → number-literal skip →
|
|
198
|
+
extended-syntax base lookup → not-found candidates; nested variables
|
|
199
|
+
always follow their container.
|
|
200
|
+
"""
|
|
201
|
+
if base.startswith("{") and base.endswith("}"):
|
|
202
|
+
# `${{expr}}`: only bare `$var` refs inside the expression count.
|
|
203
|
+
yield from _python_expression_candidates(base[1:-1], line, col_offset + 3)
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
nested = contains_variable(base, "$@&%")
|
|
207
|
+
stripped = base.strip()
|
|
208
|
+
inner_range = _make_range(
|
|
209
|
+
line,
|
|
210
|
+
col_offset + 2 + (len(base) - len(base.lstrip())),
|
|
211
|
+
len(stripped),
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
if model.find_variable(name, resolve_line, resolve_col, extended=False) is not None:
|
|
215
|
+
# Raw name resolves (including literal names with spaces or nested
|
|
216
|
+
# syntax, e.g. a definition row `${VALID VAR ${A}}`).
|
|
217
|
+
yield ModelVariableCandidate(range=inner_range, lookup_name=name, primary=True)
|
|
218
|
+
elif is_number_literal(name):
|
|
219
|
+
# Number literals (`${42}`, `${0x1F}`) are values, not variables.
|
|
220
|
+
pass
|
|
221
|
+
else:
|
|
222
|
+
extended = _MATCH_EXTENDED.match(base)
|
|
223
|
+
if extended is not None:
|
|
224
|
+
base_name = extended.group(1).strip()
|
|
225
|
+
base_lookup = f"{name[0]}{{{base_name}}}"
|
|
226
|
+
base_range = _make_range(line, col_offset + 2, len(base_name))
|
|
227
|
+
if base_name and not contains_variable(base_name, "$@&%"):
|
|
228
|
+
if model.find_variable(base_lookup, resolve_line, resolve_col, extended=False) is not None:
|
|
229
|
+
yield ModelVariableCandidate(range=base_range, lookup_name=base_lookup, primary=True)
|
|
230
|
+
elif not is_number_literal(base_lookup) and not contains_variable(base, "$@&"):
|
|
231
|
+
# Legacy quirk: the unresolved-container check runs with
|
|
232
|
+
# the default `$@&` identifiers, so `%{}`-only nesting
|
|
233
|
+
# still yields the not-found pair.
|
|
234
|
+
yield ModelVariableCandidate(range=base_range, lookup_name=base_lookup, primary=True)
|
|
235
|
+
yield ModelVariableCandidate(range=inner_range, lookup_name=name, primary=False)
|
|
236
|
+
elif not nested:
|
|
237
|
+
yield ModelVariableCandidate(range=inner_range, lookup_name=name, primary=True)
|
|
238
|
+
|
|
239
|
+
if nested:
|
|
240
|
+
if ungated:
|
|
241
|
+
# Legacy re-slices ungated containers raw (`value[2:-1]`) instead
|
|
242
|
+
# of tokenizing them: the whole inner span comes back once as an
|
|
243
|
+
# unresolvable pseudo-variable; the nested variables themselves
|
|
244
|
+
# are never reached.
|
|
245
|
+
yield ModelVariableCandidate(
|
|
246
|
+
range=_make_range(line, col_offset + 2, len(base)),
|
|
247
|
+
lookup_name=base,
|
|
248
|
+
primary=True,
|
|
249
|
+
)
|
|
250
|
+
return
|
|
251
|
+
# Nested occurrences inside the container's braces.
|
|
252
|
+
yield from _scan_value(base, line, col_offset + 2, False, model, resolve_line, resolve_col)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _python_expression_candidates(expr: str, line: int, col_offset: int) -> Iterator[ModelVariableCandidate]:
|
|
256
|
+
"""Name-only candidates for the ``$var`` refs of a `${{expr}}` body,
|
|
257
|
+
positions identical to the analyzer's `PYTHON_VARIABLE_REF` sub-tokens."""
|
|
258
|
+
for ref in _build_python_expression_sub_tokens(expr, line, col_offset) or []:
|
|
259
|
+
yield from _expression_ref_candidate(ref)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _expression_ref_candidate(ref: SemanticToken) -> Iterator[ModelVariableCandidate]:
|
|
263
|
+
"""Name-only candidate of a `PYTHON_VARIABLE_REF` (`$var` → `var`)."""
|
|
264
|
+
if ref.length <= 1:
|
|
265
|
+
return
|
|
266
|
+
name = ref.value[1:] if ref.value.startswith("$") else ref.value
|
|
267
|
+
yield ModelVariableCandidate(
|
|
268
|
+
range=_make_range(ref.line, ref.col_offset + 1, ref.length - 1),
|
|
269
|
+
lookup_name=f"${{{name}}}",
|
|
270
|
+
primary=True,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _make_range(line: int, col_offset: int, length: int) -> Range:
|
|
275
|
+
return Range(
|
|
276
|
+
start=Position(line=line - 1, character=col_offset),
|
|
277
|
+
end=Position(line=line - 1, character=col_offset + length),
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def iter_statement_tokens_at_line(model: SemanticModel, line: int) -> Iterator[SemanticToken]:
|
|
282
|
+
"""Scannable top-level tokens of every statement covering a 1-indexed
|
|
283
|
+
line.
|
|
284
|
+
|
|
285
|
+
A line can hold several statements (inline `IF` header plus its branch
|
|
286
|
+
calls), so the single most-specific `statement_at()` result is not
|
|
287
|
+
enough for position-based candidate searches.
|
|
288
|
+
"""
|
|
289
|
+
for stmt in model.statements:
|
|
290
|
+
if stmt.line_start <= line <= stmt.line_end:
|
|
291
|
+
yield from scannable_statement_tokens(stmt)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def iter_line_variable_candidates(
|
|
295
|
+
model: SemanticModel,
|
|
296
|
+
line: int,
|
|
297
|
+
resolve_col: Optional[int] = None,
|
|
298
|
+
) -> Iterator[ModelVariableCandidate]:
|
|
299
|
+
"""Regular variable candidates of every statement covering a 1-indexed
|
|
300
|
+
line."""
|
|
301
|
+
for stmt in model.statements:
|
|
302
|
+
if stmt.line_start <= line <= stmt.line_end:
|
|
303
|
+
for token in scannable_statement_tokens(stmt):
|
|
304
|
+
yield from iter_model_variable_candidates(token, model, line, resolve_col, stmt.kind)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def iter_line_condition_ref_candidates(model: SemanticModel, line: int) -> Iterator[ModelVariableCandidate]:
|
|
308
|
+
"""Bare ``$var`` expression-reference candidates of every condition cell
|
|
309
|
+
on a 1-indexed line."""
|
|
310
|
+
for stmt in model.statements:
|
|
311
|
+
if stmt.line_start <= line <= stmt.line_end:
|
|
312
|
+
for token in stmt.tokens:
|
|
313
|
+
yield from iter_condition_ref_candidates(token)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def find_model_variable_range_at(
|
|
317
|
+
model: SemanticModel,
|
|
318
|
+
position: Position,
|
|
319
|
+
within: Range,
|
|
320
|
+
) -> Optional[Range]:
|
|
321
|
+
"""First candidate range containing the position — the model-path
|
|
322
|
+
replacement for the legacy selection-range variable step.
|
|
323
|
+
|
|
324
|
+
``within`` is the range of the AST token under the cursor: legacy only
|
|
325
|
+
iterates the variables of that one token, so candidates outside it (a
|
|
326
|
+
neighbouring cell whose end touches the position) must not match.
|
|
327
|
+
"""
|
|
328
|
+
for candidate in iter_line_variable_candidates(model, position.line + 1, position.character):
|
|
329
|
+
if position in candidate.range and candidate.range.start >= within.start and candidate.range.end <= within.end:
|
|
330
|
+
return candidate.range
|
|
331
|
+
return None
|