Skip to content

Commit 7ddde2d

Browse files
authored
refactor: make LspApplyDocumentEditCommand take plain TextEdits (#2393)
1 parent 7458bf8 commit 7ddde2d

File tree

11 files changed

+172
-162
lines changed

11 files changed

+172
-162
lines changed

plugin/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from .core.collections import DottedDict
22
from .core.css import css
3+
from .core.edit import apply_text_edits
34
from .core.file_watcher import FileWatcher
45
from .core.file_watcher import FileWatcherEvent
56
from .core.file_watcher import FileWatcherEventType
@@ -22,12 +23,14 @@
2223
from .core.url import uri_to_filename # deprecated
2324
from .core.version import __version__
2425
from .core.views import MarkdownLangMap
26+
from .core.views import uri_from_view
2527
from .core.workspace import WorkspaceFolder
2628

2729
# This is the public API for LSP-* packages
2830
__all__ = [
2931
'__version__',
3032
'AbstractPlugin',
33+
'apply_text_edits',
3134
'ClientConfig',
3235
'css',
3336
'DottedDict',
@@ -49,6 +52,7 @@
4952
'Session',
5053
'SessionBufferProtocol',
5154
'unregister_plugin',
55+
'uri_from_view',
5256
'uri_to_filename', # deprecated
5357
'WorkspaceFolder',
5458
]

plugin/color.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .core.edit import parse_text_edit
1+
from .core.edit import apply_text_edits
22
from .core.protocol import ColorInformation
33
from .core.protocol import ColorPresentation
44
from .core.protocol import ColorPresentationParams
@@ -58,4 +58,4 @@ def _on_select(self, index: int) -> None:
5858
if index > -1:
5959
color_pres = self._filtered_response[index]
6060
text_edit = color_pres.get('textEdit') or {'range': self._range, 'newText': color_pres['label']}
61-
self.view.run_command('lsp_apply_document_edit', {'changes': [parse_text_edit(text_edit, self._version)]})
61+
apply_text_edits(self.view, [text_edit], required_view_version=self._version)

plugin/completion.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from .core.constants import COMPLETION_KINDS
2-
from .core.edit import parse_text_edit
2+
from .core.edit import apply_text_edits
33
from .core.logging import debug
44
from .core.promise import Promise
55
from .core.protocol import CompletionEditRange
@@ -371,8 +371,7 @@ def _on_resolved_async(self, session_name: str, item: CompletionItem) -> None:
371371
def _on_resolved(self, session_name: str, item: CompletionItem) -> None:
372372
additional_edits = item.get('additionalTextEdits')
373373
if additional_edits:
374-
edits = [parse_text_edit(additional_edit) for additional_edit in additional_edits]
375-
self.view.run_command("lsp_apply_document_edit", {'changes': edits})
374+
apply_text_edits(self.view, additional_edits)
376375
command = item.get("command")
377376
if command:
378377
debug('Running server command "{}" for view {}'.format(command, self.view.id()))

plugin/core/edit.py

Lines changed: 23 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
11
from .logging import debug
2-
from .open import open_file
3-
from .promise import Promise
42
from .protocol import Position
53
from .protocol import TextEdit
64
from .protocol import UINT_MAX
75
from .protocol import WorkspaceEdit
86
from .typing import List, Dict, Optional, Tuple
9-
from functools import partial
107
import sublime
118

129

13-
# tuple of start, end, newText, version
14-
TextEditTuple = Tuple[Tuple[int, int], Tuple[int, int], str, Optional[int]]
10+
WorkspaceChanges = Dict[str, Tuple[List[TextEdit], Optional[int]]]
1511

1612

17-
def parse_workspace_edit(workspace_edit: WorkspaceEdit) -> Dict[str, List[TextEditTuple]]:
18-
changes = {} # type: Dict[str, List[TextEditTuple]]
13+
def parse_workspace_edit(workspace_edit: WorkspaceEdit) -> WorkspaceChanges:
14+
changes = {} # type: WorkspaceChanges
1915
document_changes = workspace_edit.get('documentChanges')
2016
if isinstance(document_changes, list):
2117
for document_change in document_changes:
@@ -26,38 +22,34 @@ def parse_workspace_edit(workspace_edit: WorkspaceEdit) -> Dict[str, List[TextEd
2622
text_document = document_change["textDocument"]
2723
uri = text_document['uri']
2824
version = text_document.get('version')
29-
text_edit = list(parse_text_edit(change, version) for change in document_change.get('edits'))
30-
changes.setdefault(uri, []).extend(text_edit)
25+
edits = document_change.get('edits')
26+
changes.setdefault(uri, ([], version))[0].extend(edits)
3127
else:
3228
raw_changes = workspace_edit.get('changes')
3329
if isinstance(raw_changes, dict):
34-
for uri, uri_changes in raw_changes.items():
35-
changes[uri] = list(parse_text_edit(change) for change in uri_changes)
30+
for uri, edits in raw_changes.items():
31+
changes[uri] = (edits, None)
3632
return changes
3733

3834

3935
def parse_range(range: Position) -> Tuple[int, int]:
4036
return range['line'], min(UINT_MAX, range['character'])
4137

4238

43-
def parse_text_edit(text_edit: TextEdit, version: Optional[int] = None) -> TextEditTuple:
44-
return (
45-
parse_range(text_edit['range']['start']),
46-
parse_range(text_edit['range']['end']),
47-
# Strip away carriage returns -- SublimeText takes care of that.
48-
text_edit.get('newText', '').replace("\r", ""),
49-
version
39+
def apply_text_edits(
40+
view: sublime.View,
41+
edits: Optional[List[TextEdit]],
42+
*,
43+
process_placeholders: Optional[bool] = False,
44+
required_view_version: Optional[int] = None
45+
) -> None:
46+
if not edits:
47+
return
48+
view.run_command(
49+
'lsp_apply_document_edit',
50+
{
51+
'changes': edits,
52+
'process_placeholders': process_placeholders,
53+
'required_view_version': required_view_version,
54+
}
5055
)
51-
52-
53-
def apply_workspace_edit(window: sublime.Window, changes: Dict[str, List[TextEditTuple]]) -> Promise:
54-
"""
55-
DEPRECATED: Use session.apply_workspace_edit_async instead.
56-
"""
57-
return Promise.all([open_file(window, uri).then(partial(apply_edits, edits)) for uri, edits in changes.items()])
58-
59-
60-
def apply_edits(edits: List[TextEditTuple], view: Optional[sublime.View]) -> None:
61-
if view and view.is_valid():
62-
# Text commands run blocking. After this call has returned the changes are applied.
63-
view.run_command("lsp_apply_document_edit", {"changes": edits})

plugin/core/sessions.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
from .collections import DottedDict
22
from .constants import SEMANTIC_TOKENS_MAP
33
from .diagnostics_storage import DiagnosticsStorage
4-
from .edit import apply_edits
4+
from .edit import apply_text_edits
55
from .edit import parse_workspace_edit
6-
from .edit import TextEditTuple
6+
from .edit import WorkspaceChanges
77
from .file_watcher import DEFAULT_KIND
88
from .file_watcher import file_watcher_event_type_to_lsp_file_change_type
99
from .file_watcher import FileWatcher
@@ -67,6 +67,7 @@
6767
from .protocol import SymbolTag
6868
from .protocol import TextDocumentClientCapabilities
6969
from .protocol import TextDocumentSyncKind
70+
from .protocol import TextEdit
7071
from .protocol import TokenFormat
7172
from .protocol import UnregistrationParams
7273
from .protocol import WindowClientCapabilities
@@ -94,7 +95,7 @@
9495
from .types import SettingsRegistration
9596
from .types import sublime_pattern_to_glob
9697
from .types import WORKSPACE_DIAGNOSTICS_TIMEOUT
97-
from .typing import Callable, cast, Dict, Any, Optional, List, Tuple, Generator, Type, TypeGuard, Protocol, Mapping, Set, TypeVar, Union # noqa: E501
98+
from .typing import Callable, cast, Dict, Any, Optional, List, Tuple, Generator, Type, TypeGuard, Protocol, Set, TypeVar, Union # noqa: E501
9899
from .url import filename_to_uri
99100
from .url import parse_uri
100101
from .url import unparse_uri
@@ -976,7 +977,7 @@ def on_workspace_configuration(self, params: Dict, configuration: Any) -> Any:
976977
"""
977978
return configuration
978979

979-
def on_pre_server_command(self, command: Mapping[str, Any], done_callback: Callable[[], None]) -> bool:
980+
def on_pre_server_command(self, command: ExecuteCommandParams, done_callback: Callable[[], None]) -> bool:
980981
"""
981982
Intercept a command that is about to be sent to the language server.
982983
@@ -1768,12 +1769,22 @@ def apply_workspace_edit_async(self, edit: WorkspaceEdit) -> Promise[None]:
17681769
"""
17691770
return self.apply_parsed_workspace_edits(parse_workspace_edit(edit))
17701771

1771-
def apply_parsed_workspace_edits(self, changes: Dict[str, List[TextEditTuple]]) -> Promise[None]:
1772+
def apply_parsed_workspace_edits(self, changes: WorkspaceChanges) -> Promise[None]:
17721773
promises = [] # type: List[Promise[None]]
1773-
for uri, edits in changes.items():
1774-
promises.append(self.open_uri_async(uri).then(functools.partial(apply_edits, edits)))
1774+
for uri, (edits, view_version) in changes.items():
1775+
promises.append(
1776+
self.open_uri_async(uri).then(functools.partial(self._apply_text_edits, edits, view_version, uri))
1777+
)
17751778
return Promise.all(promises).then(lambda _: None)
17761779

1780+
def _apply_text_edits(
1781+
self, edits: List[TextEdit], view_version: Optional[int], uri: str, view: Optional[sublime.View]
1782+
) -> None:
1783+
if view is None or not view.is_valid():
1784+
print('LSP: ignoring edits due to no view for uri: {}'.format(uri))
1785+
return
1786+
apply_text_edits(view, edits, required_view_version=view_version)
1787+
17771788
def decode_semantic_token(
17781789
self, token_type_encoded: int, token_modifiers_encoded: int) -> Tuple[str, List[str], Optional[str]]:
17791790
types_legend = tuple(cast(List[str], self.get_capability('semanticTokensProvider.legend.tokenTypes')))

plugin/edit.py

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
from .core.edit import TextEditTuple
2-
from .core.logging import debug
1+
from .core.edit import parse_range
2+
from .core.protocol import TextEdit
33
from .core.typing import List, Optional, Any, Generator, Iterable, Tuple
44
from contextlib import contextmanager
55
import operator
@@ -8,6 +8,9 @@
88
import sublime_plugin
99

1010

11+
TextEditTuple = Tuple[Tuple[int, int], Tuple[int, int], str]
12+
13+
1114
@contextmanager
1215
def temporary_setting(settings: sublime.Settings, key: str, val: Any) -> Generator[None, None, None]:
1316
prev_val = None
@@ -25,20 +28,25 @@ class LspApplyDocumentEditCommand(sublime_plugin.TextCommand):
2528
re_placeholder = re.compile(r'\$(0|\{0:([^}]*)\})')
2629

2730
def run(
28-
self, edit: sublime.Edit, changes: Optional[List[TextEditTuple]] = None, process_placeholders: bool = False
31+
self,
32+
edit: sublime.Edit,
33+
changes: List[TextEdit],
34+
required_view_version: Optional[int] = None,
35+
process_placeholders: bool = False,
2936
) -> None:
3037
# Apply the changes in reverse, so that we don't invalidate the range
3138
# of any change that we haven't applied yet.
3239
if not changes:
3340
return
41+
view_version = self.view.change_count()
42+
if required_view_version is not None and required_view_version != view_version:
43+
print('LSP: ignoring edit due to non-matching document version')
44+
return
45+
edits = [_parse_text_edit(change) for change in changes or []]
3446
with temporary_setting(self.view.settings(), "translate_tabs_to_spaces", False):
35-
view_version = self.view.change_count()
3647
last_row, _ = self.view.rowcol_utf16(self.view.size())
3748
placeholder_region_count = 0
38-
for start, end, replacement, version in reversed(_sort_by_application_order(changes)):
39-
if version is not None and version != view_version:
40-
debug('ignoring edit due to non-matching document version')
41-
continue
49+
for start, end, replacement in reversed(_sort_by_application_order(edits)):
4250
placeholder_region = None # type: Optional[Tuple[Tuple[int, int], Tuple[int, int]]]
4351
if process_placeholders and replacement:
4452
parsed = self.parse_snippet(replacement)
@@ -96,6 +104,15 @@ def parse_snippet(self, replacement: str) -> Optional[Tuple[str, Tuple[int, int]
96104
return (new_replacement, placeholder_start_and_length)
97105

98106

107+
def _parse_text_edit(text_edit: TextEdit) -> TextEditTuple:
108+
return (
109+
parse_range(text_edit['range']['start']),
110+
parse_range(text_edit['range']['end']),
111+
# Strip away carriage returns -- SublimeText takes care of that.
112+
text_edit.get('newText', '').replace("\r", "")
113+
)
114+
115+
99116
def _sort_by_application_order(changes: Iterable[TextEditTuple]) -> List[TextEditTuple]:
100117
# The spec reads:
101118
# > However, it is possible that multiple edits have the same start position: multiple

plugin/formatting.py

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from .core.collections import DottedDict
2-
from .core.edit import parse_text_edit
2+
from .core.edit import apply_text_edits
33
from .core.promise import Promise
44
from .core.protocol import Error
55
from .core.protocol import TextDocumentSaveReason
@@ -8,7 +8,7 @@
88
from .core.registry import windows
99
from .core.sessions import Session
1010
from .core.settings import userprefs
11-
from .core.typing import Any, Callable, List, Optional, Iterator, Union
11+
from .core.typing import Callable, List, Optional, Iterator, Union
1212
from .core.views import entire_content_region
1313
from .core.views import first_selection_region
1414
from .core.views import has_single_nonempty_selection
@@ -50,13 +50,6 @@ def format_document(text_command: LspTextCommand, formatter: Optional[str] = Non
5050
return Promise.resolve(None)
5151

5252

53-
def apply_text_edits_to_view(
54-
response: Optional[List[TextEdit]], view: sublime.View, *, process_placeholders: bool = False
55-
) -> None:
56-
edits = list(parse_text_edit(change) for change in response) if response else []
57-
view.run_command('lsp_apply_document_edit', {'changes': edits, 'process_placeholders': process_placeholders})
58-
59-
6053
class WillSaveWaitTask(SaveTask):
6154
@classmethod
6255
def is_applicable(cls, view: sublime.View) -> bool:
@@ -85,9 +78,9 @@ def _will_save_wait_until_async(self, session: Session) -> None:
8578
self._on_response,
8679
lambda error: self._on_response(None))
8780

88-
def _on_response(self, response: Any) -> None:
89-
if response and not self._cancelled:
90-
apply_text_edits_to_view(response, self._task_runner.view)
81+
def _on_response(self, response: FormatResponse) -> None:
82+
if response and not isinstance(response, Error) and not self._cancelled:
83+
apply_text_edits(self._task_runner.view, response)
9184
sublime.set_timeout_async(self._handle_next_session_async)
9285

9386

@@ -108,7 +101,7 @@ def run_async(self) -> None:
108101

109102
def _on_response(self, response: FormatResponse) -> None:
110103
if response and not isinstance(response, Error) and not self._cancelled:
111-
apply_text_edits_to_view(response, self._task_runner.view)
104+
apply_text_edits(self._task_runner.view, response)
112105
sublime.set_timeout_async(self._on_complete)
113106

114107

@@ -143,7 +136,7 @@ def run(self, edit: sublime.Edit, event: Optional[dict] = None, select: bool = F
143136

144137
def on_result(self, result: FormatResponse) -> None:
145138
if result and not isinstance(result, Error):
146-
apply_text_edits_to_view(result, self.view)
139+
apply_text_edits(self.view, result)
147140

148141
def select_formatter(self, base_scope: str, session_names: List[str]) -> None:
149142
window = self.view.window()
@@ -194,12 +187,12 @@ def run(self, edit: sublime.Edit, event: Optional[dict] = None) -> None:
194187
selection = first_selection_region(self.view)
195188
if session and selection is not None:
196189
req = text_document_range_formatting(self.view, selection)
197-
session.send_request(req, lambda response: apply_text_edits_to_view(response, self.view))
190+
session.send_request(req, lambda response: apply_text_edits(self.view, response))
198191
elif self.view.has_non_empty_selection_region():
199192
session = self.best_session('documentRangeFormattingProvider.rangesSupport')
200193
if session:
201194
req = text_document_ranges_formatting(self.view)
202-
session.send_request(req, lambda response: apply_text_edits_to_view(response, self.view))
195+
session.send_request(req, lambda response: apply_text_edits(self.view, response))
203196

204197

205198
class LspFormatCommand(LspTextCommand):

plugin/inlay_hint.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from .core.css import css
2+
from .core.edit import apply_text_edits
23
from .core.protocol import InlayHint
34
from .core.protocol import InlayHintLabelPart
45
from .core.protocol import MarkupContent
@@ -9,7 +10,6 @@
910
from .core.settings import userprefs
1011
from .core.typing import cast, Optional, Union
1112
from .core.views import position_to_offset
12-
from .formatting import apply_text_edits_to_view
1313
import html
1414
import sublime
1515
import uuid
@@ -69,7 +69,7 @@ def handle_inlay_hint_text_edits(self, session_name: str, inlay_hint: InlayHint,
6969
return
7070
for sb in session.session_buffers_async():
7171
sb.remove_inlay_hint_phantom(phantom_uuid)
72-
apply_text_edits_to_view(text_edits, self.view)
72+
apply_text_edits(self.view, text_edits)
7373

7474
def handle_label_part_command(self, session_name: str, label_part: Optional[InlayHintLabelPart] = None) -> None:
7575
if not label_part:

0 commit comments

Comments
 (0)