Skip to content

Commit 11e7b4a

Browse files
committed
implement InlineValuesProvider and EvaluatableExpressionProvider in language server
1 parent 2655cad commit 11e7b4a

File tree

11 files changed

+400
-94
lines changed

11 files changed

+400
-94
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ All notable changes to the "robotcode" extension will be documented in this file
44

55
## [Unreleased]
66

7+
### added
8+
9+
- implement InlineValuesProvider and EvaluatableExpressionProvider in language server
10+
711
## 0.4.3
812

913
### added

robotcode/debugger/debugger.py

+3-9
Original file line numberDiff line numberDiff line change
@@ -840,10 +840,7 @@ def evaluate(
840840
if evaluate_context is None:
841841
evaluate_context = EXECUTION_CONTEXTS.current
842842

843-
if EvaluateArgumentContext(context) in [EvaluateArgumentContext.HOVER]:
844-
expression = f"${expression}"
845-
846-
result: Optional[str] = None
843+
result: Any = None
847844
try:
848845

849846
vars = evaluate_context.variables.current if frame_id is not None else evaluate_context.variables._global
@@ -859,11 +856,8 @@ def evaluate(
859856
else:
860857
result = evaluate_expression(vars.replace_string(expression), vars.store)
861858

862-
except BaseException as e:
863-
if EvaluateArgumentContext(context) in [EvaluateArgumentContext.HOVER]:
864-
return EvaluateResult("")
865-
else:
866-
result = str(e)
859+
except BaseException as e: # NOSONAR
860+
result = e
867861

868862
if result is not None:
869863
return EvaluateResult(repr(result), repr(type(result)))

robotcode/language_server/common/lsp_types.py

+8-5
Original file line numberDiff line numberDiff line change
@@ -1006,7 +1006,7 @@ def __ge__(self, other: "Position") -> bool:
10061006
line_gt = self.line > other.line
10071007

10081008
if line_gt:
1009-
return line_gt
1009+
return True
10101010

10111011
if self.line == other.line:
10121012
return self.character >= other.character
@@ -1017,7 +1017,7 @@ def __gt__(self, other: "Position") -> bool:
10171017
line_gt = self.line > other.line
10181018

10191019
if line_gt:
1020-
return line_gt
1020+
return True
10211021

10221022
if self.line == other.line:
10231023
return self.character > other.character
@@ -1028,7 +1028,7 @@ def __le__(self, other: "Position") -> bool:
10281028
line_lt = self.line < other.line
10291029

10301030
if line_lt:
1031-
return line_lt
1031+
return True
10321032

10331033
if self.line == other.line:
10341034
return self.character <= other.character
@@ -1039,7 +1039,7 @@ def __lt__(self, other: "Position") -> bool:
10391039
line_lt = self.line < other.line
10401040

10411041
if line_lt:
1042-
return line_lt
1042+
return True
10431043

10441044
if self.line == other.line:
10451045
return self.character < other.character
@@ -1084,10 +1084,13 @@ def extend(self, start_line: int = 0, start_character: int = 0, end_line: int =
10841084
)
10851085

10861086
def __contains__(self, x: object) -> bool:
1087-
if isinstance(x, Position):
1087+
if isinstance(x, (Position, Range)):
10881088
return x.is_in_range(self)
10891089
return False
10901090

1091+
def is_in_range(self, range: Range) -> bool:
1092+
return range.start.is_in_range(self) and range.end.is_in_range(self)
1093+
10911094

10921095
@dataclass(repr=False)
10931096
class TextDocumentItem(Model):

robotcode/language_server/robotframework/diagnostics/namespace.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -642,13 +642,14 @@ async def find_variable(
642642

643643
match = self._match_extended.match(name[2:-1])
644644
if match is not None:
645-
base_name, extended = match.groups()
645+
base_name, _ = match.groups()
646646
name = f"{name[0]}{{{base_name}}}"
647647

648648
matcher = VariableMatcher(name)
649649
async for m, v in self.yield_variables(nodes, position):
650650
if matcher == m:
651651
return v
652+
652653
return None
653654

654655
@_logger.call
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
from __future__ import annotations
2+
3+
import re
4+
from dataclasses import dataclass
5+
from typing import TYPE_CHECKING, Any, Generator, List, Literal, Optional, Union
6+
7+
from ....jsonrpc2.protocol import rpc_method
8+
from ....utils.async_itertools import async_dropwhile, async_takewhile
9+
from ....utils.async_tools import run_coroutine_in_thread
10+
from ....utils.logging import LoggingDescriptor
11+
from ...common.lsp_types import Model, Position, Range, TextDocumentIdentifier
12+
from ..utils.ast import (
13+
HasTokens,
14+
Token,
15+
get_nodes_at_position,
16+
get_tokens_at_position,
17+
range_from_token,
18+
tokenize_variables,
19+
)
20+
21+
if TYPE_CHECKING:
22+
from ..protocol import RobotLanguageServerProtocol
23+
24+
from .model_helper import ModelHelperMixin
25+
from .protocol_part import RobotLanguageServerProtocolPart
26+
27+
28+
@dataclass(repr=False)
29+
class EvaluatableExpressionParams(Model):
30+
text_document: TextDocumentIdentifier
31+
position: Position
32+
33+
34+
@dataclass(repr=False)
35+
class EvaluatableExpression(Model):
36+
range: Range
37+
expression: Optional[str]
38+
39+
40+
@dataclass(repr=False)
41+
class InlineValueContext(Model):
42+
frame_id: int
43+
stopped_location: Range
44+
45+
46+
@dataclass(repr=False)
47+
class InlineValuesParams(Model):
48+
text_document: TextDocumentIdentifier
49+
view_port: Range
50+
context: InlineValueContext
51+
52+
53+
@dataclass(repr=False)
54+
class InlineValueText(Model):
55+
range: Range
56+
text: str
57+
type: Literal["text"] = "text"
58+
59+
60+
@dataclass(repr=False)
61+
class InlineValueVariableLookup(Model):
62+
range: Range
63+
variable_name: Optional[str]
64+
case_sensitive_lookup: bool
65+
type: Literal["variable"] = "variable"
66+
67+
68+
@dataclass(repr=False)
69+
class InlineValueEvaluatableExpression(Model):
70+
range: Range
71+
expression: Optional[str]
72+
type: Literal["expression"] = "expression"
73+
74+
75+
InlineValue = Union[InlineValueText, InlineValueVariableLookup, InlineValueEvaluatableExpression]
76+
77+
78+
class RobotDebuggingUtilsProtocolPart(RobotLanguageServerProtocolPart, ModelHelperMixin):
79+
_logger = LoggingDescriptor()
80+
81+
def __init__(self, parent: RobotLanguageServerProtocol) -> None:
82+
super().__init__(parent)
83+
84+
_match_extended = re.compile(
85+
r"""
86+
(.+?) # base name (group 1)
87+
([^\s\w].+) # extended part (group 2)
88+
""",
89+
re.UNICODE | re.VERBOSE,
90+
)
91+
92+
@staticmethod
93+
def _get_all_variable_token_from_token(token: Token) -> Generator[Token, Any, Any]:
94+
from robot.api.parsing import Token as RobotToken
95+
from robot.variables.search import contains_variable
96+
97+
def iter_all_variables_from_token(to: Token, ignore_errors: bool = False) -> Generator[Token, Any, Any]:
98+
99+
for sub_token in tokenize_variables(to, ignore_errors=ignore_errors):
100+
if sub_token.type == RobotToken.VARIABLE:
101+
yield sub_token
102+
sub_text = sub_token.value[2:-1]
103+
104+
if contains_variable(sub_text):
105+
for j in iter_all_variables_from_token(
106+
RobotToken(token.type, sub_text, to.lineno, to.col_offset + 2),
107+
ignore_errors=ignore_errors,
108+
):
109+
if j.type == RobotToken.VARIABLE:
110+
yield j
111+
112+
for e in iter_all_variables_from_token(token, ignore_errors=True):
113+
name = e.value
114+
match = RobotDebuggingUtilsProtocolPart._match_extended.match(name[2:-1])
115+
if match is not None:
116+
base_name, _ = match.groups()
117+
name = f"{name[0]}{{{base_name}}}"
118+
yield RobotToken(e.type, name, e.lineno, e.col_offset)
119+
120+
@rpc_method(name="robot/debugging/getEvaluatableExpression", param_type=EvaluatableExpressionParams)
121+
@_logger.call
122+
async def _get_evaluatable_expression(
123+
self,
124+
text_document: TextDocumentIdentifier,
125+
position: Position,
126+
*args: Any,
127+
**kwargs: Any,
128+
) -> Optional[EvaluatableExpression]:
129+
async def run() -> Optional[EvaluatableExpression]:
130+
from robot.api import Token as RobotToken
131+
132+
document = await self.parent.documents.get(text_document.uri)
133+
if document is None:
134+
return None
135+
136+
model = await self.parent.documents_cache.get_model(document)
137+
138+
node = (await get_nodes_at_position(model, position))[-1]
139+
140+
if not isinstance(node, HasTokens):
141+
return None
142+
143+
token = get_tokens_at_position(node, position)[-1]
144+
145+
sub_token = next(
146+
(
147+
t
148+
for t in RobotDebuggingUtilsProtocolPart._get_all_variable_token_from_token(token)
149+
if t.type == RobotToken.VARIABLE and position in range_from_token(t)
150+
),
151+
None,
152+
)
153+
154+
if sub_token is None or sub_token.value is None or "${CURDIR}" == sub_token.value.upper():
155+
return None
156+
157+
return EvaluatableExpression(range_from_token(sub_token), sub_token.value)
158+
159+
return await run_coroutine_in_thread(run)
160+
161+
@rpc_method(name="robot/debugging/getInlineValues", param_type=InlineValuesParams)
162+
@_logger.call
163+
async def _get_inline_values(
164+
self,
165+
text_document: TextDocumentIdentifier,
166+
view_port: Range,
167+
context: InlineValueContext,
168+
*args: Any,
169+
**kwargs: Any,
170+
) -> List[InlineValue]:
171+
async def run() -> List[InlineValue]:
172+
from robot.api import Token as RobotToken
173+
174+
document = await self.parent.documents.get(text_document.uri)
175+
if document is None:
176+
return []
177+
178+
namespace = await self.parent.documents_cache.get_namespace(document)
179+
if namespace is None:
180+
return []
181+
182+
tokens = await self.parent.documents_cache.get_tokens(document)
183+
184+
real_range = Range(view_port.start, min(view_port.end, context.stopped_location.end))
185+
186+
result: List[InlineValue] = []
187+
async for token in async_takewhile(
188+
lambda t: range_from_token(t).end.line <= real_range.end.line,
189+
async_dropwhile(
190+
lambda t: range_from_token(t).start < real_range.start,
191+
tokens,
192+
),
193+
):
194+
added: List[str] = []
195+
for t in RobotDebuggingUtilsProtocolPart._get_all_variable_token_from_token(token):
196+
if t.type == RobotToken.VARIABLE:
197+
upper_value = t.value.upper()
198+
if upper_value not in added:
199+
result.append(InlineValueEvaluatableExpression(range_from_token(t), t.value))
200+
added.append(upper_value)
201+
202+
return result
203+
204+
return await run_coroutine_in_thread(run)

robotcode/language_server/robotframework/parts/discovering.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -24,24 +24,24 @@
2424
from ..protocol import RobotLanguageServerProtocol
2525

2626

27-
@dataclass
27+
@dataclass(repr=False)
2828
class GetAllTestsParams(Model):
2929
workspace_folder: str
3030
paths: Optional[List[str]]
3131

3232

33-
@dataclass
33+
@dataclass(repr=False)
3434
class GetTestsParams(Model):
3535
text_document: TextDocumentIdentifier
3636
id: Optional[str]
3737

3838

39-
@dataclass
39+
@dataclass(repr=False)
4040
class GetTestsFromDocumentParams(Model):
4141
text_document: TextDocumentIdentifier
4242

4343

44-
@dataclass
44+
@dataclass(repr=False)
4545
class TestItem(Model):
4646
type: str
4747
id: str
@@ -196,7 +196,7 @@ def nonexisting_paths(paths: List[str]) -> Iterator[str]:
196196
return [TestItem(type="error", id=Path.cwd().name, label=Path.cwd().name, error=str(e))]
197197

198198
@rpc_method(name="robot/discovering/getTestsFromWorkspace", param_type=GetAllTestsParams)
199-
@_logger.call(entering=True, exiting=True, exception=True)
199+
@_logger.call
200200
async def get_tests_from_workspace(
201201
self,
202202
workspace_folder: str,
@@ -207,7 +207,7 @@ async def get_tests_from_workspace(
207207
return await run_in_thread(self._get_tests_from_workspace, Uri(workspace_folder).to_path(), paths)
208208

209209
@rpc_method(name="robot/discovering/getTestsFromDocument", param_type=GetTestsParams)
210-
@_logger.call(entering=True, exiting=True, exception=True)
210+
@_logger.call
211211
async def get_tests_from_document(
212212
self, text_document: TextDocumentIdentifier, id: Optional[str], *args: Any, **kwargs: Any
213213
) -> List[TestItem]:

0 commit comments

Comments
 (0)