Skip to content

Commit 1ae9221

Browse files
committed
implement command registration and execution
1 parent 2722870 commit 1ae9221

File tree

6 files changed

+226
-3
lines changed

6 files changed

+226
-3
lines changed

robotcode/language_server/common/decorators.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,23 @@ def filter(c: Any) -> bool:
7979
)
8080

8181
return filter
82+
83+
84+
@runtime_checkable
85+
class IsCommand(Protocol):
86+
__command_name__: List[str]
87+
88+
89+
def command(name: str) -> Callable[[_F], _F]:
90+
def decorator(func: _F) -> _F:
91+
setattr(func, "__command_name__", name)
92+
return func
93+
94+
return decorator
95+
96+
97+
def get_command_name(func: _F) -> str:
98+
if isinstance(func, IsCommand):
99+
return func.__command_name__
100+
101+
raise TypeError(f"{func} is not a command.")

robotcode/language_server/common/lsp_types.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
ProgressToken = Union[str, int]
1414

1515

16+
LSPAny = Any
17+
18+
1619
class ErrorCodes:
1720
PARSE_ERROR = -32700
1821
INVALID_REQUEST = -32600
@@ -981,6 +984,22 @@ class ExecuteCommandOptions(WorkDoneProgressOptions, _ExecuteCommandOptions):
981984
pass
982985

983986

987+
@dataclass(repr=False)
988+
class ExecuteCommandRegistrationOptions(ExecuteCommandOptions):
989+
pass
990+
991+
992+
@dataclass(repr=False)
993+
class _ExecuteCommandParams(Model):
994+
command: str
995+
arguments: Optional[List[LSPAny]] = None
996+
997+
998+
@dataclass(repr=False)
999+
class ExecuteCommandParams(WorkDoneProgressParams, _ExecuteCommandParams):
1000+
pass
1001+
1002+
9841003
@dataclass(repr=False)
9851004
class SemanticTokensLegend(Model):
9861005
token_types: List[str]
@@ -2368,3 +2387,16 @@ class CodeAction:
23682387
edit: Optional[WorkspaceEdit] = None
23692388
command: Optional[Command] = None
23702389
data: Any = None
2390+
2391+
2392+
@dataclass(repr=False)
2393+
class ApplyWorkspaceEditParams(Model):
2394+
edit: WorkspaceEdit
2395+
label: Optional[str] = None
2396+
2397+
2398+
@dataclass(repr=False)
2399+
class ApplyWorkspaceEditResult(Model):
2400+
applied: bool
2401+
failure_reason: Optional[str] = None
2402+
failed_change: Optional[int] = None
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, List, Optional
4+
5+
from ....jsonrpc2.protocol import JsonRPCErrorException, rpc_method
6+
from ....utils.async_tools import threaded
7+
from ....utils.logging import LoggingDescriptor
8+
from ..decorators import get_command_name
9+
from ..has_extend_capabilities import HasExtendCapabilities
10+
from ..lsp_types import (
11+
ErrorCodes,
12+
ExecuteCommandOptions,
13+
ExecuteCommandParams,
14+
LSPAny,
15+
ServerCapabilities,
16+
)
17+
18+
if TYPE_CHECKING:
19+
from ..protocol import LanguageServerProtocol
20+
21+
from .protocol_part import LanguageServerProtocolPart
22+
23+
_FUNC_TYPE = Callable[..., Awaitable[Optional[LSPAny]]]
24+
25+
26+
class CommandsProtocolPart(LanguageServerProtocolPart, HasExtendCapabilities):
27+
28+
_logger = LoggingDescriptor()
29+
30+
def __init__(self, parent: LanguageServerProtocol) -> None:
31+
super().__init__(parent)
32+
self.commands: Dict[str, _FUNC_TYPE] = {}
33+
34+
def register(self, callback: _FUNC_TYPE, name: Optional[str] = None) -> None:
35+
command = name or get_command_name(callback)
36+
37+
if command in self.commands:
38+
self._logger.critical(f"command '{command}' already registered.")
39+
return
40+
41+
self.commands[command] = callback
42+
43+
def extend_capabilities(self, capabilities: ServerCapabilities) -> None:
44+
45+
capabilities.execute_command_provider = ExecuteCommandOptions(list(self.commands.keys()))
46+
47+
@rpc_method(name="workspace/executeCommand", param_type=ExecuteCommandParams)
48+
@threaded()
49+
async def _text_document_code_lens(
50+
self, command: str, arguments: Optional[List[LSPAny]], *args: Any, **kwargs: Any
51+
) -> Optional[LSPAny]:
52+
self._logger.info(f"execute command {command}")
53+
54+
callback = self.commands.get(command, None)
55+
if callback is None:
56+
raise JsonRPCErrorException(ErrorCodes.INVALID_PARAMS, f"Command '{command}' unknown.")
57+
58+
return await callback(*(arguments or ()))

robotcode/language_server/common/parts/workspace.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
from ....utils.uri import Uri
3737
from ...common.has_extend_capabilities import HasExtendCapabilities
3838
from ..lsp_types import (
39+
ApplyWorkspaceEditParams,
40+
ApplyWorkspaceEditResult,
3941
ConfigurationItem,
4042
ConfigurationParams,
4143
CreateFilesParams,
@@ -482,3 +484,12 @@ async def remove_file_watcher_entry(self, entry: FileWatcherEntry) -> None:
482484
):
483485
await self.parent.unregister_capability(entry.id, "workspace/didChangeWatchedFiles")
484486
# TODO: implement own filewatcher if not supported by language server client
487+
488+
async def apply_edit(self, edit: WorkspaceEdit, label: Optional[str] = None) -> ApplyWorkspaceEditResult:
489+
r = await self.parent.send_request_async(
490+
"workspace/applyEdit", ApplyWorkspaceEditParams(edit, label), return_type=ApplyWorkspaceEditResult
491+
)
492+
493+
assert r is not None
494+
495+
return r

robotcode/language_server/common/protocol.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
)
4141
from .parts.code_action import CodeActionProtocolPart
4242
from .parts.code_lens import CodeLensProtocolPart
43+
from .parts.commands import CommandsProtocolPart
4344
from .parts.completion import CompletionProtocolPart
4445
from .parts.declaration import DeclarationProtocolPart
4546
from .parts.definition import DefinitionProtocolPart
@@ -72,6 +73,7 @@ class LanguageServerProtocol(JsonRPCProtocol):
7273

7374
_logger = LoggingDescriptor()
7475

76+
commands = ProtocolPartDescriptor(CommandsProtocolPart)
7577
window = ProtocolPartDescriptor(WindowProtocolPart)
7678
documents = ProtocolPartDescriptor(TextDocumentProtocolPart)
7779
diagnostics = ProtocolPartDescriptor(DiagnosticsProtocolPart)

robotcode/language_server/robotframework/parts/code_action.py

Lines changed: 103 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,28 @@
1717
from ....utils.logging import LoggingDescriptor
1818
from ....utils.net import find_free_port
1919
from ....utils.uri import Uri
20-
from ...common.decorators import code_action_kinds, language_id
20+
from ...common.decorators import (
21+
code_action_kinds,
22+
command,
23+
get_command_name,
24+
language_id,
25+
)
2126
from ...common.lsp_types import (
27+
AnnotatedTextEdit,
28+
ChangeAnnotation,
2229
CodeAction,
2330
CodeActionContext,
2431
CodeActionKinds,
2532
Command,
33+
CreateFile,
34+
DeleteFile,
35+
MessageType,
2636
Model,
37+
OptionalVersionedTextDocumentIdentifier,
2738
Range,
39+
RenameFile,
40+
TextDocumentEdit,
41+
WorkspaceEdit,
2842
)
2943
from ...common.text_document import TextDocument
3044
from ..configuration import DocumentationServerConfig
@@ -186,6 +200,8 @@ def __init__(self, parent: RobotLanguageServerProtocol) -> None:
186200
self._documentation_server_lock = threading.RLock()
187201
self._documentation_server_port = 0
188202

203+
self.parent.commands.register(self.translate_suite)
204+
189205
async def initialized(self, sender: Any) -> None:
190206
await self._ensure_http_server_started()
191207

@@ -265,11 +281,11 @@ async def collect(
265281

266282
return [
267283
CodeAction(
268-
f"Translate Suite to {language.name}",
284+
f"Translate file to `{language.name}`",
269285
kind=CodeActionKinds.SOURCE + ".openDocumentation",
270286
command=Command(
271287
f"Translate Suite to {lang}",
272-
"robotcode.translateSuite",
288+
get_command_name(self.translate_suite),
273289
[document.document_uri, lang],
274290
),
275291
)
@@ -394,3 +410,87 @@ async def _convert_uri(self, uri: str, *args: Any, **kwargs: Any) -> Optional[st
394410
return f"http://localhost:{self._documentation_server_port}/{path.as_posix()}"
395411

396412
return None
413+
414+
@command("robotcode.translateSuite")
415+
async def translate_suite(self, document_uri: str, lang: str) -> None:
416+
from robot.conf.languages import Language
417+
from robot.parsing.lexer.tokens import Token as RobotToken
418+
419+
try:
420+
language = Language.from_name(lang)
421+
except ValueError:
422+
self.parent.window.show_message(f"Invalid language {lang}", MessageType.ERROR)
423+
return
424+
425+
document = await self.parent.documents.get(document_uri)
426+
427+
if document is None:
428+
return
429+
430+
changes: List[Union[TextDocumentEdit, CreateFile, RenameFile, DeleteFile]] = []
431+
432+
header_translations = {
433+
RobotToken.SETTING_HEADER: language.settings_header,
434+
RobotToken.VARIABLE_HEADER: language.variables_header,
435+
RobotToken.TESTCASE_HEADER: language.test_cases_header,
436+
RobotToken.TASK_HEADER: language.tasks_header,
437+
RobotToken.KEYWORD_HEADER: language.keywords_header,
438+
RobotToken.COMMENT_HEADER: language.comments_header,
439+
}
440+
settings_translations = {
441+
RobotToken.LIBRARY: language.library_setting,
442+
RobotToken.DOCUMENTATION: language.documentation_setting,
443+
RobotToken.SUITE_SETUP: language.suite_setup_setting,
444+
RobotToken.SUITE_TEARDOWN: language.suite_teardown_setting,
445+
RobotToken.METADATA: language.metadata_setting,
446+
RobotToken.KEYWORD_TAGS: language.keyword_tags_setting,
447+
RobotToken.LIBRARY: language.library_setting,
448+
RobotToken.RESOURCE: language.resource_setting,
449+
RobotToken.VARIABLES: language.variables_setting,
450+
RobotToken.SETUP: f"[{language.setup_setting}]",
451+
RobotToken.TEARDOWN: f"[{language.teardown_setting}]",
452+
RobotToken.TEMPLATE: f"[{language.template_setting}]",
453+
RobotToken.TIMEOUT: f"[{language.timeout_setting}]",
454+
RobotToken.TAGS: f"[{language.tags_setting}]",
455+
RobotToken.ARGUMENTS: f"[{language.arguments_setting}]",
456+
}
457+
458+
for token in await self.parent.documents_cache.get_tokens(document):
459+
if token.type in header_translations.keys():
460+
changes.append(
461+
TextDocumentEdit(
462+
OptionalVersionedTextDocumentIdentifier(str(document.uri), document.version),
463+
[
464+
AnnotatedTextEdit(
465+
range_from_token(token),
466+
f"*** { header_translations[token.type]} ***",
467+
annotation_id="translate_settings",
468+
)
469+
],
470+
)
471+
)
472+
elif token.type in settings_translations.keys():
473+
changes.append(
474+
TextDocumentEdit(
475+
OptionalVersionedTextDocumentIdentifier(str(document.uri), document.version),
476+
[
477+
AnnotatedTextEdit(
478+
range_from_token(token),
479+
settings_translations[token.type],
480+
annotation_id="translate_settings",
481+
)
482+
],
483+
)
484+
)
485+
else:
486+
pass
487+
488+
if not changes:
489+
return
490+
491+
edit = WorkspaceEdit(
492+
document_changes=changes,
493+
change_annotations={"translate_settings": ChangeAnnotation("Translate Settings", False)},
494+
)
495+
496+
await self.parent.workspace.apply_edit(edit, "Translate")

0 commit comments

Comments
 (0)