From eff29c9dd84d20c53730d7fd8a655c76f355d49c Mon Sep 17 00:00:00 2001 From: jwortmann Date: Tue, 27 Dec 2022 11:50:22 +0100 Subject: [PATCH] Add "Source Action" entry to the "Edit" main menu (#2149) --- Main.sublime-menu | 64 ++++++++++++++++++++++++---- boot.py | 1 + plugin/code_actions.py | 94 +++++++++++++++++++++++++++--------------- 3 files changed, 119 insertions(+), 40 deletions(-) diff --git a/Main.sublime-menu b/Main.sublime-menu index 9b65e5292..f1976a033 100644 --- a/Main.sublime-menu +++ b/Main.sublime-menu @@ -7,9 +7,54 @@ "caption": "-" }, { - "command": "lsp_refactor", + "command": "lsp_source_action", "args": {"id": -1} }, + { + "caption": "LSP: Source Action", + "children": [ + { + "command": "lsp_source_action", + "args": {"id": 0} + }, + { + "command": "lsp_source_action", + "args": {"id": 1} + }, + { + "command": "lsp_source_action", + "args": {"id": 2} + }, + { + "command": "lsp_source_action", + "args": {"id": 3} + }, + { + "command": "lsp_source_action", + "args": {"id": 4} + }, + { + "command": "lsp_source_action", + "args": {"id": 5} + }, + { + "command": "lsp_source_action", + "args": {"id": 6} + }, + { + "command": "lsp_source_action", + "args": {"id": 7} + }, + { + "command": "lsp_source_action", + "args": {"id": 8} + }, + { + "command": "lsp_source_action", + "args": {"id": 9} + } + ] + }, { "caption": "LSP: Refactor", "children": [ @@ -60,12 +105,17 @@ ] }, { - "caption": "LSP: Format File", - "command": "lsp_format_document" - }, - { - "caption": "LSP: Format Selection", - "command": "lsp_format_document_range" + "caption": "LSP: Format", + "children": [ + { + "caption": "Format File", + "command": "lsp_format_document" + }, + { + "caption": "Format Selection", + "command": "lsp_format_document_range" + } + ] } ] }, diff --git a/boot.py b/boot.py index 16d875eba..1e5798b71 100644 --- a/boot.py +++ b/boot.py @@ -5,6 +5,7 @@ # Please keep this list sorted (Edit -> Sort Lines) from .plugin.code_actions import LspCodeActionsCommand from .plugin.code_actions import LspRefactorCommand +from .plugin.code_actions import LspSourceActionCommand from .plugin.code_lens import LspCodeLensCommand from .plugin.color import LspColorPresentationCommand from .plugin.completion import LspCommitCompletionWithOppositeInsertMode diff --git a/plugin/code_actions.py b/plugin/code_actions.py index 192fe5b58..a70609d0a 100644 --- a/plugin/code_actions.py +++ b/plugin/code_actions.py @@ -17,12 +17,15 @@ from .core.views import format_code_actions_for_quick_panel from .core.views import text_document_code_action_params from .save_command import LspSaveCommand, SaveTask +from abc import ABCMeta +from abc import abstractmethod from functools import partial import sublime ConfigName = str CodeActionOrCommand = Union[CodeAction, Command] CodeActionsByConfigName = Tuple[ConfigName, List[CodeActionOrCommand]] +MENU_ACTIONS_KINDS = [CodeActionKind.Refactor, CodeActionKind.Source] def is_command(action: CodeActionOrCommand) -> TypeGuard[Command]: @@ -34,8 +37,9 @@ class CodeActionsManager: def __init__(self) -> None: self._response_cache = None # type: Optional[Tuple[str, Promise[List[CodeActionsByConfigName]]]] - self.refactor_actions_key = None # type: Optional[str] + self.menu_actions_cache_key = None # type: Optional[str] self.refactor_actions_cache = [] # type: List[Tuple[str, CodeAction]] + self.source_actions_cache = [] # type: List[Tuple[str, CodeAction]] def request_for_region_async( self, @@ -51,7 +55,7 @@ def request_for_region_async( """ listener = windows.listener_for_view(view) if not listener: - self.refactor_actions_key = None + self.menu_actions_cache_key = None return Promise.resolve([]) location_cache_key = None use_cache = not manual @@ -63,9 +67,10 @@ def request_for_region_async( return task else: self._response_cache = None - elif only_kinds == [CodeActionKind.Refactor]: - self.refactor_actions_key = "{}#{}:{}".format(view.buffer_id(), view.change_count(), region) + elif only_kinds == MENU_ACTIONS_KINDS: + self.menu_actions_cache_key = "{}#{}:{}".format(view.buffer_id(), view.change_count(), region) self.refactor_actions_cache.clear() + self.source_actions_cache.clear() def request_factory(session: Session) -> Optional[Request]: diagnostics = [] # type: List[Diagnostic] @@ -79,16 +84,15 @@ def request_factory(session: Session) -> Optional[Request]: def response_filter(session: Session, actions: List[CodeActionOrCommand]) -> List[CodeActionOrCommand]: # Filter out non "quickfix" code actions unless "only_kinds" is provided. if only_kinds: - if manual and only_kinds == [CodeActionKind.Refactor]: - self.refactor_actions_cache.extend([ - (session.config.name, cast(CodeAction, a)) for a in actions - if not is_command(a) and kinds_include_kind([CodeActionKind.Refactor], a.get('kind')) and - not a.get('disabled') - ]) - return [ - a for a in actions - if not is_command(a) and kinds_include_kind(only_kinds, a.get('kind')) and not a.get('disabled') - ] + code_actions = [cast(CodeAction, a) for a in actions if not is_command(a) and not a.get('disabled')] + if manual and only_kinds == MENU_ACTIONS_KINDS: + for action in code_actions: + kind = action.get('kind') + if kinds_include_kind([CodeActionKind.Refactor], kind): + self.refactor_actions_cache.append((session.config.name, action)) + elif kinds_include_kind([CodeActionKind.Source], kind): + self.source_actions_cache.append((session.config.name, action)) + return [action for action in code_actions if kinds_include_kind(only_kinds, action.get('kind'))] if manual: return [a for a in actions if not a.get('disabled')] # On implicit (selection change) request, only return commands and quick fix kinds. @@ -348,32 +352,38 @@ def _handle_response_async(self, session_name: str, response: Any) -> None: sublime.error_message("{}: {}".format(session_name, str(response))) -class LspRefactorCommand(LspTextCommand): +class LspMenuActionCommand(LspTextCommand, metaclass=ABCMeta): + """Handles a particular kind of code actions with the purpose to list them as items in a submenu.""" capability = 'codeActionProvider' + @property + @abstractmethod + def actions_cache(self) -> List[Tuple[str, CodeAction]]: + ... + def is_enabled(self, id: int, event: Optional[dict] = None, point: Optional[int] = None) -> bool: if not super().is_enabled(event, point): return False - return -1 < id < len(actions_manager.refactor_actions_cache) + return -1 < id < len(self.actions_cache) def is_visible(self, id: int, event: Optional[dict] = None, point: Optional[int] = None) -> bool: if id == -1: if super().is_enabled(event, point): - sublime.set_timeout_async(self._request_refactor_actions_async) + sublime.set_timeout_async(partial(self._request_menu_actions_async, event)) return False - return id < len(actions_manager.refactor_actions_cache) and self._is_cache_valid() + return id < len(self.actions_cache) and self._is_cache_valid(event) def description(self, id: int, event: Optional[dict] = None, point: Optional[int] = None) -> Optional[str]: - if -1 < id < len(actions_manager.refactor_actions_cache): - return actions_manager.refactor_actions_cache[id][1]['title'] + if -1 < id < len(self.actions_cache): + return self.actions_cache[id][1]['title'] def run(self, edit: sublime.Edit, id: int, event: Optional[dict] = None, point: Optional[int] = None) -> None: - sublime.set_timeout_async(partial(self.run_async, id)) + sublime.set_timeout_async(partial(self.run_async, id, event)) - def run_async(self, id: int) -> None: - if self._is_cache_valid(): - config_name, action = actions_manager.refactor_actions_cache[id] + def run_async(self, id: int, event: Optional[dict]) -> None: + if self._is_cache_valid(event): + config_name, action = self.actions_cache[id] session = self.session_by_name(config_name) if session: session.run_code_action_async(action, progress=True) \ @@ -383,15 +393,33 @@ def _handle_response_async(self, session_name: str, response: Any) -> None: if isinstance(response, Error): sublime.error_message("{}: {}".format(session_name, str(response))) - def _is_cache_valid(self) -> bool: - v = self.view - region = first_selection_region(v) + def _is_cache_valid(self, event: Optional[dict]) -> bool: + region = self._get_region(event) if region is None: return False - return actions_manager.refactor_actions_key == "{}#{}:{}".format(v.buffer_id(), v.change_count(), region) + v = self.view + return actions_manager.menu_actions_cache_key == "{}#{}:{}".format(v.buffer_id(), v.change_count(), region) - def _request_refactor_actions_async(self) -> None: - region = first_selection_region(self.view) - if region is None: - return - actions_manager.request_for_region_async(self.view, region, [], [CodeActionKind.Refactor], True) + def _get_region(self, event: Optional[dict]) -> Optional[sublime.Region]: + if event is not None and self.applies_to_context_menu(event): + return sublime.Region(self.view.window_to_text((event['x'], event['y']))) + return first_selection_region(self.view) + + def _request_menu_actions_async(self, event: Optional[dict]) -> None: + region = self._get_region(event) + if region is not None: + actions_manager.request_for_region_async(self.view, region, [], MENU_ACTIONS_KINDS, True) + + +class LspRefactorCommand(LspMenuActionCommand): + + @property + def actions_cache(self) -> List[Tuple[str, CodeAction]]: + return actions_manager.refactor_actions_cache + + +class LspSourceActionCommand(LspMenuActionCommand): + + @property + def actions_cache(self) -> List[Tuple[str, CodeAction]]: + return actions_manager.source_actions_cache