Skip to content

Commit 5cf050a

Browse files
committed
implement completion, hover and goto for variables
1 parent a917459 commit 5cf050a

File tree

4 files changed

+169
-31
lines changed

4 files changed

+169
-31
lines changed

robotcode/language_server/robotframework/diagnostics/library_doc.py

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,17 +121,49 @@ def __eq__(self, o: object) -> bool:
121121
return self.normalized_name == str(normalize(o, "_"))
122122

123123
def __hash__(self) -> int:
124-
return hash(self.name)
124+
return hash(self.normalized_name)
125125

126126
def __str__(self) -> str:
127127
return self.name
128128

129129
def __repr__(self) -> str:
130-
return (
131-
f"{type(self).__name__}(name={repr(self.name)}"
132-
f", normalized_name={repr(self.normalized_name)}"
133-
f"{f', embedded=True' if self.embedded_arguments else ''})"
134-
)
130+
return f"{type(self).__name__}(name={repr(self.name)})"
131+
132+
133+
class VariableMatcher:
134+
def __init__(self, name: str) -> None:
135+
from robot.utils.normalizing import normalize
136+
from robot.variables.search import VariableSearcher
137+
138+
searcher = VariableSearcher("$@&%", ignore_errors=True)
139+
match = searcher.search(name)
140+
self.name = name
141+
self.base = match.base
142+
self.normalized_name = str(normalize(match.base, "_"))
143+
144+
def __eq__(self, o: object) -> bool:
145+
from robot.utils.normalizing import normalize
146+
from robot.variables.search import VariableSearcher
147+
148+
if isinstance(o, VariableMatcher):
149+
base = o.base
150+
elif isinstance(o, str):
151+
searcher = VariableSearcher("$@&%", ignore_errors=True)
152+
match = searcher.search(o)
153+
base = match.base
154+
else:
155+
return False
156+
157+
return self.normalized_name == str(normalize(base, "_"))
158+
159+
def __hash__(self) -> int:
160+
return hash(self.normalized_name)
161+
162+
def __str__(self) -> str:
163+
return self.name
164+
165+
def __repr__(self) -> str:
166+
return f"{type(self).__name__}(name={repr(self.name)})"
135167

136168

137169
class Model(BaseModel):

robotcode/language_server/robotframework/diagnostics/namespace.py

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
KeywordDoc,
4545
KeywordMatcher,
4646
LibraryDoc,
47+
VariableMatcher,
4748
is_embedded_keyword,
4849
)
4950

@@ -145,6 +146,18 @@ class VariableDefinition(SourceEntity):
145146
def __hash__(self) -> int:
146147
return hash((type(self), self.name, self.type))
147148

149+
def range(self) -> Range:
150+
return Range(
151+
start=Position(
152+
line=self.name_token.lineno - 1 if self.name_token is not None else self.line_no - 1,
153+
character=self.name_token.col_offset if self.name_token is not None else self.col_offset,
154+
),
155+
end=Position(
156+
line=self.name_token.lineno - 1 if self.name_token is not None else self.end_line_no - 1,
157+
character=self.name_token.end_col_offset if self.name_token is not None else self.end_col_offset,
158+
),
159+
)
160+
148161

149162
@dataclass
150163
class BuiltInVariableDefinition(VariableDefinition):
@@ -184,7 +197,7 @@ async def visit_Variable(self, node: ast.AST) -> None: # noqa: N802
184197
from robot.parsing.model.statements import Variable
185198

186199
n = cast(Variable, node)
187-
name = n.get_value(Token.VARIABLE)
200+
name = n.get_token(Token.VARIABLE)
188201
if n.name:
189202
self._results.append(
190203
VariableDefinition(
@@ -723,7 +736,6 @@ def __init__(
723736
self._library_doc: Optional[LibraryDoc] = None
724737
self._imports: Optional[List[Import]] = None
725738
self._own_variables: Optional[List[VariableDefinition]] = None
726-
self._variables_definitions: Optional[Dict[str, VariableDefinition]] = None
727739
self._diagnostics: List[Diagnostic] = []
728740

729741
self._keywords: Optional[List[KeywordDoc]] = None
@@ -838,26 +850,26 @@ def get_builtin_variables(cls) -> List[BuiltInVariableDefinition]:
838850

839851
return cls._builtin_variables
840852

841-
async def get_variables(self, nodes: Optional[List[ast.AST]] = None) -> Dict[str, VariableDefinition]:
853+
async def get_variables(self, nodes: Optional[List[ast.AST]] = None) -> Dict[VariableMatcher, VariableDefinition]:
842854
from robot.parsing.model.blocks import Keyword
843855

844856
await self._ensure_initialized()
845857

846-
if self._variables_definitions is None:
847-
result: Dict[str, VariableDefinition] = {}
858+
result: Dict[VariableMatcher, VariableDefinition] = {}
848859

849-
async for var in async_chain(
850-
*[await ArgumentsVisitor().get(self.source, n) for n in nodes or [] if isinstance(n, Keyword)],
851-
(e for e in await self.get_own_variables()),
852-
*(e.variables for e in self._resources.values()),
853-
(e for e in self.get_builtin_variables()),
854-
):
855-
if var.name is not None and var.name not in result.keys():
856-
result[var.name] = var
860+
async for var in async_chain(
861+
*[await ArgumentsVisitor().get(self.source, n) for n in nodes or [] if isinstance(n, Keyword)],
862+
(e for e in await self.get_own_variables()),
863+
*(e.variables for e in self._resources.values()),
864+
(e for e in self.get_builtin_variables()),
865+
):
866+
if var.name is not None and VariableMatcher(var.name) not in result.keys():
867+
result[VariableMatcher(var.name)] = var
857868

858-
self._variables_definitions = result
869+
return result
859870

860-
return self._variables_definitions
871+
async def find_variable(self, name: str, nodes: Optional[List[ast.AST]]) -> Optional[VariableDefinition]:
872+
return (await self.get_variables(nodes)).get(VariableMatcher(name), None)
861873

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

robotcode/language_server/robotframework/parts/goto.py

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@
1919
from ...common.text_document import TextDocument
2020
from ...common.types import Location, LocationLink, Position
2121
from ..utils.ast import (
22+
HasTokens,
2223
Token,
23-
get_node_at_position,
24+
get_nodes_at_position,
25+
get_tokens_at_position,
2426
range_from_token,
2527
range_from_token_or_node,
2628
)
@@ -64,17 +66,60 @@ def _find_method(self, cls: Type[Any]) -> Optional[_DefinitionMethod]:
6466
async def collect(
6567
self, sender: Any, document: TextDocument, position: Position
6668
) -> Union[Location, List[Location], List[LocationLink], None]:
69+
result_nodes = await get_nodes_at_position(await self.parent.documents_cache.get_model(document), position)
6770

68-
result_node = await get_node_at_position(await self.parent.documents_cache.get_model(document), position)
71+
if not result_nodes:
72+
return None
73+
74+
result_node = result_nodes[-1]
6975

7076
if result_node is None:
7177
return None
7278

7379
method = self._find_method(type(result_node))
74-
if method is None:
80+
if method is not None:
81+
result = await method(result_node, document, position)
82+
if result is not None:
83+
return result
84+
85+
return await self._definition_default(result_nodes, document, position)
86+
87+
async def _definition_default(
88+
self, nodes: List[ast.AST], document: TextDocument, position: Position
89+
) -> Union[Location, List[Location], List[LocationLink], None]:
90+
namespace = await self.parent.documents_cache.get_namespace(document)
91+
if namespace is None:
7592
return None
7693

77-
return await method(result_node, document, position)
94+
if not nodes:
95+
return None
96+
97+
node = nodes[-1]
98+
99+
if not isinstance(node, HasTokens):
100+
return None
101+
102+
tokens = get_tokens_at_position(node, position)
103+
104+
for t in tokens:
105+
try:
106+
for sub_token in t.tokenize_variables():
107+
range = range_from_token(sub_token)
108+
109+
if position.is_in_range(range):
110+
variable = await namespace.find_variable(sub_token.value, nodes)
111+
if variable is not None and variable.source:
112+
return [
113+
LocationLink(
114+
origin_selection_range=range_from_token_or_node(node, sub_token),
115+
target_uri=str(Uri.from_path(variable.source)),
116+
target_range=variable.range(),
117+
target_selection_range=variable.range(),
118+
)
119+
]
120+
except BaseException:
121+
pass
122+
return None
78123

79124
async def definition_KeywordCall( # noqa: N802
80125
self, node: ast.AST, document: TextDocument, position: Position

robotcode/language_server/robotframework/parts/hover.py

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,27 @@
11
from __future__ import annotations
22

33
import ast
4-
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Optional, Type, Union, cast
4+
from typing import (
5+
TYPE_CHECKING,
6+
Any,
7+
Awaitable,
8+
Callable,
9+
List,
10+
Optional,
11+
Type,
12+
Union,
13+
cast,
14+
)
515

616
from ....utils.logging import LoggingDescriptor
717
from ...common.language import language_id
818
from ...common.text_document import TextDocument
919
from ...common.types import Hover, MarkupContent, MarkupKind, Position
1020
from ..utils.ast import (
21+
HasTokens,
1122
Token,
12-
get_node_at_position,
23+
get_nodes_at_position,
24+
get_tokens_at_position,
1325
range_from_token,
1426
range_from_token_or_node,
1527
)
@@ -43,20 +55,57 @@ def _find_method(self, cls: Type[Any]) -> Optional[_HoverMethod]:
4355
method = self._find_method(base)
4456
if method:
4557
return cast(_HoverMethod, method)
58+
4659
return None
4760

4861
@language_id("robotframework")
4962
async def collect(self, sender: Any, document: TextDocument, position: Position) -> Optional[Hover]:
50-
result_node = await get_node_at_position(await self.parent.documents_cache.get_model(document), position)
63+
result_nodes = await get_nodes_at_position(await self.parent.documents_cache.get_model(document), position)
5164

52-
if result_node is None:
65+
if not result_nodes:
5366
return None
5467

68+
result_node = result_nodes[-1]
69+
5570
method = self._find_method(type(result_node))
56-
if method is None:
71+
if method is not None:
72+
result = await method(result_node, document, position)
73+
if result is not None:
74+
return result
75+
76+
return await self._hover_default(result_nodes, document, position)
77+
78+
async def _hover_default(self, nodes: List[ast.AST], document: TextDocument, position: Position) -> Optional[Hover]:
79+
namespace = await self.parent.documents_cache.get_namespace(document)
80+
if namespace is None:
81+
return None
82+
83+
if not nodes:
5784
return None
5885

59-
return await method(result_node, document, position)
86+
node = nodes[-1]
87+
if not isinstance(node, HasTokens):
88+
return None
89+
90+
tokens = get_tokens_at_position(node, position)
91+
92+
for t in tokens:
93+
try:
94+
for sub_token in t.tokenize_variables():
95+
range = range_from_token(sub_token)
96+
97+
if position.is_in_range(range):
98+
variable = await namespace.find_variable(sub_token.value, nodes)
99+
if variable is not None:
100+
return Hover(
101+
contents=MarkupContent(
102+
kind=MarkupKind.MARKDOWN, value=f"({variable.type.value}) {variable.name}"
103+
),
104+
range=range,
105+
)
106+
except BaseException:
107+
pass
108+
return None
60109

61110
async def hover_KeywordCall( # noqa: N802
62111
self, node: ast.AST, document: TextDocument, position: Position

0 commit comments

Comments
 (0)