Skip to content

Commit b34c8bf

Browse files
committed
refactor: prepare create keywords quickfix
1 parent d890a35 commit b34c8bf

File tree

6 files changed

+216
-30
lines changed

6 files changed

+216
-30
lines changed

robotcode/language_server/common/decorators.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def filter(c: Any) -> bool:
8383

8484
@runtime_checkable
8585
class IsCommand(Protocol):
86-
__command_name__: List[str]
86+
__command_name__: str
8787

8888

8989
def command(name: str) -> Callable[[_F], _F]:

robotcode/language_server/common/parts/commands.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
from __future__ import annotations
22

3+
import inspect
4+
import typing
35
import uuid
46
from dataclasses import dataclass
5-
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, List, Optional
7+
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, List, Optional, cast
68

79
from ....jsonrpc2.protocol import JsonRPCErrorException, rpc_method
810
from ....utils.async_tools import threaded
11+
from ....utils.dataclasses import from_dict
912
from ....utils.logging import LoggingDescriptor
10-
from ..decorators import get_command_name
13+
from ..decorators import IsCommand, get_command_name
1114
from ..has_extend_capabilities import HasExtendCapabilities
1215
from ..lsp_types import (
1316
ErrorCodes,
@@ -53,6 +56,14 @@ def register(self, callback: _FUNC_TYPE, name: Optional[str] = None) -> str:
5356

5457
return command
5558

59+
def register_all(self, instance: object) -> None:
60+
all_methods = [
61+
getattr(instance, k) for k, v in type(instance).__dict__.items() if callable(v) and not k.startswith("_")
62+
]
63+
for method in all_methods:
64+
if isinstance(method, IsCommand):
65+
self.register(cast(_FUNC_TYPE, method))
66+
5667
def get_command_name(self, callback: _FUNC_TYPE, name: Optional[str] = None) -> str:
5768
name = name or get_command_name(callback)
5869

@@ -73,4 +84,15 @@ async def _workspace_execute_command(
7384
if entry is None or entry.callback is None:
7485
raise JsonRPCErrorException(ErrorCodes.INVALID_PARAMS, f"Command '{command}' unknown.")
7586

76-
return await entry.callback(*(arguments or ()))
87+
signature = inspect.signature(entry.callback)
88+
type_hints = list(typing.get_type_hints(entry.callback).values())
89+
90+
command_args: List[Any] = []
91+
92+
if arguments:
93+
for i, v in enumerate(signature.parameters.values()):
94+
95+
if i < len(arguments):
96+
command_args.append(from_dict(arguments[i], type_hints[i]))
97+
98+
return await entry.callback(*command_args)

robotcode/language_server/robotframework/diagnostics/namespace.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1724,7 +1724,9 @@ def find_keyword(
17241724
if result is None:
17251725
self.diagnostics.append(
17261726
DiagnosticsEntry(
1727-
f"No keyword with name {repr(name)} found.", DiagnosticSeverity.ERROR, "KeywordError"
1727+
f"No keyword with name {repr(name)} found.",
1728+
DiagnosticSeverity.ERROR,
1729+
"KeywordNotFoundError",
17281730
)
17291731
)
17301732
except KeywordError as e:

robotcode/language_server/robotframework/parts/code_action.py renamed to robotcode/language_server/robotframework/parts/code_action_documentation.py

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,11 @@
1616
from ....utils.logging import LoggingDescriptor
1717
from ....utils.net import find_free_port
1818
from ....utils.uri import Uri
19-
from ...common.decorators import code_action_kinds, command, language_id
19+
from ...common.decorators import code_action_kinds, language_id
2020
from ...common.lsp_types import (
2121
CodeAction,
2222
CodeActionContext,
2323
CodeActionKinds,
24-
CodeActionTriggerKind,
2524
Command,
2625
Model,
2726
Range,
@@ -175,10 +174,9 @@ def server_bind(self) -> None:
175174

176175

177176
CODEACTIONKINDS_SOURCE_OPENDOCUMENTATION = f"{CodeActionKinds.SOURCE}.openDocumentation"
178-
CODEACTIONKINDS_QUICKFIX_CREATEKEYWORD = f"{CodeActionKinds.QUICKFIX}.createKeyword"
179177

180178

181-
class RobotCodeActionProtocolPart(RobotLanguageServerProtocolPart, ModelHelperMixin):
179+
class RobotCodeActionDocumentationProtocolPart(RobotLanguageServerProtocolPart, ModelHelperMixin):
182180
_logger = LoggingDescriptor()
183181

184182
def __init__(self, parent: RobotLanguageServerProtocol) -> None:
@@ -192,7 +190,7 @@ def __init__(self, parent: RobotLanguageServerProtocol) -> None:
192190
self._documentation_server_lock = threading.RLock()
193191
self._documentation_server_port = 0
194192

195-
self.parent.commands.register(self.comming_soon)
193+
self.parent.commands.register_all(self)
196194

197195
async def initialized(self, sender: Any) -> None:
198196
await self._ensure_http_server_started()
@@ -234,7 +232,6 @@ async def _ensure_http_server_started(self) -> None:
234232
@code_action_kinds(
235233
[
236234
CODEACTIONKINDS_SOURCE_OPENDOCUMENTATION,
237-
CODEACTIONKINDS_QUICKFIX_CREATEKEYWORD,
238235
]
239236
)
240237
@_logger.call
@@ -270,8 +267,6 @@ async def collect(
270267

271268
if isinstance(node, (KeywordCall, Fixture, TestTemplate, Template)):
272269
# only source actions
273-
if range.start != range.end:
274-
return None
275270

276271
result = await self.get_keyworddoc_and_token_from_position(
277272
node.value
@@ -285,17 +280,8 @@ async def collect(
285280
range.start,
286281
)
287282

288-
if result is None and (
289-
(context.only and CodeActionKinds.QUICKFIX in context.only)
290-
or context.trigger_kind == CodeActionTriggerKind.AUTOMATIC
291-
):
292-
return [
293-
CodeAction(
294-
"Create Keyword",
295-
kind=CodeActionKinds.QUICKFIX + ".createKeyword",
296-
command=Command("Create Keyword", self.parent.commands.get_command_name(self.comming_soon)),
297-
)
298-
]
283+
if range.start != range.end:
284+
return None
299285

300286
if result is not None:
301287
kw_doc, _ = result
@@ -409,7 +395,3 @@ async def _convert_uri(self, uri: str, *args: Any, **kwargs: Any) -> Optional[st
409395
return f"http://localhost:{self._documentation_server_port}/{path.as_posix()}"
410396

411397
return None
412-
413-
@command("robotcode.commingSoon")
414-
async def comming_soon(self) -> None:
415-
self.parent.window.show_message("Comming soon... stay tuned ...")
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
from __future__ import annotations
2+
3+
import ast
4+
from typing import TYPE_CHECKING, Any, List, Optional, Union, cast
5+
6+
from ....utils.logging import LoggingDescriptor
7+
from ...common.decorators import code_action_kinds, command, language_id
8+
from ...common.lsp_types import (
9+
AnnotatedTextEdit,
10+
ChangeAnnotation,
11+
CodeAction,
12+
CodeActionContext,
13+
CodeActionKinds,
14+
CodeActionTriggerKind,
15+
Command,
16+
DocumentUri,
17+
OptionalVersionedTextDocumentIdentifier,
18+
Position,
19+
Range,
20+
TextDocumentEdit,
21+
WorkspaceEdit,
22+
)
23+
from ...common.text_document import TextDocument
24+
from ..utils.ast_utils import Token, get_node_at_position, range_from_node
25+
from ..utils.async_ast import AsyncVisitor
26+
from .model_helper import ModelHelperMixin
27+
28+
if TYPE_CHECKING:
29+
from ..protocol import RobotLanguageServerProtocol # pragma: no cover
30+
31+
from string import Template
32+
33+
from .protocol_part import RobotLanguageServerProtocolPart
34+
35+
CODEACTIONKINDS_QUICKFIX_CREATEKEYWORD = f"{CodeActionKinds.QUICKFIX}.createKeyword"
36+
37+
38+
KEYWORD_WITH_ARGS_TEMPLATE = Template(
39+
"""\n
40+
${name}
41+
[Arguments] ${args}
42+
Fail Not implemented
43+
"""
44+
)
45+
46+
KEYWORD_TEMPLATE = Template(
47+
"""\n
48+
${name}
49+
Fail Not implemented
50+
"""
51+
)
52+
53+
54+
class FindKeywordSectionVisitor(AsyncVisitor):
55+
def __init__(self) -> None:
56+
self.keyword_sections: List[ast.AST] = []
57+
58+
async def visit_KeywordSection(self, node: ast.AST) -> None: # noqa: N802
59+
self.keyword_sections.append(node)
60+
61+
62+
async def find_keyword_sections(node: ast.AST) -> Optional[List[ast.AST]]:
63+
visitor = FindKeywordSectionVisitor()
64+
await visitor.visit(node)
65+
return visitor.keyword_sections if visitor.keyword_sections else None
66+
67+
68+
class RobotCodeActionFixesProtocolPart(RobotLanguageServerProtocolPart, ModelHelperMixin):
69+
_logger = LoggingDescriptor()
70+
71+
def __init__(self, parent: RobotLanguageServerProtocol) -> None:
72+
super().__init__(parent)
73+
74+
parent.code_action.collect.add(self.collect)
75+
76+
self.parent.commands.register_all(self)
77+
78+
@language_id("robotframework")
79+
@code_action_kinds(
80+
[
81+
CODEACTIONKINDS_QUICKFIX_CREATEKEYWORD,
82+
]
83+
)
84+
@_logger.call
85+
async def collect(
86+
self, sender: Any, document: TextDocument, range: Range, context: CodeActionContext
87+
) -> Optional[List[Union[Command, CodeAction]]]:
88+
89+
kw_not_found_in_diagnostics = next((d for d in context.diagnostics if d.code == "KeywordNotFoundError"), None)
90+
91+
if kw_not_found_in_diagnostics and (
92+
(context.only and CodeActionKinds.QUICKFIX in context.only)
93+
or context.trigger_kind in [CodeActionTriggerKind.INVOKED, CodeActionTriggerKind.AUTOMATIC]
94+
):
95+
return [
96+
CodeAction(
97+
"Create Keyword",
98+
kind=CodeActionKinds.QUICKFIX + ".createKeyword",
99+
command=Command(
100+
"Create Keyword",
101+
self.parent.commands.get_command_name(self.create_keyword),
102+
[document.document_uri, range, context],
103+
),
104+
diagnostics=[kw_not_found_in_diagnostics],
105+
)
106+
]
107+
108+
return None
109+
110+
@command("robotcode.createKeyword")
111+
async def create_keyword(self, document_uri: DocumentUri, range: Range, context: CodeActionContext) -> None:
112+
from robot.parsing.lexer import Token as RobotToken
113+
from robot.parsing.model.statements import (
114+
Fixture,
115+
KeywordCall,
116+
Template,
117+
TestTemplate,
118+
)
119+
from robot.utils.escaping import split_from_equals
120+
121+
document = await self.parent.documents.get(document_uri)
122+
if document is None:
123+
return
124+
125+
namespace = await self.parent.documents_cache.get_namespace(document)
126+
if namespace is None:
127+
return None
128+
129+
model = await self.parent.documents_cache.get_model(document, False)
130+
node = await get_node_at_position(model, range.start)
131+
132+
if isinstance(node, (KeywordCall, Fixture, TestTemplate, Template)):
133+
keyword = (
134+
node.value
135+
if isinstance(node, (TestTemplate, Template))
136+
else node.keyword
137+
if isinstance(node, KeywordCall)
138+
else node.name
139+
)
140+
141+
arguments = []
142+
143+
for t in node.get_tokens(RobotToken.ARGUMENT):
144+
name, value = split_from_equals(cast(Token, t).value)
145+
if value is not None:
146+
arguments.append(f"${{{name}}}")
147+
else:
148+
arguments.append(f"${{arg{len(arguments)+1}}}")
149+
150+
insert_text = (
151+
KEYWORD_WITH_ARGS_TEMPLATE.substitute(name=keyword, args=" ".join(arguments))
152+
if arguments
153+
else KEYWORD_TEMPLATE.substitute(name=keyword)
154+
)
155+
156+
keyword_sections = await find_keyword_sections(model)
157+
keyword_section = keyword_sections[-1] if keyword_sections else None
158+
159+
if keyword_section is not None:
160+
node_range = range_from_node(keyword_section)
161+
162+
insert_range = Range(node_range.end, node_range.end)
163+
else:
164+
insert_text = f"\n\n\n*** Keywords ***\n{insert_text}"
165+
doc_pos = Position(len(document.get_lines()), 0)
166+
insert_range = Range(doc_pos, doc_pos)
167+
168+
we = WorkspaceEdit(
169+
document_changes=[
170+
TextDocumentEdit(
171+
OptionalVersionedTextDocumentIdentifier(str(document.uri), document.version),
172+
[AnnotatedTextEdit(insert_range, insert_text, annotation_id="create_keyword")],
173+
)
174+
],
175+
change_annotations={"create_keyword": ChangeAnnotation("Create Keyword", False)},
176+
)
177+
178+
await self.parent.workspace.apply_edit(we, "Rename Keyword")

robotcode/language_server/robotframework/protocol.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
from ..common.parts.document_symbols import symbol_information_label
1717
from ..common.protocol import LanguageServerProtocol
1818
from .configuration import RobotConfig
19-
from .parts.code_action import RobotCodeActionProtocolPart
19+
from .parts.code_action_documentation import RobotCodeActionDocumentationProtocolPart
20+
from .parts.code_action_fixes import RobotCodeActionFixesProtocolPart
2021
from .parts.codelens import RobotCodeLensProtocolPart
2122
from .parts.completion import RobotCompletionProtocolPart
2223
from .parts.debugging_utils import RobotDebuggingUtilsProtocolPart
@@ -96,7 +97,8 @@ class RobotLanguageServerProtocol(LanguageServerProtocol):
9697
robot_rename = ProtocolPartDescriptor(RobotRenameProtocolPart)
9798
robot_inline_value = ProtocolPartDescriptor(RobotInlineValueProtocolPart)
9899
robot_inlay_hint = ProtocolPartDescriptor(RobotInlayHintProtocolPart)
99-
robot_code_action = ProtocolPartDescriptor(RobotCodeActionProtocolPart)
100+
robot_code_action_documentation = ProtocolPartDescriptor(RobotCodeActionDocumentationProtocolPart)
101+
robot_code_action_fixes = ProtocolPartDescriptor(RobotCodeActionFixesProtocolPart)
100102
robot_workspace = ProtocolPartDescriptor(RobotWorkspaceProtocolPart)
101103

102104
robot_discovering = ProtocolPartDescriptor(DiscoveringProtocolPart)

0 commit comments

Comments
 (0)