Skip to content

Commit bd39e30

Browse files
committed
feat(langserver): first version of completion of enums and typed dicts for RF >= 6.1
1 parent 46125a5 commit bd39e30

File tree

6 files changed

+269
-175
lines changed

6 files changed

+269
-175
lines changed

Diff for: packages/language_server/src/robotcode/language_server/robotframework/diagnostics/library_doc.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -906,10 +906,15 @@ def __hash__(self) -> int:
906906
)
907907

908908
def get_types(self, type_names: Optional[List[str]]) -> List[TypeDoc]:
909+
def alias(s: str) -> str:
910+
if s == "boolean":
911+
return "bool"
912+
return s
913+
909914
if not type_names:
910915
return []
911916

912-
return [t for t in self.types if t.name in type_names]
917+
return [t for t in self.types if alias(t.name) in type_names]
913918

914919
@property
915920
def is_deprecated(self) -> bool:
@@ -1715,7 +1720,7 @@ def get_test_library(
17151720
],
17161721
)
17171722

1718-
if get_robot_version() >= (5, 0):
1723+
if get_robot_version() >= (6, 1):
17191724
from robot.libdocpkg.datatypes import TypeDoc as RobotTypeDoc
17201725
from robot.running.arguments.argumentspec import TypeInfo
17211726

Diff for: packages/language_server/src/robotcode/language_server/robotframework/parts/completion.py

+124-60
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from robotcode.core.async_itertools import async_chain, async_chain_iterator
2828
from robotcode.core.logging import LoggingDescriptor
2929
from robotcode.core.lsp.types import (
30+
Command,
3031
CompletionContext,
3132
CompletionItem,
3233
CompletionItemKind,
@@ -104,6 +105,7 @@ async def get_header_style(self, config: CompletionConfig) -> str:
104105
".",
105106
"/",
106107
"{",
108+
"=",
107109
os.sep,
108110
],
109111
)
@@ -2021,8 +2023,8 @@ async def _complete_KeywordCall_or_Fixture( # noqa: N802
20212023
from robot.parsing.lexer.tokens import Token as RobotToken
20222024
from robot.parsing.model.statements import Statement
20232025

2024-
if context is None or context.trigger_kind != CompletionTriggerKind.INVOKED:
2025-
return []
2026+
# if context is None or context.trigger_kind != CompletionTriggerKind.INVOKED:
2027+
# return []
20262028

20272029
kw_node = cast(Statement, node)
20282030

@@ -2040,24 +2042,35 @@ async def _complete_KeywordCall_or_Fixture( # noqa: N802
20402042
if token_at_position.type not in [RobotToken.ARGUMENT, RobotToken.EOL, RobotToken.SEPARATOR]:
20412043
return None
20422044

2043-
if (
2044-
token_at_position.type in [RobotToken.EOL, RobotToken.SEPARATOR]
2045-
and len(tokens_at_position) > 1
2046-
and tokens_at_position[-2].type == RobotToken.KEYWORD
2047-
):
2045+
keyword_doc_and_token: Optional[Tuple[Optional[KeywordDoc], Token]] = None
2046+
2047+
keyword_token = kw_node.get_token(keyword_name_token_type)
2048+
if keyword_token is None:
2049+
return None
2050+
2051+
keyword_doc_and_token = await self.get_keyworddoc_and_token_from_position(
2052+
keyword_token.value,
2053+
keyword_token,
2054+
[t for t in kw_node.get_tokens(RobotToken.ARGUMENT)],
2055+
self.namespace,
2056+
range_from_token(keyword_token).start,
2057+
analyse_run_keywords=False,
2058+
)
2059+
2060+
if keyword_doc_and_token is None or keyword_doc_and_token[0] is None:
20482061
return None
20492062

2050-
token_at_position_index = kw_node.tokens.index(token_at_position)
2063+
keyword_doc = keyword_doc_and_token[0]
20512064

2052-
argument_token_index = token_at_position_index
2053-
while argument_token_index >= 0 and kw_node.tokens[argument_token_index].type != RobotToken.ARGUMENT:
2054-
argument_token_index -= 1
2065+
if keyword_doc.is_any_run_keyword():
2066+
# TODO
2067+
pass
20552068

2056-
argument_token: Optional[RobotToken] = None
2057-
if argument_token_index >= 0:
2058-
argument_token = kw_node.tokens[argument_token_index]
2069+
argument_index, kw_arguments, argument_token = self.get_argument_info_at_position(
2070+
keyword_doc, kw_node.tokens, token_at_position, position
2071+
)
20592072

2060-
result: Optional[Tuple[Optional[KeywordDoc], Token]]
2073+
complete_argument_names = True
20612074

20622075
completion_range = range_from_token(argument_token or token_at_position)
20632076
completion_range.end = range_from_token(token_at_position).end
@@ -2069,56 +2082,107 @@ async def _complete_KeywordCall_or_Fixture( # noqa: N802
20692082
else:
20702083
if "=" in (argument_token or token_at_position).value:
20712084
equal_index = (argument_token or token_at_position).value.index("=")
2072-
if completion_range.start.character + equal_index < position.character:
2073-
return None
2074-
2075-
completion_range.end.character = completion_range.start.character + equal_index + 1
2076-
else:
2077-
completion_range.end = position
2085+
if position.character < completion_range.start.character + equal_index:
2086+
completion_range.end.character = completion_range.start.character + equal_index + 1
2087+
else:
2088+
complete_argument_names = False
2089+
completion_range.start.character = completion_range.start.character + equal_index + 1
2090+
completion_range.end = position
20782091

2079-
result = await self.get_keyworddoc_and_token_from_position(
2080-
keyword_token.value,
2081-
keyword_token,
2082-
[cast(Token, t) for t in kw_node.get_tokens(RobotToken.ARGUMENT)],
2083-
self.namespace,
2084-
range_from_token(keyword_token).start,
2085-
analyse_run_keywords=False,
2086-
)
2092+
result = []
20872093

2088-
if result is None or result[0] is None:
2089-
return None
2094+
if argument_index >= 0 and keyword_doc.parent is not None and argument_index < len(kw_arguments):
2095+
type_infos = keyword_doc.parent.get_types(kw_arguments[argument_index].types)
2096+
for i, type_info in enumerate(type_infos):
2097+
if type_info.name == "boolean":
2098+
snippets = [
2099+
"True",
2100+
"False",
2101+
]
2102+
for i, snippet in enumerate(snippets):
2103+
result.append(
2104+
CompletionItem(
2105+
label=snippet,
2106+
kind=CompletionItemKind.VALUE,
2107+
detail=type_info.name,
2108+
documentation=MarkupContent(MarkupKind.MARKDOWN, type_info.to_markdown()),
2109+
sort_text=f"02{i:03}_{snippet}",
2110+
insert_text_format=InsertTextFormat.PLAIN_TEXT,
2111+
text_edit=TextEdit(
2112+
range=completion_range,
2113+
new_text=snippet,
2114+
),
2115+
)
2116+
)
20902117

2091-
if result[0].is_any_run_keyword():
2092-
# TODO: complete run keyword
2093-
# ks = await self.get_keyworddoc_and_token_from_position(
2094-
# keyword_token.value,
2095-
# keyword_token,
2096-
# [cast(Token, t) for t in kw_node.get_tokens(RobotToken.ARGUMENT)],
2097-
# namespace,
2098-
# position,
2099-
# analyse_run_keywords=True,
2100-
# )
2101-
pass
2118+
if type_info.members:
2119+
for member in type_info.members:
2120+
result.append(
2121+
CompletionItem(
2122+
label=member.name,
2123+
kind=CompletionItemKind.ENUM_MEMBER,
2124+
detail=type_info.name,
2125+
documentation=MarkupContent(
2126+
MarkupKind.MARKDOWN, f"```python\n{member.name} = {member.value}\n```"
2127+
),
2128+
sort_text=f"01{i}_{member.name}",
2129+
insert_text_format=InsertTextFormat.PLAIN_TEXT,
2130+
text_edit=TextEdit(
2131+
range=completion_range,
2132+
new_text=member.name,
2133+
),
2134+
)
2135+
)
2136+
if type_info.items:
2137+
snippets = [
2138+
"{"
2139+
+ ", ".join(
2140+
(str(m.key) + ": ${" + str(i + 1) + "}")
2141+
for i, m in enumerate(type_info.items)
2142+
if m.required
2143+
)
2144+
+ "}",
2145+
f"{{{', '.join((str(m.key)+': ${'+str(i+1)+'}') for i, m in enumerate(type_info.items))}}}",
2146+
]
2147+
for i, snippet in enumerate(snippets):
2148+
result.append(
2149+
CompletionItem(
2150+
label=snippet,
2151+
kind=CompletionItemKind.STRUCT,
2152+
detail=type_info.name,
2153+
documentation=MarkupContent(MarkupKind.MARKDOWN, type_info.to_markdown()),
2154+
sort_text=f"02{i:03}_{snippet}",
2155+
insert_text_format=InsertTextFormat.SNIPPET,
2156+
text_edit=TextEdit(
2157+
range=completion_range,
2158+
new_text=snippet,
2159+
),
2160+
)
2161+
)
21022162

2103-
return [
2104-
CompletionItem(
2105-
label=f"{e.name}=",
2106-
kind=CompletionItemKind.VARIABLE,
2107-
detail="Argument",
2108-
filter_text=e.name,
2109-
sort_text=f"02{i}_{e.name}=",
2110-
insert_text_format=InsertTextFormat.PLAIN_TEXT,
2111-
text_edit=TextEdit(range=completion_range, new_text=f"{e.name}="),
2112-
)
2113-
for i, e in enumerate(result[0].arguments)
2114-
if e.kind
2115-
not in [
2116-
KeywordArgumentKind.VAR_POSITIONAL,
2117-
KeywordArgumentKind.VAR_NAMED,
2118-
KeywordArgumentKind.NAMED_ONLY_MARKER,
2119-
KeywordArgumentKind.POSITIONAL_ONLY_MARKER,
2163+
if complete_argument_names:
2164+
result += [
2165+
CompletionItem(
2166+
label=f"{e.name}=",
2167+
kind=CompletionItemKind.VARIABLE,
2168+
detail="Argument",
2169+
filter_text=e.name,
2170+
sort_text=f"03{i:03}_{e.name}=",
2171+
insert_text_format=InsertTextFormat.PLAIN_TEXT,
2172+
text_edit=TextEdit(range=completion_range, new_text=f"{e.name}="),
2173+
command=Command("", "editor.action.triggerSuggest", []),
2174+
)
2175+
for i, e in enumerate(kw_arguments)
2176+
if e.kind
2177+
not in [
2178+
KeywordArgumentKind.VAR_POSITIONAL,
2179+
KeywordArgumentKind.VAR_NAMED,
2180+
KeywordArgumentKind.NAMED_ONLY_MARKER,
2181+
KeywordArgumentKind.POSITIONAL_ONLY_MARKER,
2182+
]
21202183
]
2121-
]
2184+
2185+
return result
21222186

21232187
async def complete_KeywordCall( # noqa: N802
21242188
self,

Diff for: packages/language_server/src/robotcode/language_server/robotframework/parts/inlay_hint.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,11 @@ async def _get_inlay_hint(
150150
if i >= len(arguments):
151151
break
152152

153-
arg = kw_arguments[i if i < len(kw_arguments) else len(kw_arguments) - 1]
153+
index = i if i < len(kw_arguments) else len(kw_arguments) - 1
154+
if index < 0:
155+
continue
156+
157+
arg = kw_arguments[index]
154158
if i >= len(kw_arguments) and arg.kind not in [
155159
KeywordArgumentKind.VAR_POSITIONAL,
156160
]:

0 commit comments

Comments
 (0)