Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "Source Action" entry to the "Edit" main menu #2149

Merged
merged 3 commits into from
Dec 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 57 additions & 7 deletions Main.sublime-menu
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down Expand Up @@ -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"
}
]
}
]
},
Expand Down
1 change: 1 addition & 0 deletions boot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
94 changes: 61 additions & 33 deletions plugin/code_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -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]
Expand All @@ -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.
Expand Down Expand Up @@ -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) \
Expand All @@ -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