Skip to content

Commit 291d02c

Browse files
committed
implement find references for variables
1 parent 24bc91b commit 291d02c

File tree

7 files changed

+152
-46
lines changed

7 files changed

+152
-46
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ All notable changes to the "robotcode" extension will be documented in this file
99
- goto
1010
- static and dynamic variables
1111
- correct debugger hover on variables and last fail message
12+
- implement find references for variables
1213

1314

1415
## 0.4.1

robotcode/language_server/robotframework/diagnostics/entities.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ def __hash__(self) -> int:
7676

7777
class VariableDefinitionType(Enum):
7878
VARIABLE = "variable"
79+
LOCAL_VARIABLE = "local variable"
7980
ARGUMENT = "argument"
8081
COMMAND_LINE_VARIABLE = "command line variable"
8182
BUILTIN_VARIABLE = "builtin variable"
@@ -104,6 +105,14 @@ def range(self) -> Range:
104105
)
105106

106107

108+
@dataclass
109+
class LocalVariableDefinition(VariableDefinition):
110+
type: VariableDefinitionType = VariableDefinitionType.LOCAL_VARIABLE
111+
112+
def __hash__(self) -> int:
113+
return hash((type(self), self.name, self.type))
114+
115+
107116
@dataclass
108117
class BuiltInVariableDefinition(VariableDefinition):
109118
type: VariableDefinitionType = VariableDefinitionType.BUILTIN_VARIABLE

robotcode/language_server/robotframework/diagnostics/imports_manager.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from ...common.text_document import TextDocument
3030
from ..configuration import RobotConfig
3131
from ..utils.async_ast import walk
32+
from .entities import CommandLineVariableDefinition, VariableDefinition
3233

3334
if TYPE_CHECKING:
3435
from ..protocol import RobotLanguageServerProtocol
@@ -465,6 +466,20 @@ def __init__(
465466
self._variables: OrderedDict[_VariablesEntryKey, _VariablesEntry] = OrderedDict()
466467
self.file_watchers: List[FileWatcherEntry] = []
467468
self.parent_protocol.documents.did_change.add(self.resource_document_changed)
469+
self._command_line_variables: Optional[List[VariableDefinition]] = None
470+
471+
@_logger.call
472+
def get_command_line_variables(self) -> List[VariableDefinition]:
473+
if self._command_line_variables is None:
474+
if self.config is None:
475+
self._command_line_variables = []
476+
else:
477+
self._command_line_variables = [
478+
CommandLineVariableDefinition(0, 0, 0, 0, "", f"${{{k}}}", None)
479+
for k in self.config.variables.keys()
480+
]
481+
482+
return self._command_line_variables
468483

469484
@async_tasking_event
470485
async def libraries_changed(sender, libraries: List[LibraryDoc]) -> None: # NOSONAR

robotcode/language_server/robotframework/diagnostics/library_doc.py

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,15 @@ class InvalidVariableError(Exception):
165165

166166

167167
class VariableMatcher:
168-
def __init__(self, name: str) -> None:
168+
_match_extended = re.compile(
169+
r"""
170+
(.+?) # base name (group 1)
171+
([^\s\w].+) # extended part (group 2)
172+
""",
173+
re.UNICODE | re.VERBOSE,
174+
)
175+
176+
def __init__(self, name: str, extended: bool = False) -> None:
169177
from robot.utils.normalizing import normalize
170178
from robot.variables.search import VariableSearcher
171179

@@ -176,7 +184,13 @@ def __init__(self, name: str) -> None:
176184
raise InvalidVariableError(f"Invalid variable '{name}'")
177185

178186
self.base = match.base
179-
self.normalized_name = str(normalize(match.base, "_"))
187+
188+
if extended:
189+
ext_match = self._match_extended.match(self.name[2:-1])
190+
if ext_match is not None:
191+
self.base, _ = ext_match.groups()
192+
193+
self.normalized_name = str(normalize(self.base, "_"))
180194

181195
def __eq__(self, o: object) -> bool:
182196
from robot.utils.normalizing import normalize
@@ -1224,21 +1238,18 @@ def get_variables_doc(
12241238
stem = Path(name).stem
12251239
try:
12261240
source = find_file(name, working_dir, base_dir, pythonpath, environment, variables)
1241+
with _std_capture() as std_capturer:
12271242

1228-
if source.lower().endswith((".yaml", ".yml")):
1229-
importer = YamlImporter()
1230-
else:
1231-
importer = PythonImporter()
1232-
vars: List[VariableDefinition] = [
1233-
ImportedVariableDefinition(1, 0, 1, 0, source, var[0], None)
1234-
for var in importer.import_variables(source, args)
1235-
]
1243+
if source.lower().endswith((".yaml", ".yml")):
1244+
importer = YamlImporter()
1245+
else:
1246+
importer = PythonImporter()
1247+
vars: List[VariableDefinition] = [
1248+
ImportedVariableDefinition(1, 0, 1, 0, source, var[0], None)
1249+
for var in importer.import_variables(source, args)
1250+
]
12361251

1237-
return VariablesDoc(
1238-
name=stem,
1239-
source=source,
1240-
variables=vars,
1241-
)
1252+
return VariablesDoc(name=stem, source=source, variables=vars, stdout=std_capturer.getvalue())
12421253
except BaseException as e:
12431254
return VariablesDoc(
12441255
name=stem,

robotcode/language_server/robotframework/diagnostics/namespace.py

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@
3939
from .entities import (
4040
ArgumentDefinition,
4141
BuiltInVariableDefinition,
42-
CommandLineVariableDefinition,
4342
Import,
4443
LibraryImport,
44+
LocalVariableDefinition,
4545
ResourceImport,
4646
VariableDefinition,
4747
VariablesImport,
@@ -206,7 +206,7 @@ async def visit_KeywordCall(self, node: ast.AST) -> None: # noqa: N802
206206
variable_token = self.get_variable_token(assign_token)
207207
try:
208208
if variable_token is not None and variable_token.value not in self._results:
209-
self._results[variable_token.value] = VariableDefinition(
209+
self._results[variable_token.value] = LocalVariableDefinition(
210210
name=variable_token.value,
211211
name_token=variable_token,
212212
line_no=variable_token.lineno,
@@ -228,7 +228,7 @@ async def visit_ForHeader(self, node: ast.AST) -> None: # noqa: N802
228228
for variable in variables:
229229
variable_token = self.get_variable_token(variable)
230230
if variable_token is not None and variable_token.value and variable_token.value not in self._results:
231-
self._results[variable_token.value] = VariableDefinition(
231+
self._results[variable_token.value] = LocalVariableDefinition(
232232
name=variable_token.value,
233233
name_token=variable_token,
234234
line_no=node.lineno,
@@ -580,13 +580,7 @@ def get_builtin_variables(cls) -> List[BuiltInVariableDefinition]:
580580

581581
@_logger.call
582582
def get_command_line_variables(self) -> List[VariableDefinition]:
583-
if self.imports_manager.config is None:
584-
return []
585-
586-
return [
587-
CommandLineVariableDefinition(0, 0, 0, 0, "", f"${{{k}}}", None)
588-
for k in self.imports_manager.config.variables.keys()
589-
]
583+
return self.imports_manager.get_command_line_variables()
590584

591585
@_logger.call
592586
async def get_variables(
@@ -619,7 +613,7 @@ async def get_variables(
619613
async def find_variable(
620614
self, name: str, nodes: Optional[List[ast.AST]], position: Optional[Position] = None
621615
) -> Optional[VariableDefinition]:
622-
return (await self.get_variables(nodes, position)).get(VariableMatcher(name), None)
616+
return (await self.get_variables(nodes, position)).get(VariableMatcher(name, True), None)
623617

624618
@_logger.call
625619
async def _import_imports(self, imports: Iterable[Import], base_dir: str, *, top_level: bool = False) -> None:

robotcode/language_server/robotframework/parts/completion.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -338,10 +338,12 @@ async def create_environment_variables_completion_items(self, range: Optional[Ra
338338
]
339339

340340
_VARIABLE_COMPLETION_SORT_TEXT_PREFIX = {
341-
VariableDefinitionType.VARIABLE: "035",
341+
VariableDefinitionType.LOCAL_VARIABLE: "033",
342342
VariableDefinitionType.ARGUMENT: "034",
343-
VariableDefinitionType.COMMAND_LINE_VARIABLE: "036",
344-
VariableDefinitionType.BUILTIN_VARIABLE: "037",
343+
VariableDefinitionType.VARIABLE: "035",
344+
VariableDefinitionType.IMPORTED_VARIABLE: "036",
345+
VariableDefinitionType.COMMAND_LINE_VARIABLE: "037",
346+
VariableDefinitionType.BUILTIN_VARIABLE: "038",
345347
}
346348

347349
async def create_variables_completion_items(
@@ -532,7 +534,6 @@ def enumerate_indexes(s: str, c: str) -> Iterator[int]:
532534
detail="Library",
533535
sort_text=f"030_{k}",
534536
deprecated=v.library_doc.is_deprecated,
535-
# documentation=MarkupContent(kind=MarkupKind.MARKDOWN, value=v.library_doc.to_markdown()),
536537
insert_text_format=InsertTextFormat.PLAINTEXT,
537538
text_edit=TextEdit(range=r, new_text=k) if r is not None else None,
538539
data={
@@ -550,7 +551,6 @@ def enumerate_indexes(s: str, c: str) -> Iterator[int]:
550551
detail="Resource",
551552
deprecated=v.library_doc.is_deprecated,
552553
sort_text=f"030_{k}",
553-
# documentation=MarkupContent(kind=MarkupKind.MARKDOWN, value=v.library_doc.to_markdown()),
554554
insert_text_format=InsertTextFormat.PLAINTEXT,
555555
text_edit=TextEdit(range=r, new_text=k) if r is not None else None,
556556
data={

robotcode/language_server/robotframework/parts/references.py

Lines changed: 91 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
)
1818

1919
from ....utils.async_tools import (
20-
CancelationToken,
2120
check_canceled,
2221
create_sub_task,
2322
run_coroutine_in_thread,
@@ -29,6 +28,11 @@
2928
from ...common.lsp_types import Location, Position, ReferenceContext
3029
from ...common.text_document import TextDocument
3130
from ..configuration import WorkspaceConfig
31+
from ..diagnostics.entities import (
32+
ArgumentDefinition,
33+
LocalVariableDefinition,
34+
VariableDefinition,
35+
)
3236
from ..diagnostics.library_doc import (
3337
ALL_RUN_KEYWORDS_MATCHERS,
3438
RESOURCE_FILE_EXTENSION,
@@ -136,12 +140,19 @@ async def _references_default(
136140

137141
if position.is_in_range(range):
138142
variable = await namespace.find_variable(sub_token.value, nodes, position)
139-
if variable is not None and variable.source:
143+
if variable is not None:
140144
return [
141-
Location(
142-
uri=str(Uri.from_path(variable.source)),
143-
range=variable.range(),
144-
)
145+
*(
146+
[
147+
Location(
148+
uri=str(Uri.from_path(variable.source)),
149+
range=variable.range(),
150+
),
151+
]
152+
if context.include_declaration and variable.source
153+
else []
154+
),
155+
*await self._find_variable_references(document, variable),
145156
]
146157
except (SystemExit, KeyboardInterrupt, asyncio.CancelledError):
147158
raise
@@ -298,11 +309,10 @@ async def _find_keyword_references_in_file(
298309
kw_doc: KeywordDoc,
299310
lib_doc: Optional[LibraryDoc],
300311
file: Path,
301-
cancel_token: CancelationToken,
302312
) -> List[Location]:
303313

304314
doc = await self.parent.robot_workspace.get_or_open_document(file, "robotframework")
305-
namespace = await self.parent.documents_cache.get_namespace(doc, cancelation_token=cancel_token)
315+
namespace = await self.parent.documents_cache.get_namespace(doc)
306316

307317
if (
308318
lib_doc is not None
@@ -313,11 +323,9 @@ async def _find_keyword_references_in_file(
313323
):
314324
return []
315325

316-
return await self._find_keyword_references_in_namespace(namespace, kw_doc, cancel_token)
326+
return await self._find_keyword_references_in_namespace(namespace, kw_doc)
317327

318-
async def _find_keyword_references_in_namespace(
319-
self, namespace: Namespace, kw_doc: KeywordDoc, cancel_token: CancelationToken
320-
) -> List[Location]:
328+
async def _find_keyword_references_in_namespace(self, namespace: Namespace, kw_doc: KeywordDoc) -> List[Location]:
321329
from robot.parsing.lexer.tokens import Token as RobotToken
322330
from robot.parsing.model.statements import (
323331
Fixture,
@@ -503,8 +511,6 @@ async def _find_keyword_references(self, document: TextDocument, kw_doc: Keyword
503511
or await namespace.get_library_doc()
504512
)
505513

506-
cancel_token = CancelationToken()
507-
508514
futures: List[Awaitable[List[Location]]] = []
509515
result: List[Location] = []
510516

@@ -516,7 +522,77 @@ async def _find_keyword_references(self, document: TextDocument, kw_doc: Keyword
516522
ignore_patterns=config.exclude_patterns or [], # type: ignore
517523
absolute=True,
518524
):
519-
futures.append(create_sub_task(self._find_keyword_references_in_file(kw_doc, lib_doc, f, cancel_token)))
525+
futures.append(create_sub_task(self._find_keyword_references_in_file(kw_doc, lib_doc, f)))
526+
527+
for e in await asyncio.gather(*futures, return_exceptions=True):
528+
if isinstance(e, BaseException):
529+
self._logger.exception(e)
530+
continue
531+
result.extend(e)
532+
533+
return result
534+
535+
@_logger.call
536+
async def _find_variable_references_in_file(
537+
self,
538+
variable: VariableDefinition,
539+
file: Path,
540+
) -> List[Location]:
541+
from robot.parsing.lexer.tokens import Token as RobotToken
542+
from robot.parsing.model.blocks import Block, Keyword, Section, TestCase
543+
544+
doc = await self.parent.robot_workspace.get_or_open_document(file, "robotframework")
545+
namespace = await self.parent.documents_cache.get_namespace(doc)
546+
model = await self.parent.documents_cache.get_model(doc)
547+
548+
result: List[Location] = []
549+
current_block: Optional[Block] = None
550+
551+
async for node in iter_nodes(model):
552+
if isinstance(node, Section):
553+
current_block = None
554+
elif isinstance(node, (TestCase, Keyword)):
555+
current_block = node
556+
557+
if isinstance(node, HasTokens):
558+
for token in node.tokens:
559+
for sub_token in tokenize_variables(token):
560+
if sub_token.type == RobotToken.VARIABLE:
561+
found_variable = await namespace.find_variable(
562+
sub_token.value,
563+
[*([current_block] if current_block is not None else []), node],
564+
range_from_token(token).start,
565+
)
566+
567+
if found_variable == variable:
568+
result.append(Location(str(doc.uri), range_from_token(sub_token)))
569+
570+
return result
571+
572+
async def _find_variable_references(self, document: TextDocument, variable: VariableDefinition) -> List[Location]:
573+
folder = self.parent.workspace.get_workspace_folder(document.uri)
574+
if folder is None:
575+
return []
576+
577+
namespace = await self.parent.documents_cache.get_namespace(document)
578+
if namespace is None:
579+
return None
580+
581+
futures: List[Awaitable[List[Location]]] = []
582+
result: List[Location] = []
583+
584+
config = await self.parent.workspace.get_configuration(WorkspaceConfig, folder.uri) or WorkspaceConfig()
585+
586+
if isinstance(variable, (ArgumentDefinition, LocalVariableDefinition)):
587+
futures.append(create_sub_task(self._find_variable_references_in_file(variable, document.uri.to_path())))
588+
else:
589+
async for f in iter_files(
590+
folder.uri.to_path(),
591+
(f"**/*.{{{ROBOT_FILE_EXTENSION[1:]},{RESOURCE_FILE_EXTENSION[1:]}}}"),
592+
ignore_patterns=config.exclude_patterns or [], # type: ignore
593+
absolute=True,
594+
):
595+
futures.append(create_sub_task(self._find_variable_references_in_file(variable, f)))
520596

521597
for e in await asyncio.gather(*futures, return_exceptions=True):
522598
if isinstance(e, BaseException):

0 commit comments

Comments
 (0)