Repository: sublimelsp/LSP

Issue:
Handle multiple formatters
**Is your feature request related to a problem? Please describe.**
If multiple sessions with formatting capability are active then we should maybe allow the user to select which one to use.

But it could get annoying to select the formatter each time so maybe preferred choice should be stored somewhere then.
And then there should maybe exist an option to change the preferred choice.

VSCode for reference:
![Screenshot 2022-12-22 at 20 18 54](https://user-images.githubusercontent.com/153197/209210347-2ba283c4-2952-44ce-b29d-e9a46e151848.png)



Discussion:
> But it could get annoying to select the formatter each time so maybe preferred choice should be stored somewhere then.

Maybe just ask the first time `Format Document` is executed and store that selection for the current session. `Format Document With...`  would change the formatter used by `Format Document`.
Not convinced that storing in memory would be sufficient. Might be annoying for those who restart ST often and expect specific formatter to be always used in specific projects.

I suppose we could store the decision in the project if one is present. Otherwise just in memory.

But also it might be necessary to store decision per-file type.

Resolving patch:
diff --git a/plugin/core/windows.py b/plugin/core/windows.py
--- a/plugin/core/windows.py
+++ b/plugin/core/windows.py
@@ -80,6 +80,8 @@ def __init__(self, window: sublime.Window, workspace: ProjectFolders, config_man
         self._server_log = []  # type: List[Tuple[str, str]]
         self.panel_manager = PanelManager(self._window)  # type: Optional[PanelManager]
         self.tree_view_sheets = {}  # type: Dict[str, TreeViewSheet]
+        self.formatters = {}  # type: Dict[str, str]
+        self.suppress_sessions_restart_on_project_update = False
         self.total_error_count = 0
         self.total_warning_count = 0
         sublime.set_timeout(functools.partial(self._update_panel_main_thread, _NO_DIAGNOSTICS_PLACEHOLDER, []))
@@ -106,6 +108,9 @@ def on_load_project_async(self) -> None:
         self._config_manager.update()
 
     def on_post_save_project_async(self) -> None:
+        if self.suppress_sessions_restart_on_project_update:
+            self.suppress_sessions_restart_on_project_update = False
+            return
         self.on_load_project_async()
 
     def update_workspace_folders_async(self) -> None:
diff --git a/plugin/formatting.py b/plugin/formatting.py
--- a/plugin/formatting.py
+++ b/plugin/formatting.py
@@ -1,9 +1,11 @@
+from .core.collections import DottedDict
 from .core.edit import parse_text_edit
 from .core.promise import Promise
 from .core.protocol import Error
 from .core.protocol import TextDocumentSaveReason
 from .core.protocol import TextEdit
 from .core.registry import LspTextCommand
+from .core.registry import windows
 from .core.sessions import Session
 from .core.settings import userprefs
 from .core.typing import Any, Callable, List, Optional, Iterator, Union
@@ -15,14 +17,28 @@
 from .core.views import text_document_ranges_formatting
 from .core.views import will_save_wait_until
 from .save_command import LspSaveCommand, SaveTask
+from functools import partial
 import sublime
 
 
 FormatResponse = Union[List[TextEdit], None, Error]
 
 
-def format_document(text_command: LspTextCommand) -> Promise[FormatResponse]:
+def get_formatter(window: Optional[sublime.Window], base_scope: str) -> Optional[str]:
+    window_manager = windows.lookup(window)
+    if not window_manager:
+        return None
+    project_data = window_manager.window.project_data()
+    return DottedDict(project_data).get('settings.LSP.formatters.{}'.format(base_scope)) if \
+        isinstance(project_data, dict) else window_manager.formatters.get(base_scope)
+
+
+def format_document(text_command: LspTextCommand, formatter: Optional[str] = None) -> Promise[FormatResponse]:
     view = text_command.view
+    if formatter:
+        session = text_command.session_by_name(formatter, LspFormatDocumentCommand.capability)
+        if session:
+            return session.send_request_task(text_document_formatting(view))
     session = text_command.best_session(LspFormatDocumentCommand.capability)
     if session:
         # Either use the documentFormattingProvider ...
@@ -84,7 +100,9 @@ def is_applicable(cls, view: sublime.View) -> bool:
     def run_async(self) -> None:
         super().run_async()
         self._purge_changes_async()
-        format_document(self._task_runner).then(self._on_response)
+        base_scope = self._task_runner.view.syntax().scope
+        formatter = get_formatter(self._task_runner.view.window(), base_scope)
+        format_document(self._task_runner, formatter).then(self._on_response)
 
     def _on_response(self, response: FormatResponse) -> None:
         if response and not isinstance(response, Error) and not self._cancelled:
@@ -100,16 +118,59 @@ class LspFormatDocumentCommand(LspTextCommand):
 
     capability = 'documentFormattingProvider'
 
-    def is_enabled(self, event: Optional[dict] = None, point: Optional[int] = None) -> bool:
+    def is_enabled(self, event: Optional[dict] = None, select: bool = False) -> bool:
+        if select:
+            return len(list(self.sessions(self.capability))) > 1
         return super().is_enabled() or bool(self.best_session(LspFormatDocumentRangeCommand.capability))
 
-    def run(self, edit: sublime.Edit, event: Optional[dict] = None) -> None:
-        format_document(self).then(self.on_result)
+    def run(self, edit: sublime.Edit, event: Optional[dict] = None, select: bool = False) -> None:
+        session_names = [session.config.name for session in self.sessions(self.capability)]
+        base_scope = self.view.syntax().scope
+        if select:
+            self.select_formatter(base_scope, session_names)
+        elif len(session_names) > 1:
+            formatter = get_formatter(self.view.window(), base_scope)
+            if formatter:
+                session = self.session_by_name(formatter, self.capability)
+                if session:
+                    session.send_request_task(text_document_formatting(self.view)).then(self.on_result)
+                    return
+            self.select_formatter(base_scope, session_names)
+        else:
+            format_document(self).then(self.on_result)
 
     def on_result(self, result: FormatResponse) -> None:
         if result and not isinstance(result, Error):
             apply_text_edits_to_view(result, self.view)
 
+    def select_formatter(self, base_scope: str, session_names: List[str]) -> None:
+        window = self.view.window()
+        if not window:
+            return
+        window.show_quick_panel(
+            session_names, partial(self.on_select_formatter, base_scope, session_names), placeholder="Select Formatter")
+
+    def on_select_formatter(self, base_scope: str, session_names: List[str], index: int) -> None:
+        if index == -1:
+            return
+        session_name = session_names[index]
+        window_manager = windows.lookup(self.view.window())
+        if window_manager:
+            window = window_manager.window
+            project_data = window.project_data()
+            if isinstance(project_data, dict):
+                project_settings = project_data.setdefault('settings', dict())
+                project_lsp_settings = project_settings.setdefault('LSP', dict())
+                project_formatter_settings = project_lsp_settings.setdefault('formatters', dict())
+                project_formatter_settings[base_scope] = session_name
+                window_manager.suppress_sessions_restart_on_project_update = True
+                window.set_project_data(project_data)
+            else:  # Save temporarily for this window
+                window_manager.formatters[base_scope] = session_name
+        session = self.session_by_name(session_name, self.capability)
+        if session:
+            session.send_request_task(text_document_formatting(self.view)).then(self.on_result)
+
 
 class LspFormatDocumentRangeCommand(LspTextCommand):
 


Test patch: