From 9bbf1021f7176deb58b935a97602bd2fa1a070e8 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sat, 13 Jan 2024 23:39:23 +0100 Subject: [PATCH 1/6] refactor: make LspApplyDocumentEditCommand take plain TextEdits --- plugin/__init__.py | 2 + plugin/color.py | 4 +- plugin/completion.py | 5 +- plugin/core/edit.py | 52 ++++++-------- plugin/core/sessions.py | 21 ++++-- plugin/edit.py | 33 ++++++--- plugin/formatting.py | 23 +++--- plugin/inlay_hint.py | 4 +- plugin/rename.py | 14 ++-- tests/test_edit.py | 129 ++++++++++++++++------------------ tests/test_single_document.py | 34 +++++---- 11 files changed, 164 insertions(+), 157 deletions(-) diff --git a/plugin/__init__.py b/plugin/__init__.py index 5899c023c..7be37e25e 100644 --- a/plugin/__init__.py +++ b/plugin/__init__.py @@ -1,5 +1,6 @@ from .core.collections import DottedDict from .core.css import css +from .core.edit import apply_text_edits from .core.file_watcher import FileWatcher from .core.file_watcher import FileWatcherEvent from .core.file_watcher import FileWatcherEventType @@ -28,6 +29,7 @@ __all__ = [ '__version__', 'AbstractPlugin', + 'apply_text_edits', 'ClientConfig', 'css', 'DottedDict', diff --git a/plugin/color.py b/plugin/color.py index a450df633..c629c01df 100644 --- a/plugin/color.py +++ b/plugin/color.py @@ -1,4 +1,4 @@ -from .core.edit import parse_text_edit +from .core.edit import apply_text_edits from .core.protocol import ColorInformation from .core.protocol import ColorPresentation from .core.protocol import ColorPresentationParams @@ -58,4 +58,4 @@ def _on_select(self, index: int) -> None: if index > -1: color_pres = self._filtered_response[index] text_edit = color_pres.get('textEdit') or {'range': self._range, 'newText': color_pres['label']} - self.view.run_command('lsp_apply_document_edit', {'changes': [parse_text_edit(text_edit, self._version)]}) + apply_text_edits(self.view, [text_edit], required_view_version=self._version) diff --git a/plugin/completion.py b/plugin/completion.py index 2169eb097..77d919d41 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -1,5 +1,5 @@ from .core.constants import COMPLETION_KINDS -from .core.edit import parse_text_edit +from .core.edit import apply_text_edits from .core.logging import debug from .core.promise import Promise from .core.protocol import CompletionEditRange @@ -371,8 +371,7 @@ def _on_resolved_async(self, session_name: str, item: CompletionItem) -> None: def _on_resolved(self, session_name: str, item: CompletionItem) -> None: additional_edits = item.get('additionalTextEdits') if additional_edits: - edits = [parse_text_edit(additional_edit) for additional_edit in additional_edits] - self.view.run_command("lsp_apply_document_edit", {'changes': edits}) + apply_text_edits(self.view, additional_edits) command = item.get("command") if command: debug('Running server command "{}" for view {}'.format(command, self.view.id())) diff --git a/plugin/core/edit.py b/plugin/core/edit.py index d32fc6a49..50fbc7a36 100644 --- a/plugin/core/edit.py +++ b/plugin/core/edit.py @@ -1,21 +1,17 @@ from .logging import debug -from .open import open_file -from .promise import Promise from .protocol import Position from .protocol import TextEdit from .protocol import UINT_MAX from .protocol import WorkspaceEdit from .typing import List, Dict, Optional, Tuple -from functools import partial import sublime -# tuple of start, end, newText, version -TextEditTuple = Tuple[Tuple[int, int], Tuple[int, int], str, Optional[int]] +WorkspaceChanges = Dict[str, Tuple[List[TextEdit], Optional[int]]] -def parse_workspace_edit(workspace_edit: WorkspaceEdit) -> Dict[str, List[TextEditTuple]]: - changes = {} # type: Dict[str, List[TextEditTuple]] +def parse_workspace_edit(workspace_edit: WorkspaceEdit) -> WorkspaceChanges: + changes = {} # type: WorkspaceChanges document_changes = workspace_edit.get('documentChanges') if isinstance(document_changes, list): for document_change in document_changes: @@ -26,13 +22,13 @@ def parse_workspace_edit(workspace_edit: WorkspaceEdit) -> Dict[str, List[TextEd 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, []).extend(text_edit) + edits = document_change.get('edits') + changes.setdefault(uri, ([], version))[0].extend(edits) else: raw_changes = workspace_edit.get('changes') if isinstance(raw_changes, dict): - for uri, uri_changes in raw_changes.items(): - changes[uri] = list(parse_text_edit(change) for change in uri_changes) + for uri, edits in raw_changes.items(): + changes[uri] = (edits, None) return changes @@ -40,24 +36,18 @@ def parse_range(range: Position) -> Tuple[int, int]: return range['line'], min(UINT_MAX, range['character']) -def parse_text_edit(text_edit: TextEdit, version: Optional[int] = None) -> TextEditTuple: - return ( - parse_range(text_edit['range']['start']), - parse_range(text_edit['range']['end']), - # Strip away carriage returns -- SublimeText takes care of that. - text_edit.get('newText', '').replace("\r", ""), - version +def apply_text_edits( + view: sublime.View, + edits: Optional[List[TextEdit]], + *, + process_placeholders: Optional[bool] = False, + required_view_version: Optional[int] = None +) -> None: + view.run_command( + 'lsp_apply_document_edit', + { + 'changes': edits or [], + 'process_placeholders': process_placeholders, + 'required_view_version': required_view_version, + } ) - - -def apply_workspace_edit(window: sublime.Window, changes: Dict[str, List[TextEditTuple]]) -> Promise: - """ - 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: - 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/sessions.py b/plugin/core/sessions.py index 7435cb12b..6b6cf5eb2 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -1,9 +1,9 @@ from .collections import DottedDict from .constants import SEMANTIC_TOKENS_MAP from .diagnostics_storage import DiagnosticsStorage -from .edit import apply_edits +from .edit import apply_text_edits from .edit import parse_workspace_edit -from .edit import TextEditTuple +from .edit import WorkspaceChanges from .file_watcher import DEFAULT_KIND from .file_watcher import file_watcher_event_type_to_lsp_file_change_type from .file_watcher import FileWatcher @@ -67,6 +67,7 @@ from .protocol import SymbolTag from .protocol import TextDocumentClientCapabilities from .protocol import TextDocumentSyncKind +from .protocol import TextEdit from .protocol import TokenFormat from .protocol import UnregistrationParams from .protocol import WindowClientCapabilities @@ -1768,12 +1769,22 @@ def apply_workspace_edit_async(self, edit: WorkspaceEdit) -> Promise[None]: """ return self.apply_parsed_workspace_edits(parse_workspace_edit(edit)) - def apply_parsed_workspace_edits(self, changes: Dict[str, List[TextEditTuple]]) -> Promise[None]: + def apply_parsed_workspace_edits(self, changes: WorkspaceChanges) -> 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))) + for uri, (edits, view_version) in changes.items(): + promises.append( + self.open_uri_async(uri).then(functools.partial(self._apply_text_edits, edits, view_version, uri)) + ) return Promise.all(promises).then(lambda _: None) + def _apply_text_edits( + self, edits: List[TextEdit], view_version: Optional[int], uri: str, view: Optional[sublime.View] + ) -> None: + if view is None or not view.is_valid(): + print('LSP: ignoring edits due to no view for uri: {}'.format(uri)) + return + apply_text_edits(view, edits, required_view_version=view_version) + def decode_semantic_token( self, token_type_encoded: int, token_modifiers_encoded: int) -> Tuple[str, List[str], Optional[str]]: types_legend = tuple(cast(List[str], self.get_capability('semanticTokensProvider.legend.tokenTypes'))) diff --git a/plugin/edit.py b/plugin/edit.py index 23fa4595f..f0f5dd15a 100644 --- a/plugin/edit.py +++ b/plugin/edit.py @@ -1,5 +1,5 @@ -from .core.edit import TextEditTuple -from .core.logging import debug +from .core.edit import parse_range +from .core.protocol import TextEdit from .core.typing import List, Optional, Any, Generator, Iterable, Tuple from contextlib import contextmanager import operator @@ -8,6 +8,9 @@ import sublime_plugin +TextEditTuple = Tuple[Tuple[int, int], Tuple[int, int], str] + + @contextmanager def temporary_setting(settings: sublime.Settings, key: str, val: Any) -> Generator[None, None, None]: prev_val = None @@ -25,20 +28,25 @@ class LspApplyDocumentEditCommand(sublime_plugin.TextCommand): re_placeholder = re.compile(r'\$(0|\{0:([^}]*)\})') def run( - self, edit: sublime.Edit, changes: Optional[List[TextEditTuple]] = None, process_placeholders: bool = False + self, + edit: sublime.Edit, + changes: List[TextEdit], + required_view_version: Optional[int] = None, + process_placeholders: bool = False, ) -> 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 + view_version = self.view.change_count() + if required_view_version is not None and required_view_version != view_version: + print('LSP: ignoring edit due to non-matching document version') + return + edits = [_parse_text_edit(change) for change in changes or []] 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()) placeholder_region_count = 0 - 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 + for start, end, replacement in reversed(_sort_by_application_order(edits)): placeholder_region = None # type: Optional[Tuple[Tuple[int, int], Tuple[int, int]]] if process_placeholders and replacement: parsed = self.parse_snippet(replacement) @@ -96,6 +104,15 @@ def parse_snippet(self, replacement: str) -> Optional[Tuple[str, Tuple[int, int] return (new_replacement, placeholder_start_and_length) +def _parse_text_edit(text_edit: TextEdit) -> TextEditTuple: + return ( + parse_range(text_edit['range']['start']), + parse_range(text_edit['range']['end']), + # Strip away carriage returns -- SublimeText takes care of that. + text_edit.get('newText', '').replace("\r", "") + ) + + 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 diff --git a/plugin/formatting.py b/plugin/formatting.py index 9542732b4..a341b9e02 100644 --- a/plugin/formatting.py +++ b/plugin/formatting.py @@ -1,5 +1,5 @@ from .core.collections import DottedDict -from .core.edit import parse_text_edit +from .core.edit import apply_text_edits from .core.promise import Promise from .core.protocol import Error from .core.protocol import TextDocumentSaveReason @@ -50,13 +50,6 @@ def format_document(text_command: LspTextCommand, formatter: Optional[str] = Non return Promise.resolve(None) -def apply_text_edits_to_view( - response: Optional[List[TextEdit]], view: sublime.View, *, process_placeholders: bool = False -) -> None: - edits = list(parse_text_edit(change) for change in response) if response else [] - view.run_command('lsp_apply_document_edit', {'changes': edits, 'process_placeholders': process_placeholders}) - - class WillSaveWaitTask(SaveTask): @classmethod def is_applicable(cls, view: sublime.View) -> bool: @@ -85,9 +78,9 @@ def _will_save_wait_until_async(self, session: Session) -> None: self._on_response, lambda error: self._on_response(None)) - def _on_response(self, response: Any) -> None: - if response and not self._cancelled: - apply_text_edits_to_view(response, self._task_runner.view) + def _on_response(self, response: FormatResponse) -> None: + if response and not isinstance(response, Error) and not self._cancelled: + apply_text_edits(self._task_runner.view, response) sublime.set_timeout_async(self._handle_next_session_async) @@ -108,7 +101,7 @@ def run_async(self) -> None: def _on_response(self, response: FormatResponse) -> None: if response and not isinstance(response, Error) and not self._cancelled: - apply_text_edits_to_view(response, self._task_runner.view) + apply_text_edits(self._task_runner.view, response) sublime.set_timeout_async(self._on_complete) @@ -143,7 +136,7 @@ def run(self, edit: sublime.Edit, event: Optional[dict] = None, select: bool = F def on_result(self, result: FormatResponse) -> None: if result and not isinstance(result, Error): - apply_text_edits_to_view(result, self.view) + apply_text_edits(self.view, result) def select_formatter(self, base_scope: str, session_names: List[str]) -> None: window = self.view.window() @@ -194,12 +187,12 @@ def run(self, edit: sublime.Edit, event: Optional[dict] = None) -> None: selection = first_selection_region(self.view) if session and selection is not None: req = text_document_range_formatting(self.view, selection) - session.send_request(req, lambda response: apply_text_edits_to_view(response, self.view)) + session.send_request(req, lambda response: apply_text_edits(self.view, response)) elif self.view.has_non_empty_selection_region(): session = self.best_session('documentRangeFormattingProvider.rangesSupport') if session: req = text_document_ranges_formatting(self.view) - session.send_request(req, lambda response: apply_text_edits_to_view(response, self.view)) + session.send_request(req, lambda response: apply_text_edits(self.view, response)) class LspFormatCommand(LspTextCommand): diff --git a/plugin/inlay_hint.py b/plugin/inlay_hint.py index 7eecf46a0..c37bcf801 100644 --- a/plugin/inlay_hint.py +++ b/plugin/inlay_hint.py @@ -1,4 +1,5 @@ from .core.css import css +from .core.edit import apply_text_edits from .core.protocol import InlayHint from .core.protocol import InlayHintLabelPart from .core.protocol import MarkupContent @@ -9,7 +10,6 @@ from .core.settings import userprefs from .core.typing import cast, Optional, Union from .core.views import position_to_offset -from .formatting import apply_text_edits_to_view import html import sublime import uuid @@ -69,7 +69,7 @@ def handle_inlay_hint_text_edits(self, session_name: str, inlay_hint: InlayHint, return for sb in session.session_buffers_async(): sb.remove_inlay_hint_phantom(phantom_uuid) - apply_text_edits_to_view(text_edits, self.view) + apply_text_edits(self.view, text_edits) def handle_label_part_command(self, session_name: str, label_part: Optional[InlayHintLabelPart] = None) -> None: if not label_part: diff --git a/plugin/rename.py b/plugin/rename.py index 4fa41c190..caac622d5 100644 --- a/plugin/rename.py +++ b/plugin/rename.py @@ -1,5 +1,6 @@ +from .core.edit import parse_range from .core.edit import parse_workspace_edit -from .core.edit import TextEditTuple +from .core.edit import WorkspaceChanges from .core.protocol import PrepareRenameParams from .core.protocol import PrepareRenameResult from .core.protocol import Range @@ -173,12 +174,7 @@ def _get_relative_path(self, file_path: str) -> str: base_dir = wm.get_project_path(file_path) return os.path.relpath(file_path, base_dir) if base_dir else file_path - def _render_rename_panel( - self, - changes_per_uri: Dict[str, List[TextEditTuple]], - total_changes: int, - file_count: int - ) -> None: + def _render_rename_panel(self, changes_per_uri: WorkspaceChanges, total_changes: int, file_count: int) -> None: wm = windows.lookup(self.view.window()) if not wm: return @@ -186,14 +182,14 @@ def _render_rename_panel( if not panel: return to_render = [] # type: List[str] - for uri, changes in changes_per_uri.items(): + 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] + start = parse_range(edit['range']['start']) if scheme == "file": line_content = get_line(wm.window, file, start[0]) else: diff --git a/tests/test_edit.py b/tests/test_edit.py index 1e0a94c1a..bd5d3b108 100644 --- a/tests/test_edit.py +++ b/tests/test_edit.py @@ -1,40 +1,39 @@ -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 import apply_text_edits +from LSP.plugin.core.edit import parse_workspace_edit +from LSP.plugin.core.protocol import TextDocumentEdit, TextEdit, WorkspaceEdit +from LSP.plugin.core.typing import List from LSP.plugin.core.url import filename_to_uri from LSP.plugin.core.views import entire_content +from LSP.plugin.edit import _parse_text_edit as parse_text_edit +from LSP.plugin.edit import _sort_by_application_order as sort_by_application_order from LSP.plugin.edit import temporary_setting from setup import TextDocumentTestCase from test_protocol import LSP_RANGE import sublime import unittest -TYPE_CHECKING = False -if TYPE_CHECKING: - from typing import List, Dict, Optional, Any, Iterable - assert List and Dict and Optional and Any and Iterable - -LSP_TEXT_EDIT = dict(newText='newText\r\n', range=LSP_RANGE) FILENAME = 'C:\\file.py' if sublime.platform() == "windows" else '/file.py' URI = filename_to_uri(FILENAME) -LSP_EDIT_CHANGES = {'changes': {URI: [LSP_TEXT_EDIT]}} +LSP_TEXT_EDIT = { + 'newText': 'newText\r\n', + 'range': LSP_RANGE +} # type: TextEdit -LSP_EDIT_DOCUMENT_CHANGES = { - 'documentChanges': [{ - 'textDocument': {'uri': URI}, - 'edits': [LSP_TEXT_EDIT] - }] -} +LSP_EDIT_CHANGES = { + 'changes': {URI: [LSP_TEXT_EDIT]} +} # type: WorkspaceEdit -LSP_EDIT_DOCUMENT_CHANGES_2 = { - 'changes': None, - 'documentChanges': [{ - 'textDocument': {'uri': URI}, - 'edits': [LSP_TEXT_EDIT] - }] -} +LSP_TEXT_DOCUMENT_EDIT = { + 'textDocument': {'uri': URI, 'version': None}, + 'edits': [LSP_TEXT_EDIT] +} # type: TextDocumentEdit + +LSP_EDIT_DOCUMENT_CHANGES = { + 'documentChanges': [LSP_TEXT_DOCUMENT_EDIT] +} # type: WorkspaceEdit # Check that processing document changes does not result in clobbering. -LSP_EDIT_DOCUMENT_CHANGES_3 = { +LSP_EDIT_DOCUMENT_CHANGES_2 = { "documentChanges": [ { "edits": [ @@ -142,9 +141,9 @@ } } ] -} +} # type: WorkspaceEdit -LSP_EDIT_DOCUMENT_CHANGES_4 = { +LSP_EDIT_DOCUMENT_CHANGES_3 = { 'changes': { "file:///asdf/foo/bar": [ {"newText": "hello there", "range": LSP_RANGE}, @@ -152,61 +151,50 @@ {"newText": "kenobi", "range": LSP_RANGE} ] }, - 'documentChanges': [{ - 'textDocument': {'uri': URI}, - 'edits': [LSP_TEXT_EDIT] - }] -} + 'documentChanges': [LSP_TEXT_DOCUMENT_EDIT] +} # type: WorkspaceEdit class TextEditTests(unittest.TestCase): def test_parse_from_lsp(self): - (start, end, newText, version) = parse_text_edit(LSP_TEXT_EDIT, 0) + (start, end, newText) = parse_text_edit(LSP_TEXT_EDIT) self.assertEqual(newText, 'newText\n') # Without the \r self.assertEqual(start[0], 10) self.assertEqual(start[1], 4) self.assertEqual(end[0], 11) self.assertEqual(end[1], 3) - self.assertEqual(version, 0) class WorkspaceEditTests(unittest.TestCase): def test_parse_no_changes_from_lsp(self): - edit = parse_workspace_edit(dict()) - self.assertEqual(len(edit), 0) + changes = parse_workspace_edit({}) + self.assertEqual(len(changes), 0) def test_parse_changes_from_lsp(self): - edit = parse_workspace_edit(LSP_EDIT_CHANGES) - self.assertIn(URI, edit) - self.assertEqual(len(edit), 1) - self.assertEqual(len(edit[URI]), 1) + changes = parse_workspace_edit(LSP_EDIT_CHANGES) + self.assertIn(URI, changes) + self.assertEqual(len(changes), 1) + self.assertEqual(len(changes[URI][0]), 1) def test_parse_document_changes_from_lsp(self): - edit = parse_workspace_edit(LSP_EDIT_DOCUMENT_CHANGES) - self.assertIn(URI, edit) - self.assertEqual(len(edit), 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(URI, edit) - self.assertEqual(len(edit), 1) - self.assertEqual(len(edit[URI]), 1) + changes = parse_workspace_edit(LSP_EDIT_DOCUMENT_CHANGES) + self.assertIn(URI, changes) + self.assertEqual(len(changes), 1) + self.assertEqual(len(changes[URI][0]), 1) def test_no_clobbering_of_previous_edits(self): - edit = parse_workspace_edit(LSP_EDIT_DOCUMENT_CHANGES_3) - self.assertIn(URI, edit) - self.assertEqual(len(edit), 1) - self.assertEqual(len(edit[URI]), 5) + changes = parse_workspace_edit(LSP_EDIT_DOCUMENT_CHANGES_2) + self.assertIn(URI, changes) + self.assertEqual(len(changes), 1) + self.assertEqual(len(changes[URI][0]), 5) def test_prefers_document_edits_over_changes(self): - edit = parse_workspace_edit(LSP_EDIT_DOCUMENT_CHANGES_4) - self.assertIn(URI, edit) - self.assertEqual(len(edit), 1) - self.assertEqual(len(edit[URI]), 1) # not 3 + changes = parse_workspace_edit(LSP_EDIT_DOCUMENT_CHANGES_3) + self.assertIn(URI, changes) + self.assertEqual(len(changes), 1) + self.assertEqual(len(changes[URI][0]), 1) # not 3 class SortByApplicationOrderTests(unittest.TestCase): @@ -227,8 +215,11 @@ def test_sorts_in_application_order(self): self.assertEqual(sorted_edits[2][2], 'c') 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[URI]))) + changes = parse_workspace_edit(LSP_EDIT_DOCUMENT_CHANGES_2) + (edits, version) = changes[URI] + self.assertEqual(version, 6) + parsed_edits = [parse_text_edit(edit) for edit in edits] + sorted_edits = list(reversed(sort_by_application_order(parsed_edits))) self.assertEqual(sorted_edits[0][0], (39, 26)) self.assertEqual(sorted_edits[0][1], (39, 30)) self.assertEqual(sorted_edits[1][0], (27, 28)) @@ -239,7 +230,7 @@ class ApplyDocumentEditTestCase(TextDocumentTestCase): def test_applies_text_edit(self) -> None: self.insert_characters('abc') - edit = parse_text_edit({ + edits = [{ 'newText': 'x$0y', 'range': { 'start': { @@ -251,13 +242,13 @@ def test_applies_text_edit(self) -> None: 'character': 2, } } - }) - self.view.run_command("lsp_apply_document_edit", {"changes": [edit]}) + }] # type: List[TextEdit] + apply_text_edits(self.view, edits) self.assertEquals(entire_content(self.view), 'ax$0yc') def test_applies_text_edit_with_placeholder(self) -> None: self.insert_characters('abc') - edit = parse_text_edit({ + edits = [{ 'newText': 'x$0y', 'range': { 'start': { @@ -269,15 +260,15 @@ def test_applies_text_edit_with_placeholder(self) -> None: 'character': 2, } } - }) - self.view.run_command('lsp_apply_document_edit', {'changes': [edit], 'process_placeholders': True}) + }] # type: List[TextEdit] + apply_text_edits(self.view, edits, process_placeholders=True) self.assertEquals(entire_content(self.view), 'axyc') self.assertEqual(len(self.view.sel()), 1) self.assertEqual(self.view.sel()[0], sublime.Region(2, 2)) def test_applies_multiple_text_edits_with_placeholders(self) -> None: self.insert_characters('ab') - newline_edit = parse_text_edit({ + newline_edit = { 'newText': '\n$0', 'range': { 'start': { @@ -289,9 +280,9 @@ def test_applies_multiple_text_edits_with_placeholders(self) -> None: 'character': 1, } } - }) - edits = [newline_edit, newline_edit] - self.view.run_command('lsp_apply_document_edit', {'changes': edits, 'process_placeholders': True}) + } # type: TextEdit + edits = [newline_edit, newline_edit] # type: List[TextEdit] + apply_text_edits(self.view, edits, process_placeholders=True) self.assertEquals(entire_content(self.view), 'a\n\nb') self.assertEqual(len(self.view.sel()), 2) self.assertEqual(self.view.sel()[0], sublime.Region(2, 2)) diff --git a/tests/test_single_document.py b/tests/test_single_document.py index 74c3f7f78..b24e6ca00 100644 --- a/tests/test_single_document.py +++ b/tests/test_single_document.py @@ -1,5 +1,6 @@ from copy import deepcopy -from LSP.plugin import Request +from LSP.plugin import apply_text_edits, Request +from LSP.plugin.core.protocol import UINT_MAX from LSP.plugin.core.url import filename_to_uri from LSP.plugin.core.views import entire_content from LSP.plugin.hover import _test_contents @@ -54,21 +55,28 @@ class SingleDocumentTestCase(TextDocumentTestCase): - def test_did_open(self) -> 'Generator': + def test_did_open(self) -> None: # Just the existence of this method checks "initialize" -> "initialized" -> "textDocument/didOpen" # -> "shutdown" -> client shut down pass - def test_out_of_bounds_column_for_text_document_edit(self) -> 'Generator': + def test_out_of_bounds_column_for_text_document_edit(self) -> None: self.insert_characters("a\nb\nc\n") - self.view.run_command("lsp_apply_document_edit", {"changes": [ - ( - (1, 0), # start row-col - (1, 10000), # end row-col (the col offset is out of bounds intentionally) - "hello there", # new text - None # version - ) - ]}) + apply_text_edits(self.view, [ + { + 'newText': 'hello there', + 'range': { + 'start': { + 'line': 1, + 'character': 0, + }, + 'end': { + 'line': 1, + 'character': 10000, + } + } + }, + ]) self.assertEqual(entire_content(self.view), "a\nhello there\nc\n") def test_did_close(self) -> 'Generator': @@ -326,8 +334,8 @@ def test_rename(self) -> 'Generator': 'range': { 'start': {'character': 0, 'line': 2}, - # Check that lsp_apply_document_edit guards for overflow by using sys.maxsize + 1 - 'end': {'character': sys.maxsize + 1, 'line': 2} + # Check that lsp_apply_document_edit guards for overflow over LSP spec limit of UINT_MAX + 'end': {'character': UINT_MAX + 1, 'line': 2} }, 'newText': 'bar' } From 63f8a57165f5e9eeae5478d7ea5d72241117102c Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sat, 13 Jan 2024 23:50:36 +0100 Subject: [PATCH 2/6] remove unused --- plugin/formatting.py | 2 +- plugin/rename.py | 2 +- tests/test_single_document.py | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/plugin/formatting.py b/plugin/formatting.py index a341b9e02..4259724f7 100644 --- a/plugin/formatting.py +++ b/plugin/formatting.py @@ -8,7 +8,7 @@ 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 +from .core.typing import Callable, List, Optional, Iterator, Union from .core.views import entire_content_region from .core.views import first_selection_region from .core.views import has_single_nonempty_selection diff --git a/plugin/rename.py b/plugin/rename.py index caac622d5..ae5e8efdb 100644 --- a/plugin/rename.py +++ b/plugin/rename.py @@ -11,7 +11,7 @@ from .core.registry import LspTextCommand from .core.registry import windows from .core.sessions import Session -from .core.typing import Any, Optional, Dict, List, TypeGuard +from .core.typing import Any, Optional, List, TypeGuard from .core.typing import cast from .core.url import parse_uri from .core.views import first_selection_region diff --git a/tests/test_single_document.py b/tests/test_single_document.py index b24e6ca00..4d1bb10d3 100644 --- a/tests/test_single_document.py +++ b/tests/test_single_document.py @@ -9,7 +9,6 @@ from setup import YieldPromise import os import sublime -import sys try: From a52f0407ad6ddbc4f8798429e9136581ccbc3d6e Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sun, 14 Jan 2024 13:36:07 +0100 Subject: [PATCH 3/6] early exit --- plugin/core/edit.py | 2 ++ plugin/core/sessions.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/plugin/core/edit.py b/plugin/core/edit.py index 50fbc7a36..3ed227292 100644 --- a/plugin/core/edit.py +++ b/plugin/core/edit.py @@ -43,6 +43,8 @@ def apply_text_edits( process_placeholders: Optional[bool] = False, required_view_version: Optional[int] = None ) -> None: + if not edits: + return view.run_command( 'lsp_apply_document_edit', { diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index 6b6cf5eb2..90eaf6c84 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -977,7 +977,7 @@ def on_workspace_configuration(self, params: Dict, configuration: Any) -> Any: """ return configuration - def on_pre_server_command(self, command: Mapping[str, Any], done_callback: Callable[[], None]) -> bool: + def on_pre_server_command(self, command: ExecuteCommandParams, done_callback: Callable[[], None]) -> bool: """ Intercept a command that is about to be sent to the language server. From 21387d3753ac541a5cd6b6df3b9c481dcdd3a34b Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sun, 14 Jan 2024 13:37:34 +0100 Subject: [PATCH 4/6] unused --- plugin/core/sessions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index 90eaf6c84..ab493525a 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -95,7 +95,7 @@ from .types import SettingsRegistration from .types import sublime_pattern_to_glob from .types import WORKSPACE_DIAGNOSTICS_TIMEOUT -from .typing import Callable, cast, Dict, Any, Optional, List, Tuple, Generator, Type, TypeGuard, Protocol, Mapping, Set, TypeVar, Union # noqa: E501 +from .typing import Callable, cast, Dict, Any, Optional, List, Tuple, Generator, Type, TypeGuard, Protocol, Set, TypeVar, Union # noqa: E501 from .url import filename_to_uri from .url import parse_uri from .url import unparse_uri From 920a13c3c2e80391f91b16f11f005dfe74834e9c Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sun, 14 Jan 2024 14:05:54 +0100 Subject: [PATCH 5/6] make uri_from_view public --- plugin/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugin/__init__.py b/plugin/__init__.py index 7be37e25e..3f009dd68 100644 --- a/plugin/__init__.py +++ b/plugin/__init__.py @@ -23,6 +23,7 @@ from .core.url import uri_to_filename # deprecated from .core.version import __version__ from .core.views import MarkdownLangMap +from .core.views import uri_from_view from .core.workspace import WorkspaceFolder # This is the public API for LSP-* packages @@ -51,6 +52,7 @@ 'Session', 'SessionBufferProtocol', 'unregister_plugin', + 'uri_from_view', 'uri_to_filename', # deprecated 'WorkspaceFolder', ] From e9b716f8307ac45ca58af21382e06092047d91db Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sun, 14 Jan 2024 21:03:42 +0100 Subject: [PATCH 6/6] unnecessary --- plugin/core/edit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/core/edit.py b/plugin/core/edit.py index 3ed227292..48e509dfa 100644 --- a/plugin/core/edit.py +++ b/plugin/core/edit.py @@ -48,7 +48,7 @@ def apply_text_edits( view.run_command( 'lsp_apply_document_edit', { - 'changes': edits or [], + 'changes': edits, 'process_placeholders': process_placeholders, 'required_view_version': required_view_version, }