import sublime import sublime_plugin from .core.edit import sort_by_application_order, TextEdit from .core.logging import debug from .core.typing import List, Dict, Optional, Any, Generator from contextlib import contextmanager @contextmanager def temporary_setting(settings: sublime.Settings, key: str, val: Any) -> Generator[None, None, None]: prev_val = None has_prev_val = settings.has(key) if has_prev_val: prev_val = settings.get(key) settings.set(key, val) yield settings.erase(key) if has_prev_val and settings.get(key) != prev_val: settings.set(key, prev_val) class LspApplyWorkspaceEditCommand(sublime_plugin.WindowCommand): def run(self, changes: Optional[Dict[str, List[TextEdit]]] = None) -> None: documents_changed = 0 if changes: for path, document_changes in changes.items(): self.open_and_apply_edits(path, document_changes) documents_changed += 1 if documents_changed > 0: message = 'Applied changes to {} documents'.format(documents_changed) self.window.status_message(message) else: self.window.status_message('No changes to apply to workspace') def open_and_apply_edits(self, path: str, file_changes: List[TextEdit]) -> None: view = self.window.open_file(path) if view: if view.is_loading(): # TODO: wait for event instead. sublime.set_timeout_async( lambda: view.run_command('lsp_apply_document_edit', {'changes': file_changes}), 500 ) else: view.run_command('lsp_apply_document_edit', {'changes': file_changes}) else: debug('view not found to apply', path, file_changes) class LspApplyDocumentEditCommand(sublime_plugin.TextCommand): def run(self, edit: Any, changes: Optional[List[TextEdit]] = None) -> None: # Apply the changes in reverse, so that we don't invalidate the range # of any change that we haven't applied yet. if not changes: return with temporary_setting(self.view.settings(), "translate_tabs_to_spaces", False): view_version = self.view.change_count() last_row, last_col = self.view.rowcol(self.view.size()) for start, end, replacement, version in reversed(sort_by_application_order(changes)): if version is not None and version != view_version: debug('ignoring edit due to non-matching document version') continue region = sublime.Region(self.view.text_point(*start), self.view.text_point(*end)) if start[0] > last_row and replacement[0] != '\n': # Handle when a language server (eg gopls) inserts at a row beyond the document # some editors create the line automatically, sublime needs to have the newline prepended. self.apply_change(region, '\n' + replacement, edit) last_row, last_col = self.view.rowcol(self.view.size()) else: self.apply_change(region, replacement, edit) def apply_change(self, region: sublime.Region, replacement: str, edit: Any) -> None: if region.empty(): self.view.insert(edit, region.a, replacement) else: if len(replacement) > 0: self.view.replace(edit, region, replacement) else: self.view.erase(edit, region)