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.
Files changed (31) hide show
  1. robotcode/language_server/__version__.py +1 -1
  2. robotcode/language_server/common/parts/diagnostics.py +50 -17
  3. robotcode/language_server/common/parts/documents.py +34 -31
  4. robotcode/language_server/common/parts/workspace.py +57 -55
  5. robotcode/language_server/common/protocol.py +3 -1
  6. robotcode/language_server/robotframework/configuration.py +3 -0
  7. robotcode/language_server/robotframework/parts/code_action_documentation.py +440 -129
  8. robotcode/language_server/robotframework/parts/code_action_helper_mixin.py +17 -2
  9. robotcode/language_server/robotframework/parts/code_action_quick_fixes.py +6 -25
  10. robotcode/language_server/robotframework/parts/code_action_refactor.py +8 -6
  11. robotcode/language_server/robotframework/parts/code_lens.py +3 -4
  12. robotcode/language_server/robotframework/parts/completion.py +193 -21
  13. robotcode/language_server/robotframework/parts/debugging_utils.py +75 -6
  14. robotcode/language_server/robotframework/parts/diagnostics.py +15 -3
  15. robotcode/language_server/robotframework/parts/documents_cache.py +5 -1
  16. robotcode/language_server/robotframework/parts/hover.py +103 -21
  17. robotcode/language_server/robotframework/parts/http_server.py +17 -7
  18. robotcode/language_server/robotframework/parts/inline_value.py +69 -7
  19. robotcode/language_server/robotframework/parts/keywords_treeview.py +151 -53
  20. robotcode/language_server/robotframework/parts/model_variables.py +331 -0
  21. robotcode/language_server/robotframework/parts/references.py +47 -31
  22. robotcode/language_server/robotframework/parts/rename.py +1 -2
  23. robotcode/language_server/robotframework/parts/robot_workspace.py +229 -25
  24. robotcode/language_server/robotframework/parts/selection_range.py +26 -13
  25. robotcode/language_server/robotframework/parts/semantic_tokens.py +414 -113
  26. robotcode/language_server/robotframework/parts/signature_help.py +162 -70
  27. robotcode/language_server/robotframework/protocol.py +1 -0
  28. {robotcode_language_server-2.6.2.dist-info → robotcode_language_server-2.8.0.dev548.dist-info}/METADATA +7 -6
  29. {robotcode_language_server-2.6.2.dist-info → robotcode_language_server-2.8.0.dev548.dist-info}/RECORD +31 -30
  30. {robotcode_language_server-2.6.2.dist-info → robotcode_language_server-2.8.0.dev548.dist-info}/WHEEL +1 -1
  31. {robotcode_language_server-2.6.2.dist-info → robotcode_language_server-2.8.0.dev548.dist-info}/entry_points.txt +0 -0
@@ -3,24 +3,30 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass
4
4
  from typing import TYPE_CHECKING, Any, Optional
5
5
 
6
- from robot.parsing.model.statements import Statement
6
+ from robot.parsing.model.statements import IfElseHeader, Statement, WhileHeader
7
7
 
8
8
  from robotcode.core.lsp.types import Position, Range, TextDocumentIdentifier
9
9
  from robotcode.core.utils.dataclasses import CamelSnakeMixin
10
10
  from robotcode.core.utils.logging import LoggingDescriptor
11
11
  from robotcode.jsonrpc2.protocol import rpc_method
12
12
  from robotcode.robot.diagnostics.model_helper import ModelHelper
13
+ from robotcode.robot.diagnostics.semantic_analyzer.model import SemanticModel
13
14
  from robotcode.robot.utils.ast import (
14
15
  get_nodes_at_position,
15
16
  get_tokens_at_position,
16
17
  range_from_token,
17
18
  )
18
19
 
20
+ from .model_variables import iter_line_condition_ref_candidates, iter_line_variable_candidates
19
21
  from .protocol_part import RobotLanguageServerProtocolPart
20
22
 
21
23
  if TYPE_CHECKING:
22
24
  from ..protocol import RobotLanguageServerProtocol
23
25
 
26
+ # The statements whose first argument cell is an expression (legacy
27
+ # `ModelHelper.get_expression_statement_types()`).
28
+ _EXPRESSION_STATEMENT_TYPES = (IfElseHeader, WhileHeader)
29
+
24
30
 
25
31
  @dataclass(repr=False)
26
32
  class EvaluatableExpressionParams(CamelSnakeMixin):
@@ -34,12 +40,61 @@ class EvaluatableExpression(CamelSnakeMixin):
34
40
  expression: Optional[str]
35
41
 
36
42
 
37
- class RobotDebuggingUtilsProtocolPart(RobotLanguageServerProtocolPart, ModelHelper):
43
+ class RobotDebuggingUtilsProtocolPart(RobotLanguageServerProtocolPart):
38
44
  _logger = LoggingDescriptor()
39
45
 
40
46
  def __init__(self, parent: RobotLanguageServerProtocol) -> None:
41
47
  super().__init__(parent)
42
48
 
49
+ @staticmethod
50
+ def _find_model_expression(
51
+ semantic_model: SemanticModel,
52
+ position: Position,
53
+ token_range: Range,
54
+ expression_range: Optional[Range],
55
+ ) -> Optional[EvaluatableExpression]:
56
+ """Model path: first resolvable variable occurrence at the position.
57
+
58
+ Mirrors the legacy two-step lookup: regular variables limited to the
59
+ AST token under the cursor first, then — only when the cursor sits in
60
+ the condition cell of an expression statement (``expression_range``) —
61
+ the bare ``$var`` expression references as fallback.
62
+ """
63
+ line, col = position.line + 1, position.character
64
+
65
+ found: Optional[tuple[Range, Any]] = None
66
+ for candidate in iter_line_variable_candidates(semantic_model, line, col):
67
+ if (
68
+ position not in candidate.range
69
+ or candidate.range.start < token_range.start
70
+ or candidate.range.end > token_range.end
71
+ ):
72
+ continue
73
+ # `extended=False`: the extended-syntax fallback is already
74
+ # encoded in the candidates' lookup names.
75
+ var = semantic_model.find_variable(candidate.lookup_name, line, col, extended=False)
76
+ if var is not None:
77
+ found = (candidate.range, var)
78
+ break
79
+
80
+ if found is None and expression_range is not None and position in expression_range:
81
+ for candidate in iter_line_condition_ref_candidates(semantic_model, line):
82
+ if position not in candidate.range:
83
+ continue
84
+ var = semantic_model.find_variable(candidate.lookup_name, line, col, extended=False)
85
+ if var is not None:
86
+ found = (candidate.range, var)
87
+ break
88
+
89
+ if found is None:
90
+ return None
91
+
92
+ var_range, var = found
93
+ if var.name == "${CURDIR}":
94
+ return None
95
+
96
+ return EvaluatableExpression(var_range, var.name)
97
+
43
98
  @rpc_method(name="robot/debugging/getEvaluatableExpression", param_type=EvaluatableExpressionParams, threaded=True)
44
99
  @_logger.call
45
100
  async def _get_evaluatable_expression(
@@ -59,17 +114,31 @@ class RobotDebuggingUtilsProtocolPart(RobotLanguageServerProtocolPart, ModelHelp
59
114
  model = self.parent.documents_cache.get_model(document)
60
115
 
61
116
  nodes = get_nodes_at_position(model, position)
117
+ if not nodes:
118
+ return None
62
119
  node = nodes[-1]
63
120
 
64
121
  if not isinstance(node, Statement):
65
122
  return None
66
123
 
67
- token = get_tokens_at_position(node, position)[-1]
124
+ tokens = get_tokens_at_position(node, position)
125
+ if not tokens:
126
+ return None
127
+ token = tokens[-1]
128
+
129
+ semantic_model = namespace.semantic_model
130
+ if semantic_model is not None:
131
+ expression_range: Optional[Range] = None
132
+ if isinstance(node, _EXPRESSION_STATEMENT_TYPES):
133
+ arg_token = node.get_token(RobotToken.ARGUMENT)
134
+ if arg_token is not None:
135
+ expression_range = range_from_token(arg_token)
136
+ return self._find_model_expression(semantic_model, position, range_from_token(token), expression_range)
68
137
 
69
138
  token_and_var = next(
70
139
  (
71
140
  (t, v)
72
- for t, v in self.iter_variables_from_token(token, namespace, position)
141
+ for t, v in ModelHelper.iter_variables_from_token(token, namespace, position)
73
142
  if position in range_from_token(t)
74
143
  ),
75
144
  None,
@@ -77,14 +146,14 @@ class RobotDebuggingUtilsProtocolPart(RobotLanguageServerProtocolPart, ModelHelp
77
146
 
78
147
  if (
79
148
  token_and_var is None
80
- and isinstance(node, self.get_expression_statement_types())
149
+ and isinstance(node, ModelHelper.get_expression_statement_types())
81
150
  and (token := node.get_token(RobotToken.ARGUMENT)) is not None
82
151
  and position in range_from_token(token)
83
152
  ):
84
153
  token_and_var = next(
85
154
  (
86
155
  (var_token, var)
87
- for var_token, var in self.iter_expression_variables_from_token(token, namespace, position)
156
+ for var_token, var in ModelHelper.iter_expression_variables_from_token(token, namespace, position)
88
157
  if position in range_from_token(var_token)
89
158
  ),
90
159
  None,
@@ -1,7 +1,7 @@
1
1
  from concurrent.futures import CancelledError
2
2
  from typing import TYPE_CHECKING, Any, List, Optional
3
3
 
4
- from robotcode.core.concurrent import check_current_task_canceled
4
+ from robotcode.core.concurrent import check_current_task_canceled, run_as_task
5
5
  from robotcode.core.language import language_id
6
6
  from robotcode.core.lsp.types import (
7
7
  Diagnostic,
@@ -50,7 +50,8 @@ class RobotDiagnosticsProtocolPart(RobotLanguageServerProtocolPart):
50
50
 
51
51
  def _on_initialized(self, sender: Any) -> None:
52
52
  self.parent.diagnostics.analyze.add(self.analyze)
53
- self.parent.documents_cache.namespace_initialized(self._on_namespace_initialized)
53
+ self.parent.documents_cache.namespace_initialized.add(self._on_namespace_initialized)
54
+ self.parent.documents_cache.namespace_invalidated.add(self._on_namespace_invalidated)
54
55
  self.parent.documents_cache.libraries_changed.add(self._on_libraries_changed)
55
56
  self.parent.documents_cache.resources_changed.add(self._on_resources_changed)
56
57
  self.parent.documents_cache.variables_changed.add(self._on_variables_changed)
@@ -85,8 +86,19 @@ class RobotDiagnosticsProtocolPart(RobotLanguageServerProtocolPart):
85
86
 
86
87
  @language_id("robotframework")
87
88
  def _on_namespace_initialized(self, sender: Any, namespace: Namespace) -> None:
89
+ # only a document open in the editor: a closed one gets its diagnostics renewed through the invalidation
90
+ # that started the rebuild or through its own changes, marking it again would analyze it once more
91
+ if namespace.document is not None and namespace.document.opened_in_editor:
92
+ # the namespace is initialized under the lock of the document's cache entry, while the diagnostics
93
+ # loop holds the diagnostics lock when it waits for a task that may wait for that entry
94
+ run_as_task(self.parent.diagnostics.force_refresh_document, namespace.document)
95
+
96
+ @language_id("robotframework")
97
+ def _on_namespace_invalidated(self, sender: Any, namespace: Namespace) -> None:
88
98
  if namespace.document is not None:
89
- self.parent.diagnostics.force_refresh_document(namespace.document)
99
+ # also a closed document: the workspace diagnostics loop renews its analysis, which refreshes its
100
+ # references and diagnostics
101
+ run_as_task(self.parent.diagnostics.force_refresh_document, namespace.document)
90
102
 
91
103
  @language_id("robotframework")
92
104
  def _on_get_related_documents(self, sender: Any, document: TextDocument) -> Optional[List[TextDocument]]:
@@ -38,8 +38,12 @@ class DocumentsCachePart(RobotLanguageServerProtocolPart, DocumentsCacheHelper):
38
38
  )
39
39
 
40
40
  def calc_cache_path(self, folder_uri: Uri) -> Path:
41
- cache_config = self.parent.workspace.get_configuration(CacheConfig, folder_uri)
42
41
  cache_base_path = folder_uri.to_path()
42
+ # `cache-dir` of the robot.toml wins over the location the editor asks for.
43
+ if self.analysis_config.cache_dir:
44
+ return cache_base_path / self.analysis_config.cache_dir
45
+
46
+ cache_config = self.parent.workspace.get_configuration(CacheConfig, folder_uri)
43
47
  if (
44
48
  cache_config.save_location == CacheSaveLocation.WORKSPACE_STORAGE
45
49
  and isinstance(self.parent.initialization_options, dict)
@@ -12,9 +12,10 @@ from typing import (
12
12
  cast,
13
13
  )
14
14
 
15
+ from robot.model.metadata import Metadata as RobotMetadata
15
16
  from robot.parsing.lexer.tokens import Token
16
17
  from robot.parsing.model.blocks import TestCase, TestCaseSection
17
- from robot.parsing.model.statements import Documentation, Tags
18
+ from robot.parsing.model.statements import Documentation, Metadata, Tags
18
19
 
19
20
  from robotcode.core.concurrent import check_current_task_canceled
20
21
  from robotcode.core.language import language_id
@@ -27,15 +28,24 @@ from robotcode.core.lsp.types import (
27
28
  )
28
29
  from robotcode.core.text_document import TextDocument
29
30
  from robotcode.core.utils.logging import TRACE, LoggingDescriptor
30
- from robotcode.robot.diagnostics.entities import VariableDefinitionType
31
- from robotcode.robot.diagnostics.model_helper import ModelHelper
31
+ from robotcode.robot.diagnostics.entities import (
32
+ LibraryEntry,
33
+ ResourceEntry,
34
+ VariableDefinitionType,
35
+ VariablesEntry,
36
+ )
37
+ from robotcode.robot.diagnostics.library_doc import KeywordDoc
38
+ from robotcode.robot.diagnostics.namespace import Namespace
32
39
  from robotcode.robot.utils.ast import (
33
40
  get_nodes_at_position,
34
41
  range_from_node,
35
42
  range_from_token,
36
43
  )
44
+ from robotcode.robot.utils.markdown_docs import LinkResolver, replace_anchor_links
37
45
  from robotcode.robot.utils.markdownformatter import MarkDownFormatter
46
+ from robotcode.robot.utils.variables import contains_variable
38
47
 
48
+ from .code_action_documentation import DocumentationTarget, keyword_reference_targets, link_documentation
39
49
  from .protocol_part import RobotLanguageServerProtocolPart
40
50
 
41
51
  if TYPE_CHECKING:
@@ -54,7 +64,7 @@ class MyRepr(reprlib.Repr):
54
64
  _my_repr = MyRepr()
55
65
 
56
66
 
57
- class RobotHoverProtocolPart(RobotLanguageServerProtocolPart, ModelHelper):
67
+ class RobotHoverProtocolPart(RobotLanguageServerProtocolPart):
58
68
  _logger = LoggingDescriptor()
59
69
 
60
70
  def __init__(self, parent: "RobotLanguageServerProtocol") -> None:
@@ -176,7 +186,7 @@ class RobotHoverProtocolPart(RobotLanguageServerProtocolPart, ModelHelper):
176
186
 
177
187
  all_kw_refs = namespace.keyword_references
178
188
  if all_kw_refs:
179
- result: List[Tuple[Range, str]] = []
189
+ result: List[Tuple[Range, KeywordDoc]] = []
180
190
 
181
191
  for kw, kw_refs in all_kw_refs.items():
182
192
  check_current_task_canceled()
@@ -191,23 +201,20 @@ class RobotHoverProtocolPart(RobotLanguageServerProtocolPart, ModelHelper):
191
201
  )
192
202
 
193
203
  if found_range is not None:
194
- if kw.libtype == "RESOURCE":
195
- txt = kw.to_markdown(
196
- modify_doc_handler=lambda t: namespace.imports_manager.replace_variables_scalar(
197
- t,
198
- str(document.uri.to_path().parent),
199
- namespace.get_resolvable_variables(),
200
- ignore_errors=True,
201
- )
202
- )
203
- else:
204
- txt = kw.to_markdown()
205
-
206
- result.append((found_range, txt))
204
+ result.append((found_range, kw))
207
205
  if result:
208
206
  r = result[0][0]
209
207
  if all(r == i[0] for i in result):
210
- doc = "\n\n---\n\n".join(i[1] for i in result)
208
+ links = self.parent.robot_initialization_options.documentation_viewer_links
209
+ # a call that matches several keywords has no target
210
+ target = (
211
+ self.parent.robot_code_action_documentation.target_at(document, Range(position, position))
212
+ if links and len(result) == 1
213
+ else None
214
+ )
215
+ doc = "\n\n---\n\n".join(
216
+ self._keyword_markdown(i[1], document, namespace, links, target) for i in result
217
+ )
211
218
 
212
219
  return Hover(
213
220
  contents=MarkupContent(kind=MarkupKind.MARKDOWN, value=doc),
@@ -236,13 +243,73 @@ class RobotHoverProtocolPart(RobotLanguageServerProtocolPart, ModelHelper):
236
243
  return Hover(
237
244
  contents=MarkupContent(
238
245
  kind=MarkupKind.MARKDOWN,
239
- value=ns.library_doc.to_markdown(),
246
+ value=self._namespace_markdown(ns, document, namespace, position),
240
247
  ),
241
248
  range=found_range,
242
249
  )
243
250
 
244
251
  return None
245
252
 
253
+ def _keyword_markdown(
254
+ self,
255
+ kw: KeywordDoc,
256
+ document: TextDocument,
257
+ namespace: Namespace,
258
+ links: bool,
259
+ target: Optional[DocumentationTarget],
260
+ ) -> str:
261
+ modify_doc_handler: Optional[Callable[[str], str]] = None
262
+ if kw.libtype == "RESOURCE":
263
+
264
+ def modify_doc_handler(t: str) -> str:
265
+ # without a variable, the replacement only doubles each backslash, which breaks escaped link texts
266
+ if links and not contains_variable(t, "$@&%"):
267
+ return t
268
+ return cast(
269
+ str,
270
+ namespace.imports_manager.replace_variables_scalar(
271
+ t,
272
+ str(document.uri.to_path().parent),
273
+ namespace.get_resolvable_variables(),
274
+ ignore_errors=True,
275
+ ),
276
+ )
277
+
278
+ def render(link_resolver: Optional[LinkResolver]) -> str:
279
+ return kw.to_markdown(
280
+ modify_doc_handler=modify_doc_handler,
281
+ link_resolver=link_resolver,
282
+ reference_targets=keyword_reference_targets(kw, link_resolver is not None),
283
+ )
284
+
285
+ if not links:
286
+ return render(None)
287
+ link_resolver = (
288
+ self.parent.robot_code_action_documentation.link_resolver(document, namespace, target, kw.parent)
289
+ if target is not None and kw.parent is not None
290
+ else None
291
+ )
292
+ return link_documentation(render, target, kw.parent, link_resolver=link_resolver)
293
+
294
+ def _namespace_markdown(
295
+ self, ns: LibraryEntry, document: TextDocument, namespace: Namespace, position: Position
296
+ ) -> str:
297
+ if not self.parent.robot_initialization_options.documentation_viewer_links:
298
+ return ns.library_doc.to_markdown()
299
+
300
+ target: Optional[DocumentationTarget] = None
301
+ if not isinstance(ns, VariablesEntry):
302
+ documentation = self.parent.robot_code_action_documentation
303
+ # on an import name the target of the source actions, which knows the import's own
304
+ # arguments; on an alias or a prefix the page of the hovered import
305
+ target = documentation.target_at(document, Range(position, position))
306
+ if target is None or target.keyword is not None:
307
+ target = documentation.entry_target(ns, not isinstance(ns, ResourceEntry), document, namespace)
308
+
309
+ return link_documentation(
310
+ lambda link_resolver: ns.library_doc.to_markdown(link_resolver=link_resolver), target, ns.library_doc
311
+ )
312
+
246
313
  def hover_TestCase( # noqa: N802
247
314
  self,
248
315
  node: ast.AST,
@@ -262,6 +329,10 @@ class RobotHoverProtocolPart(RobotLanguageServerProtocolPart, ModelHelper):
262
329
 
263
330
  doc = next((e for e in test_case.body if isinstance(e, Documentation)), None)
264
331
  tags = next((e for e in test_case.body if isinstance(e, Tags)), None)
332
+ # Robot Framework's own mapping: names are case, space and underscore insensitive, the
333
+ # first spelling is kept, the last value wins and the entries are sorted by name.
334
+ # A `[Metadata]` setting with nothing after it is no metadata.
335
+ metadata = RobotMetadata((e.name, e.value) for e in test_case.body if isinstance(e, Metadata) and e.name)
265
336
 
266
337
  section = next((e for e in nodes if isinstance(e, TestCaseSection)), None)
267
338
  if section is not None and section.tasks:
@@ -277,13 +348,24 @@ class RobotHoverProtocolPart(RobotLanguageServerProtocolPart, ModelHelper):
277
348
  txt += "\n*Tags*: "
278
349
  txt += f"{', '.join(tags.values)}\n"
279
350
 
351
+ if metadata:
352
+ txt += "\n*Metadata*:\n"
353
+ for name, value in metadata.items():
354
+ # list items keep one line per entry, indented lines continue a multi-line value
355
+ value = value.replace("\n", "\n ")
356
+ txt += f"- {name}: {value}\n"
357
+
280
358
  txt = namespace.imports_manager.replace_variables_scalar(
281
359
  txt,
282
360
  str(document.uri.to_path().parent),
283
361
  namespace.get_resolvable_variables(),
284
362
  ignore_errors=True,
285
363
  )
364
+ txt = MarkDownFormatter().format(txt)
365
+ if self.parent.robot_initialization_options.documentation_viewer_links:
366
+ # a test case has no page, links to a place in its documentation would open nothing
367
+ txt = replace_anchor_links(txt, lambda anchor: None)
286
368
  return Hover(
287
- contents=MarkupContent(kind=MarkupKind.MARKDOWN, value=MarkDownFormatter().format(txt)),
369
+ contents=MarkupContent(kind=MarkupKind.MARKDOWN, value=txt),
288
370
  range=range_from_token(name_token),
289
371
  )
@@ -16,10 +16,11 @@ from urllib.parse import parse_qs, urlparse
16
16
  from robotcode.core.utils.logging import LoggingDescriptor
17
17
  from robotcode.core.utils.net import find_free_port
18
18
  from robotcode.robot.diagnostics.library_doc import (
19
+ LibraryDoc,
19
20
  get_library_doc,
20
21
  get_robot_library_html_doc_str,
21
22
  )
22
- from robotcode.robot.diagnostics.model_helper import ModelHelper
23
+ from robotcode.robot.utils.markdown_docs import anchor_link_resolver
23
24
 
24
25
  from ..configuration import DocumentationServerConfig
25
26
  from .protocol_part import RobotLanguageServerProtocolPart
@@ -72,6 +73,19 @@ MARKDOWN_TEMPLATE = Template(
72
73
  )
73
74
 
74
75
 
76
+ def library_doc_to_markdown_content(libdoc: LibraryDoc) -> str:
77
+ """The Markdown the documentation view renders for a library.
78
+
79
+ The page shows the whole library, so references to keywords and sections
80
+ in Markdown documentation become links to their headings.
81
+ """
82
+ tt = str.maketrans({"<": "&lt;", ">": "&gt;"})
83
+ markdown = libdoc.to_markdown(
84
+ add_signature=False, only_doc=False, header_level=0, link_resolver=anchor_link_resolver
85
+ )
86
+ return markdown.translate(tt)
87
+
88
+
75
89
  class LibDocRequestHandler(SimpleHTTPRequestHandler):
76
90
  _logger = LoggingDescriptor()
77
91
 
@@ -106,11 +120,7 @@ class LibDocRequestHandler(SimpleHTTPRequestHandler):
106
120
  base_dir=basedir if basedir else ".",
107
121
  )
108
122
 
109
- def calc_md() -> str:
110
- tt = str.maketrans({"<": "&lt;", ">": "&gt;"})
111
- return libdoc.to_markdown(add_signature=False, only_doc=False, header_level=0).translate(tt)
112
-
113
- data = MARKDOWN_TEMPLATE.substitute(content=calc_md(), name=name)
123
+ data = MARKDOWN_TEMPLATE.substitute(content=library_doc_to_markdown_content(libdoc), name=name)
114
124
 
115
125
  self.send_response(200)
116
126
  self.send_header("Content-type", "text/html")
@@ -162,7 +172,7 @@ class DualStackServer(ThreadingHTTPServer):
162
172
  return super().server_bind()
163
173
 
164
174
 
165
- class HttpServerProtocolPart(RobotLanguageServerProtocolPart, ModelHelper):
175
+ class HttpServerProtocolPart(RobotLanguageServerProtocolPart):
166
176
  _logger = LoggingDescriptor()
167
177
 
168
178
  def __init__(self, parent: "RobotLanguageServerProtocol") -> None:
@@ -1,5 +1,5 @@
1
1
  import ast
2
- from itertools import dropwhile, takewhile
2
+ from itertools import chain, dropwhile, takewhile
3
3
  from typing import TYPE_CHECKING, Any, Iterator, List, Optional, Tuple
4
4
 
5
5
  from robot.parsing.lexer.tokens import Token
@@ -10,24 +10,33 @@ from robotcode.core.lsp.types import (
10
10
  InlineValue,
11
11
  InlineValueContext,
12
12
  InlineValueEvaluatableExpression,
13
+ Position,
13
14
  Range,
14
15
  )
15
16
  from robotcode.core.text_document import TextDocument
16
17
  from robotcode.core.utils.logging import LoggingDescriptor
17
18
  from robotcode.robot.diagnostics.model_helper import ModelHelper
19
+ from robotcode.robot.diagnostics.semantic_analyzer.enums import NodeKind
20
+ from robotcode.robot.diagnostics.semantic_analyzer.model import SemanticModel
21
+ from robotcode.robot.diagnostics.semantic_analyzer.nodes import SemanticToken
18
22
  from robotcode.robot.utils.ast import (
19
23
  iter_nodes,
20
24
  range_from_node,
21
25
  range_from_token,
22
26
  )
23
27
 
28
+ from .model_variables import (
29
+ iter_condition_ref_candidates,
30
+ iter_model_variable_candidates,
31
+ scannable_statement_tokens,
32
+ )
24
33
  from .protocol_part import RobotLanguageServerProtocolPart
25
34
 
26
35
  if TYPE_CHECKING:
27
36
  from ..protocol import RobotLanguageServerProtocol
28
37
 
29
38
 
30
- class RobotInlineValueProtocolPart(RobotLanguageServerProtocolPart, ModelHelper):
39
+ class RobotInlineValueProtocolPart(RobotLanguageServerProtocolPart):
31
40
  _logger = LoggingDescriptor()
32
41
 
33
42
  def __init__(self, parent: "RobotLanguageServerProtocol") -> None:
@@ -50,10 +59,14 @@ class RobotInlineValueProtocolPart(RobotLanguageServerProtocolPart, ModelHelper)
50
59
 
51
60
  namespace = self.parent.documents_cache.get_namespace(document)
52
61
 
53
- model = self.parent.documents_cache.get_model(document)
54
-
55
62
  real_range = Range(range.start, min(range.end, context.stopped_location.end))
56
63
 
64
+ semantic_model = namespace.semantic_model
65
+ if semantic_model is not None:
66
+ return self._collect_from_model(semantic_model, real_range, context)
67
+
68
+ model = self.parent.documents_cache.get_model(document)
69
+
57
70
  def get_tokens() -> Iterator[Tuple[Token, ast.AST]]:
58
71
  for n in iter_nodes(model):
59
72
  r = range_from_node(n)
@@ -71,15 +84,64 @@ class RobotInlineValueProtocolPart(RobotLanguageServerProtocolPart, ModelHelper)
71
84
  get_tokens(),
72
85
  ),
73
86
  ):
74
- if token.type == RobotToken.ARGUMENT and isinstance(node, self.get_expression_statement_types()):
75
- for t, var in self.iter_expression_variables_from_token(
87
+ if token.type == RobotToken.ARGUMENT and isinstance(node, ModelHelper.get_expression_statement_types()):
88
+ for t, var in ModelHelper.iter_expression_variables_from_token(
76
89
  token, namespace, context.stopped_location.start
77
90
  ):
78
91
  if var.name != "${CURDIR}":
79
92
  result.append(InlineValueEvaluatableExpression(range_from_token(t), var.name))
80
93
 
81
- for t, var in self.iter_variables_from_token(token, namespace, context.stopped_location.start):
94
+ for t, var in ModelHelper.iter_variables_from_token(token, namespace, context.stopped_location.start):
82
95
  if var.name != "${CURDIR}":
83
96
  result.append(InlineValueEvaluatableExpression(range_from_token(t), var.name))
84
97
 
85
98
  return result
99
+
100
+ def _collect_from_model(
101
+ self,
102
+ semantic_model: SemanticModel,
103
+ real_range: Range,
104
+ context: InlineValueContext,
105
+ ) -> List[InlineValue]:
106
+ """Model path: variable occurrences from the pre-built statement
107
+ tokens, resolved at the debugger's stopped location."""
108
+ resolve_line = context.stopped_location.start.line + 1
109
+ resolve_col = context.stopped_location.start.character
110
+
111
+ def get_model_tokens() -> Iterator[Tuple[NodeKind, SemanticToken]]:
112
+ for stmt in semantic_model.statements:
113
+ if not stmt.tokens:
114
+ continue
115
+ first = stmt.tokens[0]
116
+ last = stmt.tokens[-1]
117
+ start = Position(line=first.line - 1, character=first.col_offset)
118
+ end = Position(line=last.line - 1, character=last.col_offset + last.length)
119
+ if start in real_range or end in real_range:
120
+ for token in scannable_statement_tokens(stmt):
121
+ yield stmt.kind, token
122
+ if start > real_range.end:
123
+ break
124
+
125
+ result: List[InlineValue] = []
126
+ for stmt_kind, token in takewhile(
127
+ lambda t: t[1].line - 1 <= real_range.end.line,
128
+ dropwhile(
129
+ lambda t: Position(line=t[1].line - 1, character=t[1].col_offset) < real_range.start,
130
+ get_model_tokens(),
131
+ ),
132
+ ):
133
+ # Legacy per-token order: the condition cell's bare-`$var`
134
+ # expression refs first, then the regular variables.
135
+ for candidate in chain(
136
+ iter_condition_ref_candidates(token),
137
+ iter_model_variable_candidates(token, semantic_model, resolve_line, resolve_col, stmt_kind),
138
+ ):
139
+ if not candidate.primary:
140
+ continue
141
+ # `extended=False`: the extended-syntax fallback is already
142
+ # encoded in the candidates' lookup names.
143
+ var = semantic_model.find_variable(candidate.lookup_name, resolve_line, resolve_col, extended=False)
144
+ if var is not None and var.name != "${CURDIR}":
145
+ result.append(InlineValueEvaluatableExpression(candidate.range, var.name))
146
+
147
+ return result