From 757908ad385d22d31c3dd69c6a898fcb50758571 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Ch=C5=82odnicki?= Date: Sat, 24 Jun 2023 22:46:54 +0200 Subject: [PATCH] refactor handling of custom editor commands (#2280) --- plugin/code_actions.py | 9 +++++--- plugin/core/registry.py | 7 +++---- plugin/core/sessions.py | 39 +++++++++++++++++++++++++++++------ plugin/core/views.py | 8 ++++--- plugin/documents.py | 2 +- plugin/execute_command.py | 21 ++++++------------- plugin/hover.py | 2 +- plugin/inlay_hint.py | 5 ++--- tests/setup.py | 2 +- tests/test_single_document.py | 3 ++- 10 files changed, 60 insertions(+), 38 deletions(-) diff --git a/plugin/code_actions.py b/plugin/code_actions.py index f616026e8..c55877346 100644 --- a/plugin/code_actions.py +++ b/plugin/code_actions.py @@ -253,11 +253,14 @@ def _request_code_actions_async(self) -> None: def _handle_response_async(self, responses: List[CodeActionsByConfigName]) -> None: if self._cancelled: return + view = self._task_runner.view tasks = [] # type: List[Promise] for config_name, code_actions in responses: session = self._task_runner.session_by_name(config_name, 'codeActionProvider') if session: - tasks.extend([session.run_code_action_async(action, progress=False) for action in code_actions]) + tasks.extend([ + session.run_code_action_async(action, progress=False, view=view) for action in code_actions + ]) Promise.all(tasks).then(lambda _: self._on_complete()) @@ -337,7 +340,7 @@ def run_async() -> None: config_name, action = actions[index] session = self.session_by_name(config_name) if session: - session.run_code_action_async(action, progress=True) \ + session.run_code_action_async(action, progress=True, view=self.view) \ .then(lambda response: self._handle_response_async(config_name, response)) sublime.set_timeout_async(run_async) @@ -401,7 +404,7 @@ def run_async(self, id: int, event: Optional[dict]) -> None: config_name, action = self.actions_cache[id] session = self.session_by_name(config_name) if session: - session.run_code_action_async(action, progress=True) \ + session.run_code_action_async(action, progress=True, view=self.view) \ .then(lambda response: self._handle_response_async(config_name, response)) def _handle_response_async(self, session_name: str, response: Any) -> None: diff --git a/plugin/core/registry.py b/plugin/core/registry.py index 8beb16e2a..f07491bc5 100644 --- a/plugin/core/registry.py +++ b/plugin/core/registry.py @@ -1,7 +1,6 @@ from .protocol import Diagnostic from .protocol import Location from .protocol import LocationLink -from .protocol import Point from .sessions import AbstractViewListener from .sessions import Session from .tree_view import TreeDataProvider @@ -10,7 +9,7 @@ from .views import first_selection_region from .views import get_uri_and_position_from_location from .views import MissingUriError -from .views import point_to_offset +from .views import position_to_offset from .views import uri_from_view from .windows import WindowManager from .windows import WindowRegistry @@ -299,11 +298,11 @@ def navigate_diagnostics(view: sublime.View, point: Optional[int], forward: bool # this view after/before the cursor op_func = operator.gt if forward else operator.lt for diagnostic in diagnostics: - diag_pos = point_to_offset(Point.from_lsp(diagnostic['range']['start']), view) + diag_pos = position_to_offset(diagnostic['range']['start'], view) if op_func(diag_pos, point): break else: - diag_pos = point_to_offset(Point.from_lsp(diagnostics[0]['range']['start']), view) + diag_pos = position_to_offset(diagnostics[0]['range']['start'], view) view.run_command('lsp_selection_set', {'regions': [(diag_pos, diag_pos)]}) view.show_at_center(diag_pos) # We need a small delay before showing the popup to wait for the scrolling animation to finish. Otherwise ST would diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index fcf2b3150..42c57e5b6 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -1548,13 +1548,34 @@ def _template_variables(self) -> Dict[str, str]: variables.update(extra_vars) return variables - def execute_command(self, command: ExecuteCommandParams, progress: bool) -> Promise: + def execute_command( + self, command: ExecuteCommandParams, progress: bool, view: Optional[sublime.View] = None + ) -> Promise: """Run a command from any thread. Your .then() continuations will run in Sublime's worker thread.""" if self._plugin: task = Promise.packaged_task() # type: PackagedTask[None] promise, resolve = task if self._plugin.on_pre_server_command(command, lambda: resolve(None)): return promise + command_name = command['command'] + # Handle VSCode-specific command for triggering AC/sighelp + if command_name == "editor.action.triggerSuggest" and view: + # Triggered from set_timeout as suggestions popup doesn't trigger otherwise. + sublime.set_timeout(lambda: view.run_command("auto_complete")) + return Promise.resolve(None) + if command_name == "editor.action.triggerParameterHints" and view: + + def run_async() -> None: + session_view = self.session_view_for_view_async(view) + if not session_view: + return + listener = session_view.listener() + if not listener: + return + listener.do_signature_help_async(manual=False) + + sublime.set_timeout_async(run_async) + return Promise.resolve(None) # TODO: Our Promise class should be able to handle errors/exceptions return Promise( lambda resolve: self.send_request( @@ -1564,7 +1585,9 @@ def execute_command(self, command: ExecuteCommandParams, progress: bool) -> Prom ) ) - def run_code_action_async(self, code_action: Union[Command, CodeAction], progress: bool) -> Promise: + def run_code_action_async( + self, code_action: Union[Command, CodeAction], progress: bool, view: Optional[sublime.View] = None + ) -> Promise: command = code_action.get("command") if isinstance(command, str): code_action = cast(Command, code_action) @@ -1573,12 +1596,13 @@ def run_code_action_async(self, code_action: Union[Command, CodeAction], progres arguments = code_action.get('arguments', None) if isinstance(arguments, list): command_params['arguments'] = arguments - return self.execute_command(command_params, progress) + return self.execute_command(command_params, progress, view) # At this point it cannot be a command anymore, it has to be a proper code action. # A code action can have an edit and/or command. Note that it can have *both*. In case both are present, we # must apply the edits before running the command. code_action = cast(CodeAction, code_action) - return self._maybe_resolve_code_action(code_action).then(self._apply_code_action_async) + return self._maybe_resolve_code_action(code_action) \ + .then(lambda code_action: self._apply_code_action_async(code_action, view)) def open_uri_async( self, @@ -1670,7 +1694,9 @@ def _maybe_resolve_code_action(self, code_action: CodeAction) -> Promise[Union[C return self.send_request_task(request) return Promise.resolve(code_action) - def _apply_code_action_async(self, code_action: Union[CodeAction, Error, None]) -> Promise[None]: + def _apply_code_action_async( + self, code_action: Union[CodeAction, Error, None], view: Optional[sublime.View] + ) -> Promise[None]: if not code_action: return Promise.resolve(None) if isinstance(code_action, Error): @@ -1687,7 +1713,8 @@ def _apply_code_action_async(self, code_action: Union[CodeAction, Error, None]) arguments = command.get("arguments") if arguments is not None: execute_command['arguments'] = arguments - return promise.then(lambda _: self.execute_command(execute_command, False)) + return promise.then( + lambda _: self.execute_command(execute_command, progress=False, view=view)) return promise def apply_workspace_edit_async(self, edit: WorkspaceEdit) -> Promise[None]: diff --git a/plugin/core/views.py b/plugin/core/views.py index 0aa449069..ba1bcdd31 100644 --- a/plugin/core/views.py +++ b/plugin/core/views.py @@ -351,6 +351,10 @@ def position(view: sublime.View, offset: int) -> Position: return offset_to_point(view, offset).to_lsp() +def position_to_offset(position: Position, view: sublime.View) -> int: + return point_to_offset(Point.from_lsp(position), view) + + def get_symbol_kind_from_scope(scope_name: str) -> SublimeKind: best_kind = sublime.KIND_AMBIGUOUS best_kind_score = 0 @@ -363,9 +367,7 @@ def get_symbol_kind_from_scope(scope_name: str) -> SublimeKind: def range_to_region(range: Range, view: sublime.View) -> sublime.Region: - start = Point.from_lsp(range['start']) - end = Point.from_lsp(range['end']) - return sublime.Region(point_to_offset(start, view), point_to_offset(end, view)) + return sublime.Region(position_to_offset(range['start'], view), position_to_offset(range['end'], view)) def region_to_range(view: sublime.View, region: sublime.Region) -> Range: diff --git a/plugin/documents.py b/plugin/documents.py index e13130bc2..fa0e5869e 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -693,7 +693,7 @@ def handle_code_action_select(self, config_name: str, actions: List[CodeActionOr def run_async() -> None: session = self.session_by_name(config_name) if session: - session.run_code_action_async(actions[index], progress=True) + session.run_code_action_async(actions[index], progress=True, view=self.view) sublime.set_timeout_async(run_async) diff --git a/plugin/execute_command.py b/plugin/execute_command.py index d44fb9b96..cfad5d132 100644 --- a/plugin/execute_command.py +++ b/plugin/execute_command.py @@ -1,10 +1,13 @@ from .core.protocol import Error from .core.protocol import ExecuteCommandParams from .core.registry import LspTextCommand -from .core.registry import windows from .core.typing import List, Optional, Any from .core.views import first_selection_region -from .core.views import uri_from_view, offset_to_point, region_to_range, text_document_identifier, text_document_position_params # noqa: E501 +from .core.views import offset_to_point +from .core.views import region_to_range +from .core.views import text_document_identifier +from .core.views import text_document_position_params +from .core.views import uri_from_view import sublime @@ -19,18 +22,6 @@ def run(self, command_args: Optional[List[Any]] = None, session_name: Optional[str] = None, event: Optional[dict] = None) -> None: - # Handle VSCode-specific command for triggering AC/sighelp - if command_name == "editor.action.triggerSuggest": - # Triggered from set_timeout as suggestions popup doesn't trigger otherwise. - return sublime.set_timeout(lambda: self.view.run_command("auto_complete")) - if command_name == "editor.action.triggerParameterHints": - - def run_async() -> None: - listener = windows.listener_for_view(self.view) - if listener: - listener.do_signature_help_async(manual=False) - - return sublime.set_timeout_async(run_async) session = self.session_by_name(session_name if session_name else self.session_name) if session and command_name: params = {"command": command_name} # type: ExecuteCommandParams @@ -44,7 +35,7 @@ def handle_response(response: Any) -> None: return self.handle_success_async(response, command_name) - session.execute_command(params, progress=True).then(handle_response) + session.execute_command(params, progress=True, view=self.view).then(handle_response) def handle_success_async(self, result: Any, command_name: str) -> None: """ diff --git a/plugin/hover.py b/plugin/hover.py index c64cba959..e0b1257f2 100644 --- a/plugin/hover.py +++ b/plugin/hover.py @@ -363,6 +363,6 @@ def handle_code_action_select(self, config_name: str, actions: List[CodeActionOr def run_async() -> None: session = self.session_by_name(config_name) if session: - session.run_code_action_async(actions[index], progress=True) + session.run_code_action_async(actions[index], progress=True, view=self.view) sublime.set_timeout_async(run_async) diff --git a/plugin/inlay_hint.py b/plugin/inlay_hint.py index 0cfe58837..7eecf46a0 100644 --- a/plugin/inlay_hint.py +++ b/plugin/inlay_hint.py @@ -2,14 +2,13 @@ from .core.protocol import InlayHint from .core.protocol import InlayHintLabelPart from .core.protocol import MarkupContent -from .core.protocol import Point from .core.protocol import Request from .core.registry import LspTextCommand from .core.registry import LspWindowCommand from .core.sessions import Session from .core.settings import userprefs from .core.typing import cast, Optional, Union -from .core.views import point_to_offset +from .core.views import position_to_offset from .formatting import apply_text_edits_to_view import html import sublime @@ -88,7 +87,7 @@ def handle_label_part_command(self, session_name: str, label_part: Optional[Inla def inlay_hint_to_phantom(view: sublime.View, inlay_hint: InlayHint, session: Session) -> sublime.Phantom: position = inlay_hint["position"] - region = sublime.Region(point_to_offset(Point.from_lsp(position), view)) + region = sublime.Region(position_to_offset(position, view)) phantom_uuid = str(uuid.uuid4()) content = get_inlay_hint_html(view, inlay_hint, session, phantom_uuid) p = sublime.Phantom(region, content, sublime.LAYOUT_INLINE) diff --git a/tests/setup.py b/tests/setup.py index a3f6b8d14..b72f5c134 100644 --- a/tests/setup.py +++ b/tests/setup.py @@ -210,7 +210,7 @@ def await_promise(cls, promise: Union[YieldPromise, Promise]) -> Generator: def await_run_code_action(self, code_action: Dict[str, Any]) -> Generator: promise = YieldPromise() sublime.set_timeout_async( - lambda: self.session.run_code_action_async(code_action, progress=False).then( + lambda: self.session.run_code_action_async(code_action, progress=False, view=self.view).then( promise.fulfill)) yield from self.await_promise(promise) diff --git a/tests/test_single_document.py b/tests/test_single_document.py index 5e32b87a5..50a45a06f 100644 --- a/tests/test_single_document.py +++ b/tests/test_single_document.py @@ -347,7 +347,8 @@ def test_run_command(self) -> 'Generator': sublime.set_timeout_async( lambda: self.session.execute_command( {"command": "foo", "arguments": ["hello", "there", "general", "kenobi"]}, - progress=False + progress=False, + view=self.view, ).then(promise.fulfill) ) yield from self.await_promise(promise)