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", },