robotcode-language-server 2.6.2__py3-none-any.whl → 2.8.0.dev548__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- robotcode/language_server/__version__.py +1 -1
- robotcode/language_server/common/parts/diagnostics.py +50 -17
- robotcode/language_server/common/parts/documents.py +34 -31
- robotcode/language_server/common/parts/workspace.py +57 -55
- robotcode/language_server/common/protocol.py +3 -1
- robotcode/language_server/robotframework/configuration.py +3 -0
- robotcode/language_server/robotframework/parts/code_action_documentation.py +440 -129
- robotcode/language_server/robotframework/parts/code_action_helper_mixin.py +17 -2
- robotcode/language_server/robotframework/parts/code_action_quick_fixes.py +6 -25
- robotcode/language_server/robotframework/parts/code_action_refactor.py +8 -6
- robotcode/language_server/robotframework/parts/code_lens.py +3 -4
- robotcode/language_server/robotframework/parts/completion.py +193 -21
- robotcode/language_server/robotframework/parts/debugging_utils.py +75 -6
- robotcode/language_server/robotframework/parts/diagnostics.py +15 -3
- robotcode/language_server/robotframework/parts/documents_cache.py +5 -1
- robotcode/language_server/robotframework/parts/hover.py +103 -21
- robotcode/language_server/robotframework/parts/http_server.py +17 -7
- robotcode/language_server/robotframework/parts/inline_value.py +69 -7
- robotcode/language_server/robotframework/parts/keywords_treeview.py +151 -53
- robotcode/language_server/robotframework/parts/model_variables.py +331 -0
- robotcode/language_server/robotframework/parts/references.py +47 -31
- robotcode/language_server/robotframework/parts/rename.py +1 -2
- robotcode/language_server/robotframework/parts/robot_workspace.py +229 -25
- robotcode/language_server/robotframework/parts/selection_range.py +26 -13
- robotcode/language_server/robotframework/parts/semantic_tokens.py +414 -113
- robotcode/language_server/robotframework/parts/signature_help.py +162 -70
- robotcode/language_server/robotframework/protocol.py +1 -0
- {robotcode_language_server-2.6.2.dist-info → robotcode_language_server-2.8.0.dev548.dist-info}/METADATA +7 -6
- {robotcode_language_server-2.6.2.dist-info → robotcode_language_server-2.8.0.dev548.dist-info}/RECORD +31 -30
- {robotcode_language_server-2.6.2.dist-info → robotcode_language_server-2.8.0.dev548.dist-info}/WHEEL +1 -1
- {robotcode_language_server-2.6.2.dist-info → robotcode_language_server-2.8.0.dev548.dist-info}/entry_points.txt +0 -0
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
31
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
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=
|
|
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.
|
|
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({"<": "<", ">": ">"})
|
|
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
|
-
|
|
110
|
-
tt = str.maketrans({"<": "<", ">": ">"})
|
|
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
|
|
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
|
|
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,
|
|
75
|
-
for t, var in
|
|
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
|
|
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
|