Skip to content

Commit

Permalink
Create a new line for edits after the document (#717)
Browse files Browse the repository at this point in the history
* Create a new line for edits after the document

* Switch to iterable, lint fix, update test comments

* Revert use of iterator - typings don't agree

* Update last_row and log when appending a line
  • Loading branch information
tomv564 committed Sep 14, 2019
1 parent a630fcd commit 8dda8f2
Show file tree
Hide file tree
Showing 4 changed files with 42 additions and 13 deletions.
8 changes: 2 additions & 6 deletions plugin/core/edit.py
@@ -1,4 +1,5 @@
from .url import uri_to_filename
import operator
try:
from typing import List, Dict, Optional, Any, Iterable, Tuple
TextEdit = Tuple[Tuple[int, int], Tuple[int, int], str]
Expand Down Expand Up @@ -32,11 +33,6 @@ def parse_text_edit(text_edit: 'Dict[str, Any]') -> 'TextEdit':


def sort_by_application_order(changes: 'Iterable[TextEdit]') -> 'List[TextEdit]':

def get_start_position(pair: 'Tuple[int, TextEdit]'):
index, change = pair
return change[0][0], change[0][1], index

# 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
Expand All @@ -45,4 +41,4 @@ def get_start_position(pair: 'Tuple[int, TextEdit]'):
# 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(map(lambda pair: pair[1], sorted(enumerate(changes), key=get_start_position, reverse=True)))
return list(sorted(changes, key=operator.itemgetter(0)))
10 changes: 5 additions & 5 deletions plugin/core/test_edit.py
Expand Up @@ -58,14 +58,14 @@ class SortByApplicationOrderTests(unittest.TestCase):
def test_empty_sort(self):
self.assertEqual(sort_by_application_order([]), [])

def test_sorts_backwards(self):
def test_sorts_in_application_order(self):
edits = [
((0, 0), (0, 0), 'b'),
((0, 0), (0, 0), 'a'),
((0, 2), (0, 2), 'c')
]
# expect 'c' (higher start), 'a' now reverse order before 'b'
sorted = sort_by_application_order(edits)
self.assertEqual(sorted[0][2], 'c')
self.assertEqual(sorted[1][2], 'a')
self.assertEqual(sorted[2][2], 'b')
sorted_edits = sort_by_application_order(edits)
self.assertEqual(sorted_edits[0][2], 'b')
self.assertEqual(sorted_edits[1][2], 'a')
self.assertEqual(sorted_edits[2][2], 'c')
14 changes: 12 additions & 2 deletions plugin/edit.py
Expand Up @@ -41,14 +41,24 @@ def open_and_apply_edits(self, path, file_changes):


class LspApplyDocumentEditCommand(sublime_plugin.TextCommand):

def run(self, edit, changes: 'Optional[List[TextEdit]]' = None):
# Apply the changes in reverse, so that we don't invalidate the range
# of any change that we haven't applied yet.
if changes:
for change in sort_by_application_order(changes):
last_row, last_col = self.view.rowcol(self.view.size())
for change in reversed(sort_by_application_order(changes)):
start, end, newText = change
region = sublime.Region(self.view.text_point(*start), self.view.text_point(*end))
self.apply_change(region, newText, edit)

if start[0] > last_row and newText[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.
debug('adding new line for edit at line {}, document ended at line {}'.format(start[0], last_row))
self.apply_change(region, '\n' + newText, edit)
last_row, last_col = self.view.rowcol(self.view.size())
else:
self.apply_change(region, newText, edit)

def apply_change(self, region: 'sublime.Region', newText: str, edit):
if region.empty():
Expand Down
23 changes: 23 additions & 0 deletions tests/test_edit.py
Expand Up @@ -13,6 +13,29 @@ class ApplyDocumentEditTests(DeferrableTestCase):
def setUp(self):
self.view = sublime.active_window().new_file()

def test_remove_line_and_then_insert_at_that_line_at_end(self):
original = (
'a\n'
'b\n'
'c'
)
file_changes = [
((2, 0), (3, 0), ''), # out-of-bounds end position, but this is fine
((3, 0), (3, 0), 'c\n') # out-of-bounds start and end, this line doesn't exist
]
expected = (
'a\n'
'b\n'
'c\n'
)
# Old behavior:
# 1) first we end up with ('a\n', 'b\n', 'cc\n')
# 2) then we end up with ('a\n', 'b\n', '')
# New behavior:
# 1) line index 3 is "created" ('a\n', 'b\n', 'c\n', c\n'))
# 2) deletes line index 2.
self.run_test(original, expected, file_changes)

def test_apply(self):
original = (
'<dom-module id="some-thing">\n'
Expand Down

0 comments on commit 8dda8f2

Please sign in to comment.