From de0ef67befd7d2e813ebe7fcabacdb143ff9e535 Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Sun, 16 Jan 2022 00:04:53 +0100 Subject: [PATCH] Remove more usage of uri_to_filename (#1796) --- plugin/__init__.py | 6 ++- plugin/core/edit.py | 36 +++++-------- plugin/core/open.py | 74 +++++++++----------------- plugin/core/sessions.py | 112 ++++++++++++++++++++++++--------------- plugin/core/types.py | 6 ++- plugin/core/windows.py | 4 +- plugin/edit.py | 23 ++++++-- plugin/locationpicker.py | 4 +- plugin/rename.py | 85 +++++++++++++++-------------- stubs/sublime.pyi | 2 +- tests/test_edit.py | 25 ++++----- tests/test_url.py | 14 ++--- 12 files changed, 205 insertions(+), 186 deletions(-) diff --git a/plugin/__init__.py b/plugin/__init__.py index 05ea9cecb..6a87614f6 100644 --- a/plugin/__init__.py +++ b/plugin/__init__.py @@ -17,7 +17,8 @@ from .core.types import ClientConfig from .core.types import matches_pattern from .core.url import filename_to_uri -from .core.url import uri_to_filename +from .core.url import parse_uri +from .core.url import uri_to_filename # deprecated from .core.version import __version__ from .core.views import MarkdownLangMap @@ -36,6 +37,7 @@ 'MarkdownLangMap', 'matches_pattern', 'Notification', + 'parse_uri', 'register_file_watcher_implementation', 'register_plugin', 'Request', @@ -43,6 +45,6 @@ 'Session', 'SessionBufferProtocol', 'unregister_plugin', - 'uri_to_filename', + 'uri_to_filename', # deprecated 'WorkspaceFolder', ] diff --git a/plugin/core/edit.py b/plugin/core/edit.py index 717a7d3a5..71181d4e5 100644 --- a/plugin/core/edit.py +++ b/plugin/core/edit.py @@ -2,10 +2,8 @@ from .open import open_file from .promise import Promise from .protocol import TextEdit as LspTextEdit, Position -from .typing import List, Dict, Any, Iterable, Optional, Tuple -from .url import uri_to_filename +from .typing import List, Dict, Any, Optional, Tuple from functools import partial -import operator import sublime @@ -19,17 +17,19 @@ def parse_workspace_edit(workspace_edit: Dict[str, Any]) -> Dict[str, List[TextE if isinstance(document_changes, list): for document_change in document_changes: if 'kind' in document_change: + # TODO: Support resource operations (create/rename/remove) debug('Ignoring unsupported "resourceOperations" edit type') continue - uri = document_change.get('textDocument').get('uri') - version = document_change.get('textDocument').get('version') + text_document = document_change["textDocument"] + uri = text_document['uri'] + version = text_document.get('version') text_edit = list(parse_text_edit(change, version) for change in document_change.get('edits')) - changes.setdefault(uri_to_filename(uri), []).extend(text_edit) + changes.setdefault(uri, []).extend(text_edit) else: raw_changes = workspace_edit.get('changes') if isinstance(raw_changes, dict): - for uri, file_changes in raw_changes.items(): - changes[uri_to_filename(uri)] = list(parse_text_edit(change) for change in file_changes) + for uri, uri_changes in raw_changes.items(): + changes[uri] = list(parse_text_edit(change) for change in uri_changes) return changes @@ -47,24 +47,14 @@ def parse_text_edit(text_edit: LspTextEdit, version: int = None) -> TextEditTupl ) -def sort_by_application_order(changes: Iterable[TextEditTuple]) -> List[TextEditTuple]: - # The spec reads: - # > However, it is possible that multiple edits have the same start position: multiple - # > inserts, or any number of inserts followed by a single remove or replace edit. If - # > multiple inserts have the same position, the order in the array defines the order in - # > which the inserted strings appear in the resulting text. - # So we sort by start position. But if multiple text edits start at the same position, - # we use the index in the array as the key. - - return list(sorted(changes, key=operator.itemgetter(0))) - - def apply_workspace_edit(window: sublime.Window, changes: Dict[str, List[TextEditTuple]]) -> Promise: - """Apply workspace edits. This function must be called from the main thread!""" - return Promise.all([open_file(window, fn).then(partial(_apply_edits, edits)) for fn, edits in changes.items()]) + """ + DEPRECATED: Use session.apply_workspace_edit_async instead. + """ + return Promise.all([open_file(window, uri).then(partial(apply_edits, edits)) for uri, edits in changes.items()]) -def _apply_edits(edits: List[TextEditTuple], view: Optional[sublime.View]) -> None: +def apply_edits(edits: List[TextEditTuple], view: Optional[sublime.View]) -> None: if view and view.is_valid(): # Text commands run blocking. After this call has returned the changes are applied. view.run_command("lsp_apply_document_edit", {"changes": edits}) diff --git a/plugin/core/open.py b/plugin/core/open.py index 0069d72b7..498a9e88e 100644 --- a/plugin/core/open.py +++ b/plugin/core/open.py @@ -1,39 +1,43 @@ from .logging import exception_log -from .promise import PackagedTask from .promise import Promise from .promise import ResolveFunc -from .protocol import Range, RangeLsp +from .protocol import DocumentUri +from .protocol import Range +from .protocol import RangeLsp from .typing import Dict, Tuple, Optional -from .url import uri_to_filename +from .url import parse_uri from .views import range_to_region import os import sublime import subprocess -import webbrowser opening_files = {} # type: Dict[str, Tuple[Promise[Optional[sublime.View]], ResolveFunc[Optional[sublime.View]]]] def open_file( - window: sublime.Window, file_path: str, flags: int = 0, group: int = -1 + window: sublime.Window, uri: DocumentUri, flags: int = 0, group: int = -1 ) -> Promise[Optional[sublime.View]]: - """Open a file asynchronously. It is only safe to call this function from the UI thread.""" - + """ + Open a file asynchronously. + It is only safe to call this function from the UI thread. + The provided uri MUST be a file URI + """ + file = parse_uri(uri)[1] # window.open_file brings the file to focus if it's already opened, which we don't want. # So we first check if there's already a view for that file. - view = window.find_open_file(file_path) + view = window.find_open_file(file) if view: return Promise.resolve(view) - view = window.open_file(file_path, flags, group) + view = window.open_file(file, flags, group) if not view.is_loading(): # It's already loaded. Possibly already open in a tab. return Promise.resolve(view) # Is the view opening right now? Then return the associated unresolved promise for fn, value in opening_files.items(): - if fn == file_path or os.path.samefile(fn, file_path): + if fn == file or os.path.samefile(fn, file): # Return the unresolved promise. A future on_load event will resolve the promise. return value[0] @@ -41,12 +45,12 @@ def open_file( def fullfill(resolve: ResolveFunc[Optional[sublime.View]]) -> None: global opening_files # Save the promise in the first element of the tuple -- except we cannot yet do that here - opening_files[file_path] = (None, resolve) # type: ignore + opening_files[file] = (None, resolve) # type: ignore promise = Promise(fullfill) - tup = opening_files[file_path] + tup = opening_files[file] # Save the promise in the first element of the tuple so that the for-loop above can return it - opening_files[file_path] = (promise, tup[1]) + opening_files[file] = (promise, tup[1]) return promise @@ -56,54 +60,26 @@ def center_selection(v: sublime.View, r: RangeLsp) -> sublime.View: window = v.window() if window: window.focus_view(v) - v.show_at_center(selection) + if int(sublime.version()) >= 4124: + v.show_at_center(selection, animate=False) + else: + # TODO: remove later when a stable build lands + v.show_at_center(selection) # type: ignore return v -def open_file_and_center(window: sublime.Window, file_path: str, r: Optional[RangeLsp], flags: int = 0, - group: int = -1) -> Promise[Optional[sublime.View]]: - """Open a file asynchronously and center the range. It is only safe to call this function from the UI thread.""" - - def center(v: Optional[sublime.View]) -> Optional[sublime.View]: - if v and v.is_valid(): - return center_selection(v, r) if r else v - return None - - # TODO: ST API does not allow us to say "do not focus this new view" - return open_file(window, file_path, flags, group).then(center) - - -def open_file_and_center_async(window: sublime.Window, file_path: str, r: Optional[RangeLsp], flags: int = 0, - group: int = -1) -> Promise[Optional[sublime.View]]: - """Open a file asynchronously and center the range, worker thread version.""" - pair = Promise.packaged_task() # type: PackagedTask[Optional[sublime.View]] - sublime.set_timeout( - lambda: open_file_and_center(window, file_path, r, flags, group).then( - lambda view: sublime.set_timeout_async( - lambda: pair[1](view) - ) - ) - ) - return pair[0] - - def open_externally(uri: str, take_focus: bool) -> bool: """ A blocking function that invokes the OS's "open with default extension" """ - if uri.startswith("http:") or uri.startswith("https:"): - return webbrowser.open(uri, autoraise=take_focus) - file = uri_to_filename(uri) try: # TODO: handle take_focus if sublime.platform() == "windows": - # os.startfile only exists on windows, but pyright does not understand sublime.platform(). - # TODO: How to make pyright understand platform-specific code with sublime.platform()? - os.startfile(file) # type: ignore + os.startfile(uri) # type: ignore elif sublime.platform() == "osx": - subprocess.check_call(("/usr/bin/open", file)) + subprocess.check_call(("/usr/bin/open", uri)) else: # linux - subprocess.check_call(("xdg-open", file)) + subprocess.check_call(("xdg-open", uri)) return True except Exception as ex: exception_log("Failed to open {}".format(uri), ex) diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index c21efcbbb..a02631ae2 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -1,7 +1,8 @@ from .collections import DottedDict from .diagnostics_manager import DiagnosticsManager -from .edit import apply_workspace_edit +from .edit import apply_edits from .edit import parse_workspace_edit +from .edit import TextEditTuple from .file_watcher import DEFAULT_KIND from .file_watcher import file_watcher_event_type_to_lsp_file_change_type from .file_watcher import FileWatcher @@ -12,10 +13,11 @@ from .logging import exception_log from .open import center_selection from .open import open_externally +from .open import open_file from .progress import WindowProgressReporter from .promise import PackagedTask from .promise import Promise -from .protocol import CodeAction, CodeLens, InsertTextMode, Location, LocationLink, Position +from .protocol import CodeAction, CodeLens, InsertTextMode, Location, LocationLink from .protocol import Command from .protocol import CompletionItemTag from .protocol import Diagnostic @@ -59,7 +61,6 @@ from .views import MarkdownLangMap from .views import SEMANTIC_TOKENS_MAP from .views import SYMBOL_KINDS -from .views import to_encoded_filename from .workspace import is_subpath_of from abc import ABCMeta from abc import abstractmethod @@ -1410,16 +1411,12 @@ def run_code_action_async(self, code_action: Union[Command, CodeAction], progres def open_uri_async( self, uri: DocumentUri, - r: Optional[RangeLsp], + r: Optional[RangeLsp] = None, flags: int = 0, group: int = -1 - ) -> Promise[bool]: + ) -> Promise[Optional[sublime.View]]: if uri.startswith("file:"): - # TODO: open_file_and_center_async seems broken for views that have *just* been opened via on_load - path = self.config.map_server_uri_to_client_path(uri) - pos = r["start"] if r else {"line": 0, "character": 0} # type: Position - self.window.open_file(to_encoded_filename(path, pos), flags | sublime.ENCODED_POSITION, group) - return Promise.resolve(True) + return self._open_file_uri_async(uri, r, flags, group) # Try to find a pre-existing session-buffer sb = self.get_session_buffer_for_uri_async(uri) if sb: @@ -1427,35 +1424,63 @@ def open_uri_async( self.window.focus_view(view) if r: center_selection(view, r) - return Promise.resolve(True) + return Promise.resolve(view) # There is no pre-existing session-buffer, so we have to go through AbstractPlugin.on_open_uri_async. if self._plugin: - # I cannot type-hint an unpacked tuple - pair = Promise.packaged_task() # type: PackagedTask[Tuple[str, str, str]] - # It'd be nice to have automatic tuple unpacking continuations - callback = lambda a, b, c: pair[1]((a, b, c)) # noqa: E731 - if self._plugin.on_open_uri_async(uri, callback): - result = Promise.packaged_task() # type: PackagedTask[bool] - - def open_scratch_buffer(title: str, content: str, syntax: str) -> None: - v = self.window.new_file(syntax=syntax, flags=flags) - # Note: the __init__ of ViewEventListeners is invoked in the next UI frame, so we can fill in the - # settings object here at our leisure. - v.settings().set("lsp_uri", uri) - v.set_scratch(True) - v.set_name(title) - v.run_command("append", {"characters": content}) - v.set_read_only(True) - if r: - center_selection(v, r) - sublime.set_timeout_async(lambda: result[1](True)) - - pair[0].then(lambda tup: sublime.set_timeout(lambda: open_scratch_buffer(*tup))) - return result[0] - return Promise.resolve(False) + return self._open_uri_with_plugin_async(self._plugin, uri, r, flags, group) + return Promise.resolve(None) + + def _open_file_uri_async( + self, + uri: DocumentUri, + r: Optional[RangeLsp] = None, + flags: int = 0, + group: int = -1 + ) -> Promise[Optional[sublime.View]]: + result = Promise.packaged_task() # type: PackagedTask[Optional[sublime.View]] + + def handle_continuation(view: Optional[sublime.View]) -> None: + if view and r: + center_selection(view, r) + sublime.set_timeout_async(lambda: result[1](view)) + + sublime.set_timeout(lambda: open_file(self.window, uri, flags, group).then(handle_continuation)) + return result[0] + + def _open_uri_with_plugin_async( + self, + plugin: AbstractPlugin, + uri: DocumentUri, + r: Optional[RangeLsp], + flags: int, + group: int, + ) -> Promise[Optional[sublime.View]]: + # I cannot type-hint an unpacked tuple + pair = Promise.packaged_task() # type: PackagedTask[Tuple[str, str, str]] + # It'd be nice to have automatic tuple unpacking continuations + callback = lambda a, b, c: pair[1]((a, b, c)) # noqa: E731 + if plugin.on_open_uri_async(uri, callback): + result = Promise.packaged_task() # type: PackagedTask[Optional[sublime.View]] + + def open_scratch_buffer(title: str, content: str, syntax: str) -> None: + v = self.window.new_file(syntax=syntax, flags=flags) + # Note: the __init__ of ViewEventListeners is invoked in the next UI frame, so we can fill in the + # settings object here at our leisure. + v.settings().set("lsp_uri", uri) + v.set_scratch(True) + v.set_name(title) + v.run_command("append", {"characters": content}) + v.set_read_only(True) + if r: + center_selection(v, r) + sublime.set_timeout_async(lambda: result[1](v)) + + pair[0].then(lambda tup: sublime.set_timeout(lambda: open_scratch_buffer(*tup))) + return result[0] + return Promise.resolve(None) def open_location_async(self, location: Union[Location, LocationLink], flags: int = 0, - group: int = -1) -> Promise[bool]: + group: int = -1) -> Promise[Optional[sublime.View]]: uri, r = get_uri_and_range_from_location(location) return self.open_uri_async(uri, r, flags, group) @@ -1479,7 +1504,7 @@ def _apply_code_action_async(self, code_action: Union[CodeAction, Error, None]) self.window.status_message("Failed to apply code action: {}".format(code_action)) return Promise.resolve(None) edit = code_action.get("edit") - promise = self._apply_workspace_edit_async(edit) if edit else Promise.resolve(None) + promise = self.apply_workspace_edit_async(edit) if edit else Promise.resolve(None) command = code_action.get("command") if isinstance(command, dict): execute_command = { @@ -1489,15 +1514,18 @@ def _apply_code_action_async(self, code_action: Union[CodeAction, Error, None]) return promise.then(lambda _: self.execute_command(execute_command, False)) return promise - def _apply_workspace_edit_async(self, edit: Any) -> Promise[None]: + def apply_workspace_edit_async(self, edit: Dict[str, Any]) -> Promise[None]: """ Apply workspace edits, and return a promise that resolves on the async thread again after the edits have been applied. """ - changes = parse_workspace_edit(edit) - return Promise.on_main_thread(None) \ - .then(lambda _: apply_workspace_edit(self.window, changes)) \ - .then(lambda _: Promise.on_async_thread(None)) + return self.apply_parsed_workspace_edits(parse_workspace_edit(edit)) + + def apply_parsed_workspace_edits(self, changes: Dict[str, List[TextEditTuple]]) -> Promise[None]: + promises = [] # type: List[Promise[None]] + for uri, edits in changes.items(): + promises.append(self.open_uri_async(uri).then(functools.partial(apply_edits, edits))) + return Promise.all(promises).then(lambda _: None) def decode_semantic_token( self, token_type_encoded: int, token_modifiers_encoded: int) -> Tuple[str, List[str], Optional[str]]: @@ -1537,7 +1565,7 @@ def m_workspace_configuration(self, params: Dict[str, Any], request_id: Any) -> def m_workspace_applyEdit(self, params: Any, request_id: Any) -> None: """handles the workspace/applyEdit request""" - self._apply_workspace_edit_async(params.get('edit', {})).then( + self.apply_workspace_edit_async(params.get('edit', {})).then( lambda _: self.send_response(Response(request_id, {"applied": True}))) def m_workspace_codeLens_refresh(self, _: Any, request_id: Any) -> None: diff --git a/plugin/core/types.py b/plugin/core/types.py index 4bce58d73..8f2b3c5cf 100644 --- a/plugin/core/types.py +++ b/plugin/core/types.py @@ -5,7 +5,7 @@ from .typing import Any, Optional, List, Dict, Generator, Callable, Iterable, Union, Set, Tuple, TypedDict, TypeVar from .typing import cast from .url import filename_to_uri -from .url import uri_to_filename +from .url import parse_uri from threading import RLock from wcmatch.glob import BRACE from wcmatch.glob import globmatch @@ -817,7 +817,9 @@ def map_client_path_to_server_uri(self, path: str) -> str: return filename_to_uri(path) def map_server_uri_to_client_path(self, uri: str) -> str: - path = uri_to_filename(uri) + scheme, path = parse_uri(uri) + if scheme != "file": + raise ValueError("{}: {} URI scheme is unsupported".format(uri, scheme)) if self.path_maps: for path_map in self.path_maps: path, mapped = path_map.map_from_remote_to_local(path) diff --git a/plugin/core/windows.py b/plugin/core/windows.py index 704312bba..e531a9163 100644 --- a/plugin/core/windows.py +++ b/plugin/core/windows.py @@ -111,11 +111,11 @@ def open_location_async( view: sublime.View, flags: int = 0, group: int = -1 - ) -> Promise[bool]: + ) -> Promise[Optional[sublime.View]]: for session in self.sessions(view): if session_name is None or session_name == session.config.name: return session.open_location_async(location, flags, group) - return Promise.resolve(False) + return Promise.resolve(None) def register_listener_async(self, listener: AbstractViewListener) -> None: set_diagnostics_count(listener.view, self.total_error_count, self.total_warning_count) diff --git a/plugin/edit.py b/plugin/edit.py index eb35ce2d6..5d81e5a19 100644 --- a/plugin/edit.py +++ b/plugin/edit.py @@ -1,9 +1,10 @@ -import sublime -import sublime_plugin -from .core.edit import sort_by_application_order, TextEditTuple +from .core.edit import TextEditTuple from .core.logging import debug -from .core.typing import List, Optional, Any, Generator +from .core.typing import List, Optional, Any, Generator, Iterable from contextlib import contextmanager +import operator +import sublime +import sublime_plugin @contextmanager @@ -29,7 +30,7 @@ def run(self, edit: Any, changes: Optional[List[TextEditTuple]] = None) -> None: with temporary_setting(self.view.settings(), "translate_tabs_to_spaces", False): view_version = self.view.change_count() last_row, _ = self.view.rowcol_utf16(self.view.size()) - for start, end, replacement, version in reversed(sort_by_application_order(changes)): + 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 @@ -53,3 +54,15 @@ def apply_change(self, region: sublime.Region, replacement: str, edit: Any) -> N self.view.replace(edit, region, replacement) else: self.view.erase(edit, region) + + +def _sort_by_application_order(changes: Iterable[TextEditTuple]) -> List[TextEditTuple]: + # The spec reads: + # > However, it is possible that multiple edits have the same start position: multiple + # > inserts, or any number of inserts followed by a single remove or replace edit. If + # > multiple inserts have the same position, the order in the array defines the order in + # > which the inserted strings appear in the resulting text. + # So we sort by start position. But if multiple text edits start at the same position, + # we use the index in the array as the key. + + return list(sorted(changes, key=operator.itemgetter(0))) diff --git a/plugin/locationpicker.py b/plugin/locationpicker.py index eefea9466..ee95d96ad 100644 --- a/plugin/locationpicker.py +++ b/plugin/locationpicker.py @@ -16,8 +16,8 @@ def open_location_async(session: Session, location: Union[Location, LocationLink if side_by_side: flags |= sublime.ADD_TO_SELECTION | sublime.SEMI_TRANSIENT - def check_success_async(success: bool) -> None: - if not success: + def check_success_async(view: Optional[sublime.View]) -> None: + if not view: sublime.error_message("Unable to open URI") session.open_location_async(location, flags).then(check_success_async) diff --git a/plugin/rename.py b/plugin/rename.py index 39ed5adae..33f0208f9 100644 --- a/plugin/rename.py +++ b/plugin/rename.py @@ -1,4 +1,3 @@ -from .core.edit import apply_workspace_edit from .core.edit import parse_workspace_edit from .core.edit import TextEditTuple from .core.panels import ensure_panel @@ -8,10 +7,16 @@ from .core.registry import get_position from .core.registry import LspTextCommand from .core.registry import windows -from .core.types import PANEL_FILE_REGEX, PANEL_LINE_REGEX +from .core.sessions import Session +from .core.types import PANEL_FILE_REGEX +from .core.types import PANEL_LINE_REGEX from .core.typing import Any, Optional, Dict, List -from .core.views import first_selection_region, range_to_region, get_line +from .core.url import parse_uri +from .core.views import first_selection_region +from .core.views import get_line +from .core.views import range_to_region from .core.views import text_document_position_params +import functools import os import sublime import sublime_plugin @@ -107,37 +112,32 @@ def run( def _do_rename(self, position: int, new_name: str) -> None: session = self.best_session(self.capability) - if session: - position_params = text_document_position_params(self.view, position) - params = { - "textDocument": position_params["textDocument"], - "position": position_params["position"], - "newName": new_name, - } - session.send_request( - Request("textDocument/rename", params, self.view, progress=True), - # This has to run on the main thread due to calling apply_workspace_edit - lambda r: sublime.set_timeout(lambda: self.on_rename_result(r)) - ) - - def on_rename_result(self, response: Any) -> None: - window = self.view.window() - if window: - if response: - changes = parse_workspace_edit(response) - file_count = len(changes.keys()) - if file_count > 1: - total_changes = sum(map(len, changes.values())) - message = "Replace {} occurrences across {} files?".format(total_changes, file_count) - choice = sublime.yes_no_cancel_dialog(message, "Replace", "Dry Run") - if choice == sublime.DIALOG_YES: - apply_workspace_edit(window, changes) - elif choice == sublime.DIALOG_NO: - self._render_rename_panel(changes, total_changes, file_count) - else: - apply_workspace_edit(window, changes) - else: - window.status_message('Nothing to rename') + if not session: + return + position_params = text_document_position_params(self.view, position) + params = { + "textDocument": position_params["textDocument"], + "position": position_params["position"], + "newName": new_name, + } + request = Request("textDocument/rename", params, self.view, progress=True) + session.send_request(request, functools.partial(self._on_rename_result_async, session)) + + def _on_rename_result_async(self, session: Session, response: Any) -> None: + if not response: + return session.window.status_message('Nothing to rename') + changes = parse_workspace_edit(response) + count = len(changes.keys()) + if count == 1: + session.apply_parsed_workspace_edits(changes) + return + total_changes = sum(map(len, changes.values())) + message = "Replace {} occurrences across {} files?".format(total_changes, count) + choice = sublime.yes_no_cancel_dialog(message, "Replace", "Dry Run") + if choice == sublime.DIALOG_YES: + session.apply_parsed_workspace_edits(changes) + elif choice == sublime.DIALOG_NO: + self._render_rename_panel(changes, total_changes, count) def on_prepare_result(self, response: Any, pos: int) -> None: if response is None: @@ -169,7 +169,7 @@ def _get_relative_path(self, file_path: str) -> str: def _render_rename_panel( self, - changes: Dict[str, List[TextEditTuple]], + changes_per_uri: Dict[str, List[TextEditTuple]], total_changes: int, file_count: int ) -> None: @@ -180,11 +180,18 @@ def _render_rename_panel( if not panel: return to_render = [] # type: List[str] - for file, file_changes in changes.items(): - to_render.append('{}:'.format(self._get_relative_path(file))) - for edit in file_changes: + for uri, changes in changes_per_uri.items(): + scheme, file = parse_uri(uri) + if scheme == "file": + to_render.append('{}:'.format(self._get_relative_path(file))) + else: + to_render.append('{}:'.format(uri)) + for edit in changes: start = edit[0] - line_content = get_line(window, file, start[0]) + if scheme == "file": + line_content = get_line(window, file, start[0]) + else: + line_content = '' to_render.append(" {:>4}:{:<4} {}".format(start[0] + 1, start[1] + 1, line_content)) to_render.append("") # this adds a spacing between filenames characters = "\n".join(to_render) diff --git a/stubs/sublime.pyi b/stubs/sublime.pyi index ca11aca02..5737d52b5 100644 --- a/stubs/sublime.pyi +++ b/stubs/sublime.pyi @@ -864,7 +864,7 @@ class View: def show(self, x: Union[Selection, Region, int], show_surrounds: bool = ...) -> None: ... - def show_at_center(self, x: Union[Selection, Region, int]) -> None: + def show_at_center(self, x: Union[Selection, Region, int], animate: bool = True) -> None: ... def viewport_position(self) -> Tuple[int, int]: diff --git a/tests/test_edit.py b/tests/test_edit.py index 9be7c14d7..aedeba603 100644 --- a/tests/test_edit.py +++ b/tests/test_edit.py @@ -1,4 +1,5 @@ -from LSP.plugin.core.edit import sort_by_application_order, parse_workspace_edit, parse_text_edit +from LSP.plugin.core.edit import parse_workspace_edit, parse_text_edit +from LSP.plugin.edit import _sort_by_application_order as sort_by_application_order from LSP.plugin.core.url import filename_to_uri from LSP.plugin.edit import temporary_setting from test_protocol import LSP_RANGE @@ -176,34 +177,34 @@ def test_parse_no_changes_from_lsp(self): def test_parse_changes_from_lsp(self): edit = parse_workspace_edit(LSP_EDIT_CHANGES) - self.assertIn(FILENAME, edit) + self.assertIn(URI, edit) self.assertEqual(len(edit), 1) - self.assertEqual(len(edit[FILENAME]), 1) + self.assertEqual(len(edit[URI]), 1) def test_parse_document_changes_from_lsp(self): edit = parse_workspace_edit(LSP_EDIT_DOCUMENT_CHANGES) - self.assertIn(FILENAME, edit) + self.assertIn(URI, edit) self.assertEqual(len(edit), 1) - self.assertEqual(len(edit[FILENAME]), 1) + self.assertEqual(len(edit[URI]), 1) def test_protocol_violation(self): # This should ignore the None in 'changes' edit = parse_workspace_edit(LSP_EDIT_DOCUMENT_CHANGES_2) - self.assertIn(FILENAME, edit) + self.assertIn(URI, edit) self.assertEqual(len(edit), 1) - self.assertEqual(len(edit[FILENAME]), 1) + self.assertEqual(len(edit[URI]), 1) def test_no_clobbering_of_previous_edits(self): edit = parse_workspace_edit(LSP_EDIT_DOCUMENT_CHANGES_3) - self.assertIn(FILENAME, edit) + self.assertIn(URI, edit) self.assertEqual(len(edit), 1) - self.assertEqual(len(edit[FILENAME]), 5) + self.assertEqual(len(edit[URI]), 5) def test_prefers_document_edits_over_changes(self): edit = parse_workspace_edit(LSP_EDIT_DOCUMENT_CHANGES_4) - self.assertIn(FILENAME, edit) + self.assertIn(URI, edit) self.assertEqual(len(edit), 1) - self.assertEqual(len(edit[FILENAME]), 1) # not 3 + self.assertEqual(len(edit[URI]), 1) # not 3 class SortByApplicationOrderTests(unittest.TestCase): @@ -225,7 +226,7 @@ def test_sorts_in_application_order(self): def test_sorts_in_application_order2(self): edits = parse_workspace_edit(LSP_EDIT_DOCUMENT_CHANGES_3) - sorted_edits = list(reversed(sort_by_application_order(edits[FILENAME]))) + sorted_edits = list(reversed(sort_by_application_order(edits[URI]))) self.assertEqual(sorted_edits[0][0], (39, 26)) self.assertEqual(sorted_edits[0][1], (39, 30)) self.assertEqual(sorted_edits[1][0], (27, 28)) diff --git a/tests/test_url.py b/tests/test_url.py index 1c8d47af9..1409f4892 100644 --- a/tests/test_url.py +++ b/tests/test_url.py @@ -1,11 +1,11 @@ -from LSP.plugin.core.url import filename_to_uri, parse_uri -from LSP.plugin.core.url import uri_to_filename +from LSP.plugin.core.url import filename_to_uri +from LSP.plugin.core.url import parse_uri from LSP.plugin.core.url import view_to_uri +import os +import sublime import sys import unittest import unittest.mock -import sublime -import os @unittest.skipUnless(sys.platform.startswith("win"), "requires Windows") @@ -15,11 +15,11 @@ def test_converts_path_to_uri(self): self.assertEqual("file:///C:/dir%20ectory/file.txt", filename_to_uri("c:\\dir ectory\\file.txt")) def test_converts_uri_to_path(self): - self.assertEqual("C:\\dir ectory\\file.txt", uri_to_filename("file:///c:/dir ectory/file.txt")) + self.assertEqual("C:\\dir ectory\\file.txt", parse_uri("file:///c:/dir ectory/file.txt")[1]) def test_converts_encoded_bad_drive_uri_to_path(self): # url2pathname does not understand %3A - self.assertEqual("C:\\dir ectory\\file.txt", uri_to_filename("file:///c%3A/dir%20ectory/file.txt")) + self.assertEqual("C:\\dir ectory\\file.txt", parse_uri("file:///c%3A/dir%20ectory/file.txt")[1]) def test_view_to_uri_with_valid_filename(self): view = sublime.active_window().active_view() @@ -43,7 +43,7 @@ def test_converts_path_to_uri(self): self.assertEqual("file:///dir%20ectory/file.txt", filename_to_uri("/dir ectory/file.txt")) def test_converts_uri_to_path(self): - self.assertEqual("/dir ectory/file.txt", uri_to_filename("file:///dir ectory/file.txt")) + self.assertEqual("/dir ectory/file.txt", parse_uri("file:///dir ectory/file.txt")[1]) def test_view_to_uri_with_valid_filename(self): view = sublime.active_window().active_view()