Skip to content

Commit

Permalink
feat: support experimental snippet text edits
Browse files Browse the repository at this point in the history
  • Loading branch information
rchl committed Jan 4, 2024
1 parent 36871c2 commit c49cf3e
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 5 deletions.
44 changes: 42 additions & 2 deletions plugin/edit.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from .core.edit import TextEditTuple
from .core.logging import debug
from .core.typing import List, Optional, Any, Generator, Iterable
from .core.typing import List, Optional, Any, Generator, Iterable, Tuple
from contextlib import contextmanager
import operator
import re
import sublime
import sublime_plugin

Expand All @@ -21,19 +22,39 @@ def temporary_setting(settings: sublime.Settings, key: str, val: Any) -> Generat


class LspApplyDocumentEditCommand(sublime_plugin.TextCommand):
re_snippet = re.compile(r'\$(0|\{0:([^}]*)\})')

def run(self, edit: sublime.Edit, changes: Optional[List[TextEditTuple]] = None) -> None:
def run(
self, edit: sublime.Edit, changes: Optional[List[TextEditTuple]] = None, process_snippets: 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
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())
snippet_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
snippet_region = None # type: Optional[Tuple[Tuple[int, int], Tuple[int, int]]]
if process_snippets and replacement:
parsed = self.parse_snippet(replacement)
if parsed:
replacement, (placeholder_start, placeholder_length) = parsed
# There might be newlines before the placeholder. Find the actual line and character offset
# of the placeholder.
prefix = replacement[0:placeholder_start]
last_newline_start = prefix.rfind('\n')
start_line = start[0] + prefix.count('\n')
if last_newline_start == -1:
start_column = start[1] + placeholder_start
else:
start_column = len(prefix) - last_newline_start - 1
end_column = start_column + placeholder_length
snippet_region = ((start_line, start_column), (start_line, end_column))
region = sublime.Region(
self.view.text_point_utf16(*start, clamp_column=True),
self.view.text_point_utf16(*end, clamp_column=True)
Expand All @@ -45,6 +66,16 @@ def run(self, edit: sublime.Edit, changes: Optional[List[TextEditTuple]] = None)
last_row, _ = self.view.rowcol(self.view.size())
else:
self.apply_change(region, replacement, edit)
if snippet_region is not None:
if snippet_region_count == 0:
self.view.sel().clear()
snippet_region_count += 1
self.view.sel().add(sublime.Region(
self.view.text_point_utf16(*snippet_region[0], clamp_column=True),
self.view.text_point_utf16(*snippet_region[1], clamp_column=True)
))
if snippet_region_count == 1:
self.view.show(self.view.sel())

def apply_change(self, region: sublime.Region, replacement: str, edit: sublime.Edit) -> None:
if region.empty():
Expand All @@ -55,6 +86,15 @@ def apply_change(self, region: sublime.Region, replacement: str, edit: sublime.E
else:
self.view.erase(edit, region)

def parse_snippet(self, replacement: str) -> Optional[Tuple[str, Tuple[int, int]]]:
match = re.search(self.re_snippet, replacement)
if not match:
return
placeholder = match.group(2) or ''
new_replacement = replacement.replace(match.group(0), placeholder)
placeholder_start_and_length = (match.start(0), len(placeholder))
return (new_replacement, placeholder_start_and_length)


def _sort_by_application_order(changes: Iterable[TextEditTuple]) -> List[TextEditTuple]:
# The spec reads:
Expand Down
6 changes: 4 additions & 2 deletions plugin/formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,11 @@ 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) -> None:
def apply_text_edits_to_view(
response: Optional[List[TextEdit]], view: sublime.View, *, process_snippets: 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})
view.run_command('lsp_apply_document_edit', {'changes': edits, 'process_snippets': process_snippets})


class WillSaveWaitTask(SaveTask):
Expand Down
2 changes: 1 addition & 1 deletion stubs/sublime.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -1758,7 +1758,7 @@ class View:
# def is_in_edit(self) -> bool: # undocumented
# ...

def insert(self, edit: Edit, pt: int, text: str) -> None:
def insert(self, edit: Edit, pt: int, text: str) -> int:
"""
Insert the given string into the buffer.
Expand Down

0 comments on commit c49cf3e

Please sign in to comment.