From af3b9eec1e0abef8d51d0c219f38981c28b122b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Fri, 2 Feb 2024 09:49:02 +0100 Subject: [PATCH] Add format on paste (#2397) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add format on paste * include whitespace to improve format on paste Including whitespace does help some servers to format a range as expected like LSP-json, See: https://github.com/sublimelsp/LSP/pull/2311#issuecomment-1688593038 * tweak comment * fix flake * move before run_command * move up to be before the early return * introduce lsp_format_on_paste instead of format_on_paste So it can be defined in Preferences.sublime-settings * fix flake * fix over indent flake :( * use $ref for lsp_format_on_paste * Rafal having good suggestions as always :) * handle paste_and_indent as well if lsp_format_on_paste is enabled * make sure that we have LSP running before we override the paste_and_indent and generally make sure to run format_on_paste if the session supports documentRangeFormattingProvider * Update plugin/documents.py Co-authored-by: Rafał Chłodnicki * fix : * the first paragraph is not necessary * remove unnecessary line and move inside if * avoid quick flash of selected region before format is run Before, occasionally it was possible to see a brief moment when text gets selected, Now, we move that from the async thread to the main thread, thus we will not see that flash * update text to be more user friendly --------- Co-authored-by: Rafał Chłodnicki --- LSP.sublime-settings | 5 +++- docs/src/client_configuration.md | 2 +- plugin/core/types.py | 2 ++ plugin/documents.py | 46 +++++++++++++++++++++++++++++++- sublime-package.json | 13 ++++++++- 5 files changed, 64 insertions(+), 4 deletions(-) diff --git a/LSP.sublime-settings b/LSP.sublime-settings index baf277228..4a84fd54f 100644 --- a/LSP.sublime-settings +++ b/LSP.sublime-settings @@ -7,7 +7,10 @@ // --- Document Formatting ------------------------------------------------------------ - // Run the server's formatProvider (if supported) on a file before saving. + // If supported, format the pasted text. + "lsp_format_on_paste": false, + + // If supported, format a file before saving. // This option is also supported in syntax-specific settings and/or in the // "settings" section of project files. "lsp_format_on_save": false, diff --git a/docs/src/client_configuration.md b/docs/src/client_configuration.md index 8b01dd573..c066a1d3d 100644 --- a/docs/src/client_configuration.md +++ b/docs/src/client_configuration.md @@ -79,7 +79,7 @@ The port number can be inserted into the server's startup `command` in your clie ## Per-project overrides -Global LSP settings (which currently are `lsp_format_on_save` and `lsp_code_actions_on_save`) can be overridden per-project in `.sublime-project` file: +Global LSP settings (which currently are `lsp_format_on_save`, `lsp_format_on_paste` and `lsp_code_actions_on_save`) can be overridden per-project in `.sublime-project` file: ```jsonc { diff --git a/plugin/core/types.py b/plugin/core/types.py index 7fac717c2..91b7d16df 100644 --- a/plugin/core/types.py +++ b/plugin/core/types.py @@ -215,6 +215,7 @@ class Settings: log_max_size = cast(int, None) log_server = cast(List[str], None) lsp_code_actions_on_save = cast(Dict[str, bool], None) + lsp_format_on_paste = cast(bool, None) lsp_format_on_save = cast(bool, None) on_save_task_timeout_ms = cast(int, None) only_show_lsp_completions = cast(bool, None) @@ -256,6 +257,7 @@ def r(name: str, default: Union[bool, int, str, list, dict]) -> None: r("log_debug", False) r("log_max_size", 8 * 1024) r("lsp_code_actions_on_save", {}) + r("lsp_format_on_paste", False) r("lsp_format_on_save", False) r("on_save_task_timeout_ms", 2000) r("only_show_lsp_completions", False) diff --git a/plugin/documents.py b/plugin/documents.py index 49653a875..71e97be1e 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -170,6 +170,7 @@ def on_change() -> None: self._registration = SettingsRegistration(view.settings(), on_change=on_change) self._completions_task = None # type: Optional[QueryCompletionsTask] self._stored_selection = [] # type: List[sublime.Region] + self._should_format_on_paste = False self._setup() def __del__(self) -> None: @@ -525,10 +526,20 @@ def on_text_command(self, command_name: str, args: Optional[dict]) -> Optional[T session = self.session_async("semanticTokensProvider") if session: return ("lsp_show_scope_name", {}) + elif command_name == 'paste_and_indent': + # it is easier to find the region to format when `paste` is invoked, + # so we intercept the `paste_and_indent` and replace it with the `paste` command. + format_on_paste = self.view.settings().get('lsp_format_on_paste', userprefs().lsp_format_on_paste) + if format_on_paste and self.session_async("documentRangeFormattingProvider"): + return ('paste', {}) return None def on_post_text_command(self, command_name: str, args: Optional[Dict[str, Any]]) -> None: - if command_name in ("next_field", "prev_field") and args is None: + if command_name == 'paste': + format_on_paste = self.view.settings().get('lsp_format_on_paste', userprefs().lsp_format_on_paste) + if format_on_paste and self.session_async("documentRangeFormattingProvider"): + self._should_format_on_paste = True + elif command_name in ("next_field", "prev_field") and args is None: sublime.set_timeout_async(lambda: self.do_signature_help_async(manual=True)) if not self.view.is_popup_visible(): return @@ -926,6 +937,9 @@ def _register_async(self) -> None: listener.on_load_async() def _on_view_updated_async(self) -> None: + if self._should_format_on_paste: + self._should_format_on_paste = False + self._format_on_paste_async() self._code_lenses_debouncer_async.debounce( self._do_code_lenses_async, timeout_ms=self.code_lenses_debounce_time) first_region, _ = self._update_stored_selection_async() @@ -959,6 +973,36 @@ def _update_stored_selection_async(self) -> Tuple[Optional[sublime.Region], bool self._stored_selection = selection return changed_first_region, True + def _format_on_paste_async(self) -> None: + clipboard_text = sublime.get_clipboard() + sel = self.view.sel() + split_clipboard_text = clipboard_text.split('\n') + multi_cursor_paste = len(split_clipboard_text) == len(sel) and len(sel) > 1 + original_selection = list(sel) + regions_to_format = [] # type: List[sublime.Region] + pasted_text = clipboard_text + # add regions to selection, in order for lsp_format_document_range to format those regions + for index, region in enumerate(sel): + if multi_cursor_paste: + pasted_text = split_clipboard_text[index] + pasted_region = self.view.find(pasted_text, region.end(), sublime.REVERSE | sublime.LITERAL) + if pasted_region: + # Including whitespace may help servers format a range better + # More info at https://github.com/sublimelsp/LSP/pull/2311#issuecomment-1688593038 + a = self.view.find_by_class(pasted_region.a, False, + sublime.CLASS_WORD_END | sublime.CLASS_PUNCTUATION_END) + formatting_region = sublime.Region(a, pasted_region.b) + regions_to_format.append(formatting_region) + self.purge_changes_async() + + def run_sync() -> None: + sel.add_all(regions_to_format) + self.view.run_command('lsp_format_document_range') + sel.clear() + sel.add_all(original_selection) + + sublime.set_timeout(run_sync) + def _clear_session_views_async(self) -> None: session_views = self._session_views diff --git a/sublime-package.json b/sublime-package.json index 74689be68..632870d7f 100644 --- a/sublime-package.json +++ b/sublime-package.json @@ -6,10 +6,15 @@ "$id": "sublime://settings/LSP", "definitions": { // User preferences that are shared with "Preferences.sublime-settings" + "lsp_format_on_paste": { + "type": "boolean", + "default": false, + "markdownDescription": "If supported, format the pasted text." + }, "lsp_format_on_save": { "type": "boolean", "default": false, - "markdownDescription": "Run the server's formatProvider (if supported) on a document before saving. This option is also supported in syntax-specific settings and/or in the `\"settings\"` section of project files." + "markdownDescription": "If supported, format a file before saving. This option is also supported in syntax-specific settings and/or in the `\"settings\"` section of project files." }, "lsp_code_actions_on_save": { "type": "object", @@ -444,6 +449,9 @@ "default": false, "markdownDescription": "Show errors and warnings count in the status bar." }, + "lsp_format_on_paste": { + "$ref": "sublime://settings/LSP#/definitions/lsp_format_on_paste" + }, "lsp_format_on_save": { "$ref": "sublime://settings/LSP#/definitions/lsp_format_on_save" }, @@ -796,6 +804,9 @@ ], "schema": { "properties": { + "lsp_format_on_paste": { + "$ref": "sublime://settings/LSP#/definitions/lsp_format_on_paste", + }, "lsp_format_on_save": { "$ref": "sublime://settings/LSP#/definitions/lsp_format_on_save", },