Repository: sublimelsp/LSP

Issue:
Split up Code Actions into "Refactors" and "Source Actions"
**Is your feature request related to a problem? Please describe.**

Currently we bin all code actions into "Code Actions", but servers like

- jdtls
- OmniSharp

have a clear distinction between "Refactorings" and "Source Actions".

**Describe the solution you'd like**

 In case of "Refactorings", these kinds should not always be directly visible to the user. Source Actions should be visible, like they are right now with annotations.

I think Refactorings should go into the "Edit" top menu to be selected with your mouse or in the command palette.


Discussion:
> Source Actions should be visible, like they are right now with annotations.

So you want to show them in the same way as normal code actions or in some other way? We already have a `Source action...` in the right click context menu. Showing those as annotations would not be correct IMO as those don't bind to specific line in the code.

Or even if they are specific to a selected line, they often would show up for every line in the document which wouldn't be good either.

I wonder what specific code actions are we talking here and how would you see them being handled?
Sorry for the late reply. I'm talking about refactorings.

<img width="1080" alt="Schermafbeelding 2022-02-27 om 11 26 25" src="https://user-images.githubusercontent.com/2431823/155878748-6c770f5f-3092-4287-95e1-54149f8910cd.png">

You can see below I get mostly refactoring kinds.

```
:: --> OmniSharp textDocument/codeAction(5): {'textDocument': {'uri': 'file:///Users/raoulwols/dev/csharp-project/Program.cs'}, 'context': {'diagnostics': []}, 'range': {'end': {'line': 14, 'character': 22}, 'start': {'line': 14, 'character': 22}}}
:: <<< OmniSharp 5: [{'diagnostics': [], 'data': {'$$__handler_id__$$': '971ea130-dc4d-4c43-9b50-74e6076b05e5'}, 'edit': {}, 'kind': 'refactor', 'title': 'Wrap every parameter -> Align wrapped parameters', 'command': {'arguments': [{'Uri': 'file:///Users/raoulwols/dev/csharp-project/Program.cs', 'Identifier': 'Align wrapped parameters', 'Name': 'Wrap every parameter -> Align wrapped parameters', 'Range': {'End': {'Line': 14, 'Character': 22}, 'Start': {'Line': 14, 'Character': 22}}}], 'command': 'omnisharp/executeCodeAction', 'title': 'Wrap every parameter -> Align wrapped parameters'}}, {'diagnostics': [], 'data': {'$$__handler_id__$$': '971ea130-dc4d-4c43-9b50-74e6076b05e5'}, 'edit': {}, 'kind': 'refactor', 'title': 'Wrap every parameter -> Indent all parameters', 'command': {'arguments': [{'Uri': 'file:///Users/raoulwols/dev/csharp-project/Program.cs', 'Identifier': 'Indent all parameters', 'Name': 'Wrap every parameter -> Indent all parameters', 'Range': {'End': {'Line': 14, 'Character': 22}, 'Start': {'Line': 14, 'Character': 22}}}], 'command': 'omnisharp/executeCodeAction', 'title': 'Wrap every parameter -> Indent all parameters'}}, {'diagnostics': [], 'data': {'$$__handler_id__$$': '971ea130-dc4d-4c43-9b50-74e6076b05e5'}, 'edit': {}, 'kind': 'refactor', 'title': 'Wrap every parameter -> Indent wrapped parameters', 'command': {'arguments': [{'Uri': 'file:///Users/raoulwols/dev/csharp-project/Program.cs', 'Identifier': 'Indent wrapped parameters', 'Name': 'Wrap every parameter -> Indent wrapped parameters', 'Range': {'End': {'Line': 14, 'Character': 22}, 'Start': {'Line': 14, 'Character': 22}}}], 'command': 'omnisharp/executeCodeAction', 'title': 'Wrap every parameter -> Indent wrapped parameters'}}, {'diagnostics': [], 'data': {'$$__handler_id__$$': '971ea130-dc4d-4c43-9b50-74e6076b05e5'}, 'edit': {}, 'kind': 'refactor', 'title': 'Unwrap and indent all parameters', 'command': {'arguments': [{'Uri': 'file:///Users/raoulwols/dev/csharp-project/Program.cs', 'Identifier': 'Unwrap and indent all parameters', 'Name': 'Unwrap and indent all parameters', 'Range': {'End': {'Line': 14, 'Character': 22}, 'Start': {'Line': 14, 'Character': 22}}}], 'command': 'omnisharp/executeCodeAction', 'title': 'Unwrap and indent all parameters'}}, {'diagnostics': [], 'data': {'$$__handler_id__$$': '971ea130-dc4d-4c43-9b50-74e6076b05e5'}, 'edit': {}, 'kind': 'refactor', 'title': 'Use expression body for methods', 'command': {'arguments': [{'Uri': 'file:///Users/raoulwols/dev/csharp-project/Program.cs', 'Identifier': 'Use expression body for methods', 'Name': 'Use expression body for methods', 'Range': {'End': {'Line': 14, 'Character': 22}, 'Start': {'Line': 14, 'Character': 22}}}], 'command': 'omnisharp/executeCodeAction', 'title': 'Use expression body for methods'}}, {'diagnostics': [], 'data': {'$$__handler_id__$$': '971ea130-dc4d-4c43-9b50-74e6076b05e5'}, 'edit': {}, 'kind': 'refactor.extract', 'title': 'Extract base class...', 'command': {'arguments': [{'Uri': 'file:///Users/raoulwols/dev/csharp-project/Program.cs', 'Identifier': 'Extract base class...', 'Name': 'Extract base class...', 'Range': {'End': {'Line': 14, 'Character': 22}, 'Start': {'Line': 14, 'Character': 22}}}], 'command': 'omnisharp/executeCodeAction', 'title': 'Extract base class...'}}]
```

Unwrapped:

```
[
    {
        "diagnostics": [],
        "data": {
            "$$__handler_id__$$": "971ea130-dc4d-4c43-9b50-74e6076b05e5"
        },
        "edit": {},
        "kind": "refactor",
        "title": "Wrap every parameter -> Align wrapped parameters",
        "command": {
            "arguments": [
                {
                    "Uri": "file:///Users/raoulwols/dev/csharp-project/Program.cs",
                    "Identifier": "Align wrapped parameters",
                    "Name": "Wrap every parameter -> Align wrapped parameters",
                    "Range": {
                        "End": {
                            "Line": 14,
                            "Character": 22
                        },
                        "Start": {
                            "Line": 14,
                            "Character": 22
                        }
                    }
                }
            ],
            "command": "omnisharp/executeCodeAction",
            "title": "Wrap every parameter -> Align wrapped parameters"
        }
    },
    {
        "diagnostics": [],
        "data": {
            "$$__handler_id__$$": "971ea130-dc4d-4c43-9b50-74e6076b05e5"
        },
        "edit": {},
        "kind": "refactor",
        "title": "Wrap every parameter -> Indent all parameters",
        "command": {
            "arguments": [
                {
                    "Uri": "file:///Users/raoulwols/dev/csharp-project/Program.cs",
                    "Identifier": "Indent all parameters",
                    "Name": "Wrap every parameter -> Indent all parameters",
                    "Range": {
                        "End": {
                            "Line": 14,
                            "Character": 22
                        },
                        "Start": {
                            "Line": 14,
                            "Character": 22
                        }
                    }
                }
            ],
            "command": "omnisharp/executeCodeAction",
            "title": "Wrap every parameter -> Indent all parameters"
        }
    },
    {
        "diagnostics": [],
        "data": {
            "$$__handler_id__$$": "971ea130-dc4d-4c43-9b50-74e6076b05e5"
        },
        "edit": {},
        "kind": "refactor",
        "title": "Wrap every parameter -> Indent wrapped parameters",
        "command": {
            "arguments": [
                {
                    "Uri": "file:///Users/raoulwols/dev/csharp-project/Program.cs",
                    "Identifier": "Indent wrapped parameters",
                    "Name": "Wrap every parameter -> Indent wrapped parameters",
                    "Range": {
                        "End": {
                            "Line": 14,
                            "Character": 22
                        },
                        "Start": {
                            "Line": 14,
                            "Character": 22
                        }
                    }
                }
            ],
            "command": "omnisharp/executeCodeAction",
            "title": "Wrap every parameter -> Indent wrapped parameters"
        }
    },
    {
        "diagnostics": [],
        "data": {
            "$$__handler_id__$$": "971ea130-dc4d-4c43-9b50-74e6076b05e5"
        },
        "edit": {},
        "kind": "refactor",
        "title": "Unwrap and indent all parameters",
        "command": {
            "arguments": [
                {
                    "Uri": "file:///Users/raoulwols/dev/csharp-project/Program.cs",
                    "Identifier": "Unwrap and indent all parameters",
                    "Name": "Unwrap and indent all parameters",
                    "Range": {
                        "End": {
                            "Line": 14,
                            "Character": 22
                        },
                        "Start": {
                            "Line": 14,
                            "Character": 22
                        }
                    }
                }
            ],
            "command": "omnisharp/executeCodeAction",
            "title": "Unwrap and indent all parameters"
        }
    },
    {
        "diagnostics": [],
        "data": {
            "$$__handler_id__$$": "971ea130-dc4d-4c43-9b50-74e6076b05e5"
        },
        "edit": {},
        "kind": "refactor",
        "title": "Use expression body for methods",
        "command": {
            "arguments": [
                {
                    "Uri": "file:///Users/raoulwols/dev/csharp-project/Program.cs",
                    "Identifier": "Use expression body for methods",
                    "Name": "Use expression body for methods",
                    "Range": {
                        "End": {
                            "Line": 14,
                            "Character": 22
                        },
                        "Start": {
                            "Line": 14,
                            "Character": 22
                        }
                    }
                }
            ],
            "command": "omnisharp/executeCodeAction",
            "title": "Use expression body for methods"
        }
    },
    {
        "diagnostics": [],
        "data": {
            "$$__handler_id__$$": "971ea130-dc4d-4c43-9b50-74e6076b05e5"
        },
        "edit": {},
        "kind": "refactor.extract",
        "title": "Extract base class...",
        "command": {
            "arguments": [
                {
                    "Uri": "file:///Users/raoulwols/dev/csharp-project/Program.cs",
                    "Identifier": "Extract base class...",
                    "Name": "Extract base class...",
                    "Range": {
                        "End": {
                            "Line": 14,
                            "Character": 22
                        },
                        "Start": {
                            "Line": 14,
                            "Character": 22
                        }
                    }
                }
            ],
            "command": "omnisharp/executeCodeAction",
            "title": "Extract base class..."
        }
    }
]
```

I think code actions of type "refactoring" should not be presented in clickable annotations or light bulbs. Those annotations/bulbs are meant for "quick fixes". Whereas "refactorings" don't actually fix anything (by design).

Instead, we should have a separate menu entry at the top menu bar called "Refactorings" that allow things like "Extract Method", "Move Class", etc. Although I don't know how to discover the various types of refactorings dynamically like that but I'm thinking out loud.

See also https://www.jetbrains.com/help/idea/refactoring-source-code.html


I was thinking about this, but I wonder whether there are some misunderstandings in the preceding comments:

>In case of "Refactorings", these kinds should not always be directly visible to the user. Source Actions should be visible, like they are right now with annotations.

Shouldn't it be the exact opposite of that? "Source Actions" (i.e. kind `source`) are not specific to a particular line, so they shouldn't be shown in the annotation (or lightbulb), but instead go into the top menu under "Edit". See also the description in the LSP specs:

```typescript
        /**
	 * Base kind for source actions: `source`.
	 *
	 * Source code actions apply to the entire file.
	 */
	export const Source: [CodeActionKind](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#codeActionKind) = 'source';
```

On the other hand "Refactorings" (kind `refactor`) only apply to one particular function, or class etc., so they depend on the current cursor position.
It could be argued whether they should be shown in the view or only as an entry under the "Edit" top menu. Maybe the latter would be better - then only code actions of the kind `quickfix`, (and actions which don't have any kind specified) would be shown in the view.

---

> We already have a `Source action...` in the right click context menu. Showing those as annotations would not be correct IMO as those don't bind to specific line in the code.

The "Source" actions *are* currently shown in the view (!)
(Not sure whether this behavior maybe was changed since that comment above was made.)
So with the explanation above, I think they should be removed from the view and also added to another entry under "Edit", with running the request for code actions of that kind for the entire content range, when selected that way.

---

Furthermore I was looking through the source code, and I think there is a bug in this part for the code actions on save:
https://github.com/sublimelsp/LSP/blob/509e4d599a5895727525a89d8963bedae4f57e16/plugin/code_actions.py#L146-L148

`codeActionKinds` is an optional property for the `codeActionProvider` server capability, and if the server doesn't provide this list, that does not mean that the server doesn't support the `CodeAction.kind` property in the response. So a request should also be made for sessions which don't declare `codeActionProvider.codeActionKinds`, and the response should simply be filtered with the matching kinds afterwards.

This statement is based on the following response: https://github.com/microsoft/language-server-protocol/issues/890#issuecomment-725504061
which I would interpret in a way that `codeActionProvider.codeActionKinds` should only be used to filter *out* when requests are unnecessary to sent, but not as a requirement that this property needs to exist for sending the request for code actions on save.
> The "Source" actions _are_ currently shown in the view (!)
> (Not sure whether this behavior maybe was changed since that comment above was made.)

I haven't looked at the LSP code but checking with typescript-language-server I'm getting `Organize Imports` when clicking the `Source action...` menu but not in the view anywhere.

EDIT:
typescript server only responds with source-level refactor if explicitly asked for it through `kind`.

> So a request should also be made for sessions which don't declare `codeActionProvider.codeActionKinds`, and the response should simply be filtered with the matching kinds afterwards.

What would we filter against when the server wouldn't provide `codeActionKinds`? Would we filter against `source.*` when triggering the `Source action...` menu? I suppose that makes sense.

As for on-save-actions do you mean we should do something like:
```py
matching_kinds = get_matching_kinds(on_save_actions, supported_kinds) if supported_kinds != None else on_save_action_kinds
```
?
> What would we filter against when the server wouldn't provide `codeActionKinds`? Would we filter against `source.*` when triggering the `Source action...` menu?

Yes, I would filter against `source.*` (meaning just `source` would be included, as well as `source.foobar`), when manually triggering "Source actions..." via main menu or right-click context menu. For the code actions on save, the returned actions should be filtered against only the (sub-)kinds of what the user has in their settings file. So `source.fixAll.foobar` would be run if the user has `source.fixAll` activated, but `source.foobar` would be ignored. And if a returned action has no kind set, it would also be ignored.

Also there seems to be some oddities going on when using the "Source actions..." context menu. With LSP-marksman installed, I get a "Create Table of Contents" action shown in the view (lightbulb or annotation), on any line / cursor position (which is a bit annyoing in my opinion). From the logs I can see that this has the `source` kind. When I click "Code Action..." from the context menu, a request is made and I can select this same action from the command palette (OK). But when I click "Source Action..." from the context menu, no request is being made, and I don't get any result. In this case I would expect LSP to make a request with `"only": ["source"]` for the `context` parameter, hopefully the server to respond with this code action, and show it in the command palette.
I haven't investigated how the caching for code actions works and what's exactly happening here.

> As for on-save-actions do you mean we should do something like:
> 
> ```python
> matching_kinds = get_matching_kinds(on_save_actions, supported_kinds) if supported_kinds != None else on_save_action_kinds
> ```

Yes


> Yes, I would filter against `source.*` (meaning just `source` would be included, as well as `source.foobar`), when manually triggering "Source actions..." via main menu or right-click context menu.

Agreed. This needs to be fixed. (https://github.com/sublimelsp/LSP/pull/2064)

> For the code actions on save, the returned actions should be filtered against only the (sub-)kinds of what the user has in their settings file. So `source.fixAll.foobar` would be run if the user has `source.fixAll` activated, but `source.foobar` would be ignored. And if a returned action has no kind set, it would also be ignored.

I think that in the "on save" case we should actually still be intersecting user's `code_actions_on_save` and `codeActionKinds`' reported by the server and only send the request if those are matching. At least that's how VSCode behaves.

> Also there seems to be some oddities going on when using the "Source actions..." context menu. With LSP-marksman installed, I get a "Create Table of Contents" action shown in the view (lightbulb or annotation)

I guess that either marksman should not be returning `source` actions until explicitly asked for those or we should be requesting view code actions with an explicit list of `kind`s that doesn't include `source`.

But looking at VSCode again (de-facto standard), it doesn't request with `only` kinds specified when requesting code actions for current selection so I think marksman is behaving incorrectly here.
Also see marksman issue I've just created: https://github.com/artempyanykh/marksman/issues/90

Resolving patch:
diff --git a/boot.py b/boot.py
--- a/boot.py
+++ b/boot.py
@@ -4,6 +4,7 @@
 
 # Please keep this list sorted (Edit -> Sort Lines)
 from .plugin.code_actions import LspCodeActionsCommand
+from .plugin.code_actions import LspRefactorCommand
 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
--- a/plugin/code_actions.py
+++ b/plugin/code_actions.py
@@ -34,6 +34,8 @@ 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.refactor_actions_cache = []  # type: List[Tuple[str, CodeAction]]
 
     def request_for_region_async(
         self,
@@ -49,6 +51,7 @@ def request_for_region_async(
         """
         listener = windows.listener_for_view(view)
         if not listener:
+            self.refactor_actions_key = None
             return Promise.resolve([])
         location_cache_key = None
         use_cache = not manual
@@ -60,6 +63,9 @@ 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)
+            self.refactor_actions_cache.clear()
 
         def request_factory(session: Session) -> Optional[Request]:
             diagnostics = []  # type: List[Diagnostic]
@@ -73,13 +79,23 @@ 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:
-                return [a for a in actions if not is_command(a) and kinds_include_kind(only_kinds, a.get('kind'))]
+                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')
+                ]
             if manual:
-                return actions
+                return [a for a in actions if not a.get('disabled')]
             # On implicit (selection change) request, only return commands and quick fix kinds.
             return [
                 a for a in actions
-                if is_command(a) or not a.get('kind') or kinds_include_kind([CodeActionKind.QuickFix], a.get('kind'))
+                if is_command(a) or not a.get('disabled') and
+                kinds_include_kind([CodeActionKind.QuickFix], a.get('kind', CodeActionKind.QuickFix))
             ]
 
         task = self._collect_code_actions_async(listener, request_factory, response_filter)
@@ -115,7 +131,7 @@ def response_filter(session: Session, actions: List[CodeActionOrCommand]) -> Lis
             # actions that need to be then manually filtered.
             session_kinds = get_session_kinds(session)
             matching_kinds = get_matching_on_save_kinds(on_save_actions, session_kinds)
-            return [a for a in actions if a.get('kind') in matching_kinds]
+            return [a for a in actions if a.get('kind') in matching_kinds and not a.get('disabled')]
 
         return self._collect_code_actions_async(listener, request_factory, response_filter)
 
@@ -129,12 +145,9 @@ def _collect_code_actions_async(
         def on_response(
             session: Session, response: Union[Error, Optional[List[CodeActionOrCommand]]]
         ) -> CodeActionsByConfigName:
-            if isinstance(response, Error):
-                actions = []
-            else:
-                actions = [action for action in (response or []) if not action.get('disabled')]
-                if actions and response_filter:
-                    actions = response_filter(session, actions)
+            actions = []
+            if response and not isinstance(response, Error) and response_filter:
+                actions = response_filter(session, response)
             return (session.config.name, actions)
 
         tasks = []  # type: List[Promise[CodeActionsByConfigName]]
@@ -333,3 +346,52 @@ def run_async() -> None:
     def _handle_response_async(self, session_name: str, response: Any) -> None:
         if isinstance(response, Error):
             sublime.error_message("{}: {}".format(session_name, str(response)))
+
+
+class LspRefactorCommand(LspTextCommand):
+
+    capability = 'codeActionProvider'
+
+    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)
+
+    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)
+            return False
+        return id < len(actions_manager.refactor_actions_cache) and self._is_cache_valid()
+
+    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']
+
+    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))
+
+    def run_async(self, id: int) -> None:
+        if self._is_cache_valid():
+            config_name, action = actions_manager.refactor_actions_cache[id]
+            session = self.session_by_name(config_name)
+            if session:
+                session.run_code_action_async(action, progress=True) \
+                    .then(lambda response: self._handle_response_async(config_name, response))
+
+    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)
+        if region is None:
+            return False
+        return actions_manager.refactor_actions_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)


Test patch: