Skip to content

Commit aa5c1a8

Browse files
committed
implement hover and goto for "FOR" variables
1 parent b01b9fc commit aa5c1a8

File tree

5 files changed

+82
-10
lines changed

5 files changed

+82
-10
lines changed

robotcode/language_server/robotframework/diagnostics/namespace.py

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from ..utils.ast import (
3939
Token,
4040
is_non_variable_token,
41+
range_from_node,
4142
range_from_token_or_node,
4243
tokenize_variables,
4344
)
@@ -219,12 +220,20 @@ async def visit_Variable(self, node: ast.AST) -> None: # noqa: N802
219220

220221

221222
class BlockVariableVisitor(AsyncVisitor):
222-
async def get(self, source: str, model: ast.AST) -> List[VariableDefinition]:
223-
self._results: List[VariableDefinition] = []
223+
async def get(self, source: str, model: ast.AST, position: Optional[Position] = None) -> List[VariableDefinition]:
224224
self.source = source
225+
self.position = position
226+
227+
self._results: List[VariableDefinition] = []
228+
225229
await self.visit(model)
230+
226231
return self._results
227232

233+
async def visit(self, node: ast.AST) -> None:
234+
if self.position is None or self.position >= range_from_node(node).start:
235+
return await super().visit(node)
236+
228237
async def visit_KeywordName(self, node: ast.AST) -> None: # noqa: N802
229238
from robot.parsing.lexer.tokens import Token as RobotToken
230239
from robot.parsing.model.statements import KeywordName
@@ -343,6 +352,39 @@ async def visit_KeywordCall(self, node: ast.AST) -> None: # noqa: N802
343352
except VariableError:
344353
pass
345354

355+
async def visit_ForHeader(self, node: ast.AST) -> None: # noqa: N802
356+
from robot.errors import VariableError
357+
from robot.parsing.lexer.tokens import Token as RobotToken
358+
from robot.parsing.model.statements import ForHeader
359+
from robot.variables.search import contains_variable
360+
361+
try:
362+
n = cast(ForHeader, node)
363+
variables = n.get_tokens(RobotToken.VARIABLE)
364+
for variable in variables:
365+
if variable is not None and variable.value and contains_variable(variable.value):
366+
self._results.append(
367+
VariableDefinition(
368+
name=variable.value,
369+
name_token=variable,
370+
line_no=node.lineno,
371+
col_offset=node.col_offset,
372+
end_line_no=node.end_lineno
373+
if node.end_lineno is not None
374+
else variable.lineno
375+
if variable.lineno is not None
376+
else -1,
377+
end_col_offset=node.end_col_offset
378+
if node.end_col_offset is not None
379+
else variable.end_col_offset
380+
if variable.end_col_offset is not None
381+
else -1,
382+
source=self.source,
383+
)
384+
)
385+
except VariableError:
386+
pass
387+
346388

347389
class ImportVisitor(AsyncVisitor):
348390
async def get(self, source: str, model: ast.AST) -> List[Import]:
@@ -953,7 +995,9 @@ def get_builtin_variables(cls) -> List[BuiltInVariableDefinition]:
953995

954996
return cls._builtin_variables
955997

956-
async def get_variables(self, nodes: Optional[List[ast.AST]] = None) -> Dict[VariableMatcher, VariableDefinition]:
998+
async def get_variables(
999+
self, nodes: Optional[List[ast.AST]] = None, position: Optional[Position] = None
1000+
) -> Dict[VariableMatcher, VariableDefinition]:
9571001
from robot.parsing.model.blocks import Keyword, TestCase
9581002

9591003
await self.ensure_initialized()
@@ -962,7 +1006,7 @@ async def get_variables(self, nodes: Optional[List[ast.AST]] = None) -> Dict[Var
9621006

9631007
async for var in async_chain(
9641008
*[
965-
await BlockVariableVisitor().get(self.source, n)
1009+
await BlockVariableVisitor().get(self.source, n, position)
9661010
for n in nodes or []
9671011
if isinstance(n, (Keyword, TestCase))
9681012
],
@@ -975,8 +1019,10 @@ async def get_variables(self, nodes: Optional[List[ast.AST]] = None) -> Dict[Var
9751019

9761020
return result
9771021

978-
async def find_variable(self, name: str, nodes: Optional[List[ast.AST]]) -> Optional[VariableDefinition]:
979-
return (await self.get_variables(nodes)).get(VariableMatcher(name), None)
1022+
async def find_variable(
1023+
self, name: str, nodes: Optional[List[ast.AST]], position: Optional[Position] = None
1024+
) -> Optional[VariableDefinition]:
1025+
return (await self.get_variables(nodes, position)).get(VariableMatcher(name), None)
9801026

9811027
async def _import_imports(self, imports: Iterable[Import], base_dir: str, *, top_level: bool = False) -> None:
9821028
async def _import(value: Import) -> Optional[LibraryEntry]:

robotcode/language_server/robotframework/parts/completion.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ async def create_variables_completion_items(
330330
),
331331
filter_text=s.name[2:-1] if range is not None else None,
332332
)
333-
for s in (await namespace.get_variables(nodes)).values()
333+
for s in (await namespace.get_variables(nodes, position)).values()
334334
if s.name is not None and (s.name_token is None or not position.is_in_range(range_from_token(s.name_token)))
335335
]
336336

robotcode/language_server/robotframework/parts/goto.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ async def _definition_default(
112112
range = range_from_token(sub_token)
113113

114114
if position.is_in_range(range):
115-
variable = await namespace.find_variable(sub_token.value, nodes)
115+
variable = await namespace.find_variable(sub_token.value, nodes, position)
116116
if variable is not None and variable.source:
117117
return [
118118
LocationLink(

robotcode/language_server/robotframework/parts/hover.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ async def _hover_default(self, nodes: List[ast.AST], document: TextDocument, pos
100100
range = range_from_token(sub_token)
101101

102102
if position.is_in_range(range):
103-
variable = await namespace.find_variable(sub_token.value, nodes)
103+
variable = await namespace.find_variable(sub_token.value, nodes, position)
104104
if variable is not None:
105105
return Hover(
106106
contents=MarkupContent(

tests/robotcode/language_server/robotframework/parts/test_hover.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,11 @@ async def test_document() -> AsyncGenerator[TextDocument, None]:
4949
data_path = Path(Path(__file__).parent, "data/hover.robot")
5050
data = data_path.read_text()
5151

52-
yield TextDocument(document_uri=data_path.as_uri(), language_id="robotframework", version=1, text=data)
52+
document = TextDocument(document_uri=data_path.as_uri(), language_id="robotframework", version=1, text=data)
53+
try:
54+
yield document
55+
finally:
56+
del document
5357

5458

5559
@pytest.mark.parametrize(
@@ -87,3 +91,25 @@ async def test_hover_should_not_find_simple_keyword_on_boundaries(
8791

8892
result = await protocol._robot_hover.collect(protocol.hover, test_document, position)
8993
assert result is None
94+
95+
96+
@pytest.mark.parametrize(
97+
("position", "variable"),
98+
[
99+
(Position(line=4, character=2), "(Variable) ${A VAR}"),
100+
(Position(line=9, character=18), "(Variable) ${A VAR}"),
101+
(Position(line=5, character=7), "(Variable) &{A DICT}"),
102+
(Position(line=10, character=36), "(Variable) &{A DICT}"),
103+
(Position(line=11, character=13), "(Variable) ${key}"), # FOR Variable
104+
(Position(line=11, character=24), "(Variable) ${value}"), # FOR Variable
105+
],
106+
)
107+
@pytest.mark.asyncio
108+
async def test_hover_should_find_variable(
109+
protocol: RobotLanguageServerProtocol, test_document: TextDocument, position: Position, variable: str
110+
) -> None:
111+
112+
result = await protocol._robot_hover.collect(protocol.hover, test_document, position)
113+
assert result
114+
assert isinstance(result.contents, MarkupContent)
115+
assert result.contents.value == variable

0 commit comments

Comments
 (0)