robotcode-language-server 2.7.0__py3-none-any.whl → 2.8.0.dev8__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/cli.py +30 -5
- robotcode/language_server/common/parts/diagnostics.py +50 -17
- robotcode/language_server/common/parts/documents.py +32 -26
- 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 +437 -146
- 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/documentation_viewer.py +127 -0
- 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 +107 -45
- 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 +446 -113
- robotcode/language_server/robotframework/parts/signature_help.py +162 -70
- robotcode/language_server/robotframework/protocol.py +12 -1
- robotcode/language_server/robotframework/server.py +4 -2
- {robotcode_language_server-2.7.0.dist-info → robotcode_language_server-2.8.0.dev8.dist-info}/METADATA +7 -6
- {robotcode_language_server-2.7.0.dist-info → robotcode_language_server-2.8.0.dev8.dist-info}/RECORD +34 -32
- {robotcode_language_server-2.7.0.dist-info → robotcode_language_server-2.8.0.dev8.dist-info}/WHEEL +1 -1
- {robotcode_language_server-2.7.0.dist-info → robotcode_language_server-2.8.0.dev8.dist-info}/entry_points.txt +0 -0
|
@@ -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
|
|
@@ -26,6 +26,7 @@ from robotcode.robot.diagnostics.entities import (
|
|
|
26
26
|
ResourceEntry,
|
|
27
27
|
VariableDefinition,
|
|
28
28
|
VariableDefinitionType,
|
|
29
|
+
VariablesEntry,
|
|
29
30
|
)
|
|
30
31
|
from robotcode.robot.diagnostics.library_doc import (
|
|
31
32
|
RESOURCE_FILE_EXTENSION,
|
|
@@ -33,7 +34,6 @@ from robotcode.robot.diagnostics.library_doc import (
|
|
|
33
34
|
KeywordDoc,
|
|
34
35
|
LibraryDoc,
|
|
35
36
|
)
|
|
36
|
-
from robotcode.robot.diagnostics.model_helper import ModelHelper
|
|
37
37
|
from robotcode.robot.diagnostics.namespace import Namespace
|
|
38
38
|
from robotcode.robot.utils import RF_VERSION
|
|
39
39
|
from robotcode.robot.utils.ast import (
|
|
@@ -55,7 +55,7 @@ _ReferencesMethod = Callable[
|
|
|
55
55
|
]
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
class RobotReferencesProtocolPart(RobotLanguageServerProtocolPart
|
|
58
|
+
class RobotReferencesProtocolPart(RobotLanguageServerProtocolPart):
|
|
59
59
|
_logger = LoggingDescriptor()
|
|
60
60
|
|
|
61
61
|
def __init__(self, parent: "RobotLanguageServerProtocol") -> None:
|
|
@@ -68,7 +68,7 @@ class RobotReferencesProtocolPart(RobotLanguageServerProtocolPart, ModelHelper):
|
|
|
68
68
|
|
|
69
69
|
parent.references.collect.add(self.collect)
|
|
70
70
|
parent.documents.did_change.add(self.document_did_change)
|
|
71
|
-
parent.documents.on_document_cache_invalidated(self.document_did_change)
|
|
71
|
+
parent.documents.on_document_cache_invalidated.add(self.document_did_change)
|
|
72
72
|
parent.diagnostics.on_workspace_diagnostics_break.add(self.on_workspace_diagnostics_break)
|
|
73
73
|
|
|
74
74
|
@event
|
|
@@ -80,7 +80,8 @@ class RobotReferencesProtocolPart(RobotLanguageServerProtocolPart, ModelHelper):
|
|
|
80
80
|
f"**/*.{{{ROBOT_FILE_EXTENSION[1:]},{RESOURCE_FILE_EXTENSION[1:]}}}",
|
|
81
81
|
WatchKind.CREATE | WatchKind.DELETE,
|
|
82
82
|
)
|
|
83
|
-
self.parent.documents_cache.namespace_invalidated(self.namespace_invalidated)
|
|
83
|
+
self.parent.documents_cache.namespace_invalidated.add(self.namespace_invalidated)
|
|
84
|
+
self.parent.documents_cache.namespace_initialized.add(self.namespace_initialized)
|
|
84
85
|
|
|
85
86
|
def do_on_file_changed(self, sender: Any, files: List[FileEvent]) -> None:
|
|
86
87
|
self.clear_cache()
|
|
@@ -93,6 +94,11 @@ class RobotReferencesProtocolPart(RobotLanguageServerProtocolPart, ModelHelper):
|
|
|
93
94
|
def namespace_invalidated(self, sender: Any, namespace: Namespace) -> None:
|
|
94
95
|
self.clear_cache()
|
|
95
96
|
|
|
97
|
+
@language_id("robotframework")
|
|
98
|
+
def namespace_initialized(self, sender: Any, namespace: Namespace) -> None:
|
|
99
|
+
# the new analysis changed the index, so a result found before it can miss its references
|
|
100
|
+
self.clear_cache()
|
|
101
|
+
|
|
96
102
|
def on_workspace_diagnostics_break(self, sender: Any) -> None:
|
|
97
103
|
self.clear_cache()
|
|
98
104
|
|
|
@@ -144,7 +150,37 @@ class RobotReferencesProtocolPart(RobotLanguageServerProtocolPart, ModelHelper):
|
|
|
144
150
|
if result is not None:
|
|
145
151
|
return result
|
|
146
152
|
|
|
147
|
-
return
|
|
153
|
+
return self._references_at_namespace_prefix(document, position, context)
|
|
154
|
+
|
|
155
|
+
def _references_at_namespace_prefix(
|
|
156
|
+
self,
|
|
157
|
+
document: TextDocument,
|
|
158
|
+
position: Position,
|
|
159
|
+
context: ReferenceContext,
|
|
160
|
+
) -> Optional[List[Location]]:
|
|
161
|
+
namespace = self.parent.documents_cache.get_namespace(document)
|
|
162
|
+
|
|
163
|
+
# the analysis keeps the range of every prefix of a keyword call, keyed by the library or resource it names
|
|
164
|
+
entries = [
|
|
165
|
+
entry
|
|
166
|
+
for entry, locations in namespace.namespace_references.items()
|
|
167
|
+
if not isinstance(entry, VariablesEntry) and any(position in location.range for location in locations)
|
|
168
|
+
]
|
|
169
|
+
if not entries:
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
# a prefix has the references of the import it names
|
|
173
|
+
result: List[Location] = []
|
|
174
|
+
for entry in entries:
|
|
175
|
+
if isinstance(entry, ResourceEntry):
|
|
176
|
+
result.extend(self._resource_references(document, entry, context))
|
|
177
|
+
elif entry.alias:
|
|
178
|
+
result.extend(self._library_alias_references(document, entry, context))
|
|
179
|
+
else:
|
|
180
|
+
result.extend(self._library_references(document, entry.library_doc))
|
|
181
|
+
|
|
182
|
+
# a prefix can name more than one entry, and their answers overlap
|
|
183
|
+
return list(dict.fromkeys(result))
|
|
148
184
|
|
|
149
185
|
def _find_references_in_workspace(
|
|
150
186
|
self,
|
|
@@ -224,10 +260,7 @@ class RobotReferencesProtocolPart(RobotLanguageServerProtocolPart, ModelHelper):
|
|
|
224
260
|
include_declaration: bool = True,
|
|
225
261
|
stop_at_first: bool = False,
|
|
226
262
|
) -> List[Location]:
|
|
227
|
-
result = []
|
|
228
|
-
|
|
229
|
-
if include_declaration and variable.source:
|
|
230
|
-
result.append(Location(str(Uri.from_path(variable.source)), variable.name_range))
|
|
263
|
+
result: List[Location] = []
|
|
231
264
|
|
|
232
265
|
if variable.type == VariableDefinitionType.LOCAL_VARIABLE:
|
|
233
266
|
result.extend(self.find_variable_references_in_file(document, variable, False))
|
|
@@ -237,6 +270,11 @@ class RobotReferencesProtocolPart(RobotLanguageServerProtocolPart, ModelHelper):
|
|
|
237
270
|
result.append(next(iter(refs)))
|
|
238
271
|
else:
|
|
239
272
|
result.extend(refs)
|
|
273
|
+
|
|
274
|
+
# the declaration last: LSP4IJ searches from the first location, and finds nothing in a Python file
|
|
275
|
+
if include_declaration and variable.source:
|
|
276
|
+
result.append(Location(str(Uri.from_path(variable.source)), variable.name_range))
|
|
277
|
+
|
|
240
278
|
return result
|
|
241
279
|
|
|
242
280
|
@_logger.call
|
|
@@ -315,33 +353,37 @@ class RobotReferencesProtocolPart(RobotLanguageServerProtocolPart, ModelHelper):
|
|
|
315
353
|
) -> List[Location]:
|
|
316
354
|
result = []
|
|
317
355
|
|
|
318
|
-
if include_declaration and kw_doc.source:
|
|
319
|
-
result.append(Location(str(Uri.from_path(kw_doc.source)), kw_doc.range))
|
|
320
|
-
|
|
321
356
|
refs = self.parent.documents_cache.get_project_index(document).find_keyword_references(kw_doc)
|
|
322
357
|
if stop_at_first and refs:
|
|
323
358
|
result.append(next(iter(refs)))
|
|
324
359
|
else:
|
|
325
360
|
result.extend(refs)
|
|
326
361
|
|
|
362
|
+
# the declaration last: LSP4IJ searches from the first location, and finds nothing in a Python file
|
|
363
|
+
if include_declaration and kw_doc.source:
|
|
364
|
+
result.append(Location(str(Uri.from_path(kw_doc.source)), kw_doc.range))
|
|
365
|
+
|
|
327
366
|
return result
|
|
328
367
|
|
|
329
368
|
@_logger.call
|
|
330
369
|
def _find_library_import_references_in_file(self, doc: TextDocument, library_doc: LibraryDoc) -> List[Location]:
|
|
331
370
|
namespace = self.parent.documents_cache.get_namespace(doc)
|
|
332
371
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
)
|
|
339
|
-
|
|
340
|
-
|
|
372
|
+
# every import statement, also one of a library that is already imported
|
|
373
|
+
result: List[Location] = [
|
|
374
|
+
Location(str(doc.uri), lib_entry.import_range)
|
|
375
|
+
for lib_entry in namespace.import_entries.values()
|
|
376
|
+
if not isinstance(lib_entry, (ResourceEntry, VariablesEntry))
|
|
377
|
+
and lib_entry.import_source == str(doc.uri.to_path())
|
|
378
|
+
and lib_entry.library_doc.source_or_origin == library_doc.source_or_origin
|
|
379
|
+
and lib_entry.library_doc.name == library_doc.name
|
|
380
|
+
]
|
|
381
|
+
|
|
382
|
+
# the analysis also keeps a repeated import as a reference of the first one
|
|
341
383
|
references = namespace.namespace_references
|
|
342
384
|
for k, v in references.items():
|
|
343
385
|
if not k.alias and k.library_doc == library_doc:
|
|
344
|
-
result.extend(v)
|
|
386
|
+
result.extend(location for location in v if location not in result)
|
|
345
387
|
|
|
346
388
|
return result
|
|
347
389
|
|
|
@@ -379,12 +421,7 @@ class RobotReferencesProtocolPart(RobotLanguageServerProtocolPart, ModelHelper):
|
|
|
379
421
|
if library_doc is None:
|
|
380
422
|
return None
|
|
381
423
|
|
|
382
|
-
return self.
|
|
383
|
-
document,
|
|
384
|
-
False,
|
|
385
|
-
self._find_library_import_references_in_file,
|
|
386
|
-
library_doc,
|
|
387
|
-
)
|
|
424
|
+
return self._library_references(document, library_doc)
|
|
388
425
|
|
|
389
426
|
separator = import_node.get_token(RobotToken.WITH_NAME)
|
|
390
427
|
alias_token = import_node.get_tokens(RobotToken.NAME)[-1] if separator else None
|
|
@@ -404,6 +441,19 @@ class RobotReferencesProtocolPart(RobotLanguageServerProtocolPart, ModelHelper):
|
|
|
404
441
|
if entry is None:
|
|
405
442
|
return None
|
|
406
443
|
|
|
444
|
+
return self._library_alias_references(document, entry, context)
|
|
445
|
+
|
|
446
|
+
def _library_references(self, document: TextDocument, library_doc: LibraryDoc) -> List[Location]:
|
|
447
|
+
return self._find_references_in_workspace(
|
|
448
|
+
document,
|
|
449
|
+
False,
|
|
450
|
+
self._find_library_import_references_in_file,
|
|
451
|
+
library_doc,
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
def _library_alias_references(
|
|
455
|
+
self, document: TextDocument, entry: LibraryEntry, context: ReferenceContext
|
|
456
|
+
) -> List[Location]:
|
|
407
457
|
result = list(self.parent.documents_cache.get_project_index(document).find_namespace_references(entry))
|
|
408
458
|
|
|
409
459
|
if context.include_declaration and entry.import_source:
|
|
@@ -415,17 +465,20 @@ class RobotReferencesProtocolPart(RobotLanguageServerProtocolPart, ModelHelper):
|
|
|
415
465
|
def _find_resource_import_references_in_file(self, doc: TextDocument, entry: ResourceEntry) -> List[Location]:
|
|
416
466
|
namespace = self.parent.documents_cache.get_namespace(doc)
|
|
417
467
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
)
|
|
424
|
-
|
|
468
|
+
# every import statement, also one of a resource file that is already imported
|
|
469
|
+
result: List[Location] = [
|
|
470
|
+
Location(str(doc.uri), lib_entry.import_range)
|
|
471
|
+
for lib_entry in namespace.import_entries.values()
|
|
472
|
+
if isinstance(lib_entry, ResourceEntry)
|
|
473
|
+
and lib_entry.import_source == str(doc.uri.to_path())
|
|
474
|
+
and lib_entry.library_doc.source == entry.library_doc.source
|
|
475
|
+
]
|
|
425
476
|
|
|
477
|
+
# the calls with the resource's name as prefix, in every file that imports the resource
|
|
426
478
|
references = namespace.namespace_references
|
|
427
|
-
|
|
428
|
-
|
|
479
|
+
for k, v in references.items():
|
|
480
|
+
if isinstance(k, ResourceEntry) and k.library_doc.source == entry.library_doc.source:
|
|
481
|
+
result.extend(location for location in v if location not in result)
|
|
429
482
|
|
|
430
483
|
return result
|
|
431
484
|
|
|
@@ -448,12 +501,14 @@ class RobotReferencesProtocolPart(RobotLanguageServerProtocolPart, ModelHelper):
|
|
|
448
501
|
if not name_token:
|
|
449
502
|
return None
|
|
450
503
|
|
|
451
|
-
|
|
504
|
+
# the entry of this statement, also of a repeated import of the resource file
|
|
452
505
|
entry = next(
|
|
453
506
|
(
|
|
454
507
|
v
|
|
455
|
-
for v in
|
|
456
|
-
if v
|
|
508
|
+
for v in namespace.import_entries.values()
|
|
509
|
+
if isinstance(v, ResourceEntry)
|
|
510
|
+
and v.import_source == namespace.source
|
|
511
|
+
and v.import_range == range_from_token(name_token)
|
|
457
512
|
),
|
|
458
513
|
None,
|
|
459
514
|
)
|
|
@@ -461,6 +516,11 @@ class RobotReferencesProtocolPart(RobotLanguageServerProtocolPart, ModelHelper):
|
|
|
461
516
|
if entry is None:
|
|
462
517
|
return None
|
|
463
518
|
|
|
519
|
+
return self._resource_references(document, entry, context)
|
|
520
|
+
|
|
521
|
+
def _resource_references(
|
|
522
|
+
self, document: TextDocument, entry: ResourceEntry, context: ReferenceContext
|
|
523
|
+
) -> List[Location]:
|
|
464
524
|
result = self._find_references_in_workspace(
|
|
465
525
|
document,
|
|
466
526
|
False,
|
|
@@ -484,12 +544,14 @@ class RobotReferencesProtocolPart(RobotLanguageServerProtocolPart, ModelHelper):
|
|
|
484
544
|
def _find_variables_import_references_in_file(self, doc: TextDocument, library_doc: LibraryDoc) -> List[Location]:
|
|
485
545
|
namespace = self.parent.documents_cache.get_namespace(doc)
|
|
486
546
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
547
|
+
# every import statement, also one of a variable file that is already imported
|
|
548
|
+
return [
|
|
549
|
+
Location(str(doc.uri), lib_entry.import_range)
|
|
550
|
+
for lib_entry in namespace.import_entries.values()
|
|
551
|
+
if isinstance(lib_entry, VariablesEntry)
|
|
552
|
+
and lib_entry.import_source == str(doc.uri.to_path())
|
|
553
|
+
and lib_entry.library_doc.source == library_doc.source
|
|
554
|
+
]
|
|
493
555
|
|
|
494
556
|
def references_VariablesImport( # noqa: N802
|
|
495
557
|
self,
|
|
@@ -33,7 +33,6 @@ from robotcode.robot.diagnostics.entities import (
|
|
|
33
33
|
VariableDefinitionType,
|
|
34
34
|
)
|
|
35
35
|
from robotcode.robot.diagnostics.library_doc import KeywordDoc
|
|
36
|
-
from robotcode.robot.diagnostics.model_helper import ModelHelper
|
|
37
36
|
|
|
38
37
|
from ...common.parts.rename import CantRenameError
|
|
39
38
|
from .protocol_part import RobotLanguageServerProtocolPart
|
|
@@ -48,7 +47,7 @@ _PrepareRenameMethod = Callable[[ast.AST, TextDocument, Position], Optional[Prep
|
|
|
48
47
|
_T = TypeVar("_T", bound=Callable[..., Any])
|
|
49
48
|
|
|
50
49
|
|
|
51
|
-
class RobotRenameProtocolPart(RobotLanguageServerProtocolPart
|
|
50
|
+
class RobotRenameProtocolPart(RobotLanguageServerProtocolPart):
|
|
52
51
|
_logger = LoggingDescriptor()
|
|
53
52
|
|
|
54
53
|
def __init__(self, parent: "RobotLanguageServerProtocol") -> None:
|