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.
Files changed (34) hide show
  1. robotcode/language_server/__version__.py +1 -1
  2. robotcode/language_server/cli.py +30 -5
  3. robotcode/language_server/common/parts/diagnostics.py +50 -17
  4. robotcode/language_server/common/parts/documents.py +32 -26
  5. robotcode/language_server/common/parts/workspace.py +57 -55
  6. robotcode/language_server/common/protocol.py +3 -1
  7. robotcode/language_server/robotframework/configuration.py +3 -0
  8. robotcode/language_server/robotframework/parts/code_action_documentation.py +437 -146
  9. robotcode/language_server/robotframework/parts/code_action_helper_mixin.py +17 -2
  10. robotcode/language_server/robotframework/parts/code_action_quick_fixes.py +6 -25
  11. robotcode/language_server/robotframework/parts/code_action_refactor.py +8 -6
  12. robotcode/language_server/robotframework/parts/code_lens.py +3 -4
  13. robotcode/language_server/robotframework/parts/completion.py +193 -21
  14. robotcode/language_server/robotframework/parts/debugging_utils.py +75 -6
  15. robotcode/language_server/robotframework/parts/diagnostics.py +15 -3
  16. robotcode/language_server/robotframework/parts/documentation_viewer.py +127 -0
  17. robotcode/language_server/robotframework/parts/documents_cache.py +5 -1
  18. robotcode/language_server/robotframework/parts/hover.py +103 -21
  19. robotcode/language_server/robotframework/parts/http_server.py +17 -7
  20. robotcode/language_server/robotframework/parts/inline_value.py +69 -7
  21. robotcode/language_server/robotframework/parts/keywords_treeview.py +151 -53
  22. robotcode/language_server/robotframework/parts/model_variables.py +331 -0
  23. robotcode/language_server/robotframework/parts/references.py +107 -45
  24. robotcode/language_server/robotframework/parts/rename.py +1 -2
  25. robotcode/language_server/robotframework/parts/robot_workspace.py +229 -25
  26. robotcode/language_server/robotframework/parts/selection_range.py +26 -13
  27. robotcode/language_server/robotframework/parts/semantic_tokens.py +446 -113
  28. robotcode/language_server/robotframework/parts/signature_help.py +162 -70
  29. robotcode/language_server/robotframework/protocol.py +12 -1
  30. robotcode/language_server/robotframework/server.py +4 -2
  31. {robotcode_language_server-2.7.0.dist-info → robotcode_language_server-2.8.0.dev8.dist-info}/METADATA +7 -6
  32. {robotcode_language_server-2.7.0.dist-info → robotcode_language_server-2.8.0.dev8.dist-info}/RECORD +34 -32
  33. {robotcode_language_server-2.7.0.dist-info → robotcode_language_server-2.8.0.dev8.dist-info}/WHEEL +1 -1
  34. {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, ModelHelper):
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 None
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
- result: List[Location] = []
334
- for lib_entry in namespace.libraries.values():
335
- if (
336
- lib_entry.import_source == str(doc.uri.to_path())
337
- and lib_entry.library_doc.source_or_origin == library_doc.source_or_origin
338
- ):
339
- result.append(Location(str(doc.uri), lib_entry.import_range))
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._find_references_in_workspace(
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
- result: List[Location] = []
419
- for lib_entry in namespace.resources.values():
420
- if (
421
- lib_entry.import_source == str(doc.uri.to_path())
422
- and lib_entry.library_doc.source == entry.library_doc.source
423
- ):
424
- result.append(Location(str(doc.uri), lib_entry.import_range))
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
- if entry in references:
428
- result.extend(references[entry])
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
- entries = namespace.resources
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 entries.values()
456
- if v.import_source == namespace.source and v.import_range == range_from_token(name_token)
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
- result: List[Location] = []
488
- for lib_entry in namespace.variables_imports.values():
489
- if lib_entry.import_source == str(doc.uri.to_path()) and lib_entry.library_doc.source == library_doc.source:
490
- result.append(Location(str(doc.uri), lib_entry.import_range))
491
-
492
- return result
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, ModelHelper):
50
+ class RobotRenameProtocolPart(RobotLanguageServerProtocolPart):
52
51
  _logger = LoggingDescriptor()
53
52
 
54
53
  def __init__(self, parent: "RobotLanguageServerProtocol") -> None: