Skip to content

Commit 94b21fb

Browse files
committed
feat: show argument infos for dynamic variables imports
1 parent 3720109 commit 94b21fb

File tree

4 files changed

+372
-71
lines changed

4 files changed

+372
-71
lines changed

packages/language_server/src/robotcode/language_server/robotframework/diagnostics/library_doc.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1512,7 +1512,9 @@ def get_variables_doc(
15121512
command_line_variables: Optional[Dict[str, Optional[Any]]] = None,
15131513
variables: Optional[Dict[str, Optional[Any]]] = None,
15141514
) -> VariablesDoc:
1515+
from robot.libdocpkg.robotbuilder import KeywordDocBuilder
15151516
from robot.output import LOGGER
1517+
from robot.running.handlers import _PythonHandler
15161518
from robot.utils.importer import Importer
15171519
from robot.variables.filesetter import PythonImporter, YamlImporter
15181520

@@ -1523,6 +1525,7 @@ def get_variables_doc(
15231525
try:
15241526
with _std_capture() as std_capturer:
15251527
import_name = find_variables(name, working_dir, base_dir, command_line_variables, variables)
1528+
get_variables = None
15261529

15271530
if import_name.lower().endswith((".yaml", ".yml")):
15281531
source = import_name
@@ -1543,6 +1546,9 @@ def __init__(self, var_file: Any) -> None:
15431546
def import_variables(self, path: str, args: Optional[Tuple[Any, ...]] = None) -> Any:
15441547
return self._get_variables(self.var_file, args)
15451548

1549+
def is_dynamic(self) -> bool:
1550+
return bool(self._is_dynamic(self.var_file))
1551+
15461552
module_importer = Importer("variable file", LOGGER)
15471553

15481554
if get_robot_version() >= (5, 0):
@@ -1555,6 +1561,9 @@ def import_variables(self, path: str, args: Optional[Tuple[Any, ...]] = None) ->
15551561

15561562
importer = MyPythonImporter(libcode)
15571563

1564+
if importer.is_dynamic():
1565+
get_variables = getattr(libcode, "get_variables", None) or getattr(libcode, "getVariables", None)
1566+
15581567
# TODO: add type information of the value including dict key names and member names
15591568
vars: List[ImportedVariableDefinition] = [
15601569
ImportedVariableDefinition(
@@ -1572,14 +1581,54 @@ def import_variables(self, path: str, args: Optional[Tuple[Any, ...]] = None) ->
15721581
for name, value in importer.import_variables(import_name, args)
15731582
]
15741583

1575-
return VariablesDoc(
1584+
libdoc = VariablesDoc(
15761585
name=stem,
15771586
source=source or module_spec.origin if module_spec is not None else import_name,
15781587
module_spec=module_spec,
15791588
variables=vars,
15801589
stdout=std_capturer.getvalue(),
15811590
python_path=sys.path,
15821591
)
1592+
1593+
if get_variables is not None:
1594+
1595+
class VarHandler(_PythonHandler):
1596+
def _get_name(self, handler_name: Any, handler_method: Any) -> Any:
1597+
return get_variables.__name__ if get_variables is not None else ""
1598+
1599+
def _get_initial_handler(self, library: Any, name: Any, method: Any) -> Any:
1600+
return None
1601+
1602+
vars_initializer = VarHandler(libdoc, get_variables.__name__, get_variables)
1603+
1604+
libdoc.inits = KeywordStore(
1605+
keywords=[
1606+
KeywordDoc(
1607+
name=libdoc.name,
1608+
args=[KeywordArgumentDoc.from_robot(a) for a in kw[0].args],
1609+
doc=kw[0].doc,
1610+
tags=list(kw[0].tags),
1611+
source=kw[0].source,
1612+
line_no=kw[0].lineno if kw[0].lineno is not None else -1,
1613+
col_offset=-1,
1614+
end_col_offset=-1,
1615+
end_line_no=-1,
1616+
type="library",
1617+
libname=libdoc.name,
1618+
libtype=libdoc.type,
1619+
longname=f"{libdoc.name}.{kw[0].name}",
1620+
is_initializer=True,
1621+
arguments=ArgumentSpec.from_robot_argument_spec(kw[1].arguments),
1622+
parent=libdoc.digest,
1623+
)
1624+
for kw in [
1625+
(KeywordDocBuilder().build_keyword(k), k)
1626+
for k in [KeywordWrapper(vars_initializer, libdoc.source or "")]
1627+
]
1628+
]
1629+
)
1630+
1631+
return libdoc
15831632
except (SystemExit, KeyboardInterrupt, IgnoreEasterEggLibraryWarning):
15841633
raise
15851634
except BaseException as e:

packages/language_server/src/robotcode/language_server/robotframework/parts/completion.py

Lines changed: 175 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1770,11 +1770,11 @@ async def complete_VariablesImport( # noqa: N802
17701770
position: Position,
17711771
context: Optional[CompletionContext],
17721772
) -> Union[List[CompletionItem], CompletionList, None]:
1773-
from robot.parsing.lexer.tokens import Token
1774-
from robot.parsing.model.statements import VariablesImport
1773+
from robot.parsing.lexer.tokens import Token as RobotToken
1774+
from robot.parsing.model.statements import Statement, VariablesImport
17751775

17761776
import_node = cast(VariablesImport, node)
1777-
import_token = import_node.get_token(Token.VARIABLES)
1777+
import_token = import_node.get_token(RobotToken.VARIABLES)
17781778

17791779
if import_token is None:
17801780
return []
@@ -1784,85 +1784,191 @@ async def complete_VariablesImport( # noqa: N802
17841784

17851785
import_token_index = import_node.tokens.index(import_token)
17861786

1787-
if len(import_node.tokens) > import_token_index + 2:
1788-
name_token = import_node.tokens[import_token_index + 2]
1789-
if not position.is_in_range(r := range_from_token(name_token)):
1790-
return None
1787+
async def complete_import() -> Optional[List[CompletionItem]]:
1788+
if len(import_node.tokens) > import_token_index + 2:
1789+
name_token = import_node.tokens[import_token_index + 2]
1790+
if not position.is_in_range(r := range_from_token(name_token)):
1791+
return None
17911792

1792-
elif len(import_node.tokens) > import_token_index + 1:
1793-
name_token = import_node.tokens[import_token_index + 1]
1794-
if position.is_in_range(r := range_from_token(name_token)):
1795-
if whitespace_at_begin_of_token(name_token) > 1:
1796-
ws_b = whitespace_from_begin_of_token(name_token)
1797-
r.start.character += 2 if ws_b and ws_b[0] != "\t" else 1
1793+
elif len(import_node.tokens) > import_token_index + 1:
1794+
name_token = import_node.tokens[import_token_index + 1]
1795+
if position.is_in_range(r := range_from_token(name_token)):
1796+
if whitespace_at_begin_of_token(name_token) > 1:
1797+
ws_b = whitespace_from_begin_of_token(name_token)
1798+
r.start.character += 2 if ws_b and ws_b[0] != "\t" else 1
17981799

1799-
if not position.is_in_range(r):
1800+
if not position.is_in_range(r):
1801+
return None
1802+
else:
18001803
return None
1801-
else:
1802-
return None
1803-
else:
1804-
return None
1804+
else:
1805+
return None
18051806

1806-
pos = position.character - r.start.character
1807-
text_before_position = str(name_token.value)[:pos].lstrip()
1807+
pos = position.character - r.start.character
1808+
text_before_position = str(name_token.value)[:pos].lstrip()
18081809

1809-
if text_before_position != "" and all(c == "." for c in text_before_position):
1810-
return None
1810+
if text_before_position != "" and all(c == "." for c in text_before_position):
1811+
return None
18111812

1812-
last_separator_index = (
1813-
len(text_before_position)
1814-
- next((i for i, c in enumerate(reversed(text_before_position)) if c in ["/", os.sep]), -1)
1815-
- 1
1816-
)
1813+
last_separator_index = (
1814+
len(text_before_position)
1815+
- next((i for i, c in enumerate(reversed(text_before_position)) if c in ["/", os.sep]), -1)
1816+
- 1
1817+
)
18171818

1818-
first_part = (
1819-
text_before_position[
1820-
: last_separator_index + (1 if text_before_position[last_separator_index] in ["/", os.sep] else 0)
1819+
first_part = (
1820+
text_before_position[
1821+
: last_separator_index + (1 if text_before_position[last_separator_index] in ["/", os.sep] else 0)
1822+
]
1823+
if last_separator_index < len(text_before_position)
1824+
else None
1825+
)
1826+
1827+
try:
1828+
complete_list = await self.namespace.imports_manager.complete_variables_import(
1829+
first_part if first_part else None,
1830+
str(self.document.uri.to_path().parent),
1831+
await self.namespace.get_resolvable_variables(nodes_at_position, position),
1832+
)
1833+
if not complete_list:
1834+
return None
1835+
except (SystemExit, KeyboardInterrupt, asyncio.CancelledError):
1836+
raise
1837+
except BaseException:
1838+
return None
1839+
1840+
if text_before_position == "":
1841+
r.start.character = position.character
1842+
else:
1843+
r.start.character += last_separator_index + 1 if last_separator_index < len(text_before_position) else 0
1844+
1845+
return [
1846+
CompletionItem(
1847+
label=e.label,
1848+
kind=CompletionItemKind.FILE
1849+
if e.kind in [CompleteResultKind.VARIABLES]
1850+
else CompletionItemKind.FILE
1851+
if e.kind in [CompleteResultKind.FILE]
1852+
else CompletionItemKind.FOLDER
1853+
if e.kind in [CompleteResultKind.FOLDER]
1854+
else None,
1855+
detail=e.kind.value,
1856+
sort_text=f"030_{e}",
1857+
insert_text_format=InsertTextFormat.PLAIN_TEXT,
1858+
text_edit=TextEdit(range=r, new_text=e.label) if r is not None else None,
1859+
data=CompletionItemData(
1860+
document_uri=str(self.document.uri),
1861+
type=e.kind.name,
1862+
name=((first_part) if first_part is not None else "") + e.label,
1863+
),
1864+
)
1865+
for e in complete_list
18211866
]
1822-
if last_separator_index < len(text_before_position)
1823-
else None
1824-
)
18251867

1826-
try:
1827-
complete_list = await self.namespace.imports_manager.complete_variables_import(
1828-
first_part if first_part else None,
1829-
str(self.document.uri.to_path().parent),
1830-
await self.namespace.get_resolvable_variables(nodes_at_position, position),
1831-
)
1832-
if not complete_list:
1868+
async def complete_arguments() -> Optional[List[CompletionItem]]:
1869+
if (
1870+
import_node.name is None
1871+
or position <= range_from_token(import_node.get_token(RobotToken.NAME)).extend(end_character=1).end
1872+
):
18331873
return None
1834-
except (SystemExit, KeyboardInterrupt, asyncio.CancelledError):
1835-
raise
1836-
except BaseException:
1874+
1875+
with_name_token = next((v for v in import_node.tokens if v.value == "WITH NAME"), None)
1876+
if with_name_token is not None and position >= range_from_token(with_name_token).start:
1877+
return None
1878+
1879+
if context is None or context.trigger_kind != CompletionTriggerKind.INVOKED:
1880+
return []
1881+
1882+
kw_node = cast(Statement, node)
1883+
1884+
tokens_at_position = get_tokens_at_position(kw_node, position)
1885+
1886+
if not tokens_at_position:
1887+
return None
1888+
1889+
token_at_position = tokens_at_position[-1]
1890+
1891+
if token_at_position.type not in [RobotToken.ARGUMENT, RobotToken.EOL, RobotToken.SEPARATOR]:
1892+
return None
1893+
1894+
if (
1895+
token_at_position.type == RobotToken.EOL
1896+
and len(tokens_at_position) > 1
1897+
and tokens_at_position[-2].type == RobotToken.KEYWORD
1898+
):
1899+
return None
1900+
1901+
token_at_position_index = kw_node.tokens.index(token_at_position)
1902+
1903+
argument_token_index = token_at_position_index
1904+
while argument_token_index >= 0 and kw_node.tokens[argument_token_index].type != RobotToken.ARGUMENT:
1905+
argument_token_index -= 1
1906+
1907+
argument_token: Optional[RobotToken] = None
1908+
if argument_token_index >= 0:
1909+
argument_token = kw_node.tokens[argument_token_index]
1910+
1911+
completion_range = range_from_token(argument_token or token_at_position)
1912+
completion_range.end = range_from_token(token_at_position).end
1913+
if (w := whitespace_at_begin_of_token(token_at_position)) > 0:
1914+
if w > 1 and range_from_token(token_at_position).start.character + 1 < position.character:
1915+
completion_range.start = position
1916+
elif completion_range.start != position:
1917+
return None
1918+
else:
1919+
if "=" in (argument_token or token_at_position).value:
1920+
equal_index = (argument_token or token_at_position).value.index("=")
1921+
if completion_range.start.character + equal_index < position.character:
1922+
return None
1923+
1924+
completion_range.end.character = completion_range.start.character + equal_index + 1
1925+
else:
1926+
completion_range.end = position
1927+
1928+
try:
1929+
libdoc = await self.namespace.get_imported_variables_libdoc(import_node.name, import_node.args)
1930+
1931+
except (SystemExit, KeyboardInterrupt, asyncio.CancelledError):
1932+
raise
1933+
except BaseException as e:
1934+
self._logger.exception(e)
1935+
return None
1936+
1937+
if libdoc is not None:
1938+
init = next((v for v in libdoc.inits.values()), None)
1939+
1940+
if init:
1941+
return [
1942+
CompletionItem(
1943+
label=f"{e.name}=",
1944+
kind=CompletionItemKind.VARIABLE,
1945+
sort_text=f"010{i}_{e.name}",
1946+
filter_text=e.name,
1947+
insert_text_format=InsertTextFormat.PLAIN_TEXT,
1948+
text_edit=TextEdit(range=completion_range, new_text=f"{e.name}="),
1949+
data=CompletionItemData(
1950+
document_uri=str(self.document.uri),
1951+
type="Argument",
1952+
name=e.name,
1953+
),
1954+
)
1955+
for i, e in enumerate(init.args)
1956+
if e.kind
1957+
not in [
1958+
KeywordArgumentKind.VAR_POSITIONAL,
1959+
KeywordArgumentKind.VAR_NAMED,
1960+
KeywordArgumentKind.NAMED_ONLY_MARKER,
1961+
KeywordArgumentKind.POSITIONAL_ONLY_MARKER,
1962+
]
1963+
]
1964+
18371965
return None
18381966

1839-
if text_before_position == "":
1840-
r.start.character = position.character
1841-
else:
1842-
r.start.character += last_separator_index + 1 if last_separator_index < len(text_before_position) else 0
1967+
result = await complete_import() or []
1968+
# TODO this is not supported in robotframework, but it would be nice to have
1969+
# result.extend(await complete_arguments() or [])
18431970

1844-
return [
1845-
CompletionItem(
1846-
label=e.label,
1847-
kind=CompletionItemKind.FILE
1848-
if e.kind in [CompleteResultKind.VARIABLES]
1849-
else CompletionItemKind.FILE
1850-
if e.kind in [CompleteResultKind.FILE]
1851-
else CompletionItemKind.FOLDER
1852-
if e.kind in [CompleteResultKind.FOLDER]
1853-
else None,
1854-
detail=e.kind.value,
1855-
sort_text=f"030_{e}",
1856-
insert_text_format=InsertTextFormat.PLAIN_TEXT,
1857-
text_edit=TextEdit(range=r, new_text=e.label) if r is not None else None,
1858-
data=CompletionItemData(
1859-
document_uri=str(self.document.uri),
1860-
type=e.kind.name,
1861-
name=((first_part) if first_part is not None else "") + e.label,
1862-
),
1863-
)
1864-
for e in complete_list
1865-
]
1971+
return result # noqa: RET504
18661972

18671973
async def _complete_KeywordCall_or_Fixture( # noqa: N802
18681974
self,

0 commit comments

Comments
 (0)