From 8dda8f21de93f57e6a85792848adce2b42cba655 Mon Sep 17 00:00:00 2001 From: Tom van Ommeren Date: Sat, 14 Sep 2019 13:52:14 +0200 Subject: [PATCH] Create a new line for edits after the document (#717) * 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 --- plugin/core/edit.py | 8 ++------ plugin/core/test_edit.py | 10 +++++----- plugin/edit.py | 14 ++++++++++++-- tests/test_edit.py | 23 +++++++++++++++++++++++ 4 files changed, 42 insertions(+), 13 deletions(-) diff --git a/plugin/core/edit.py b/plugin/core/edit.py index 0b62a0376..16d81b39d 100644 --- a/plugin/core/edit.py +++ b/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] @@ -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 @@ -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))) diff --git a/plugin/core/test_edit.py b/plugin/core/test_edit.py index 0bc0670c2..9103064fd 100644 --- a/plugin/core/test_edit.py +++ b/plugin/core/test_edit.py @@ -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') diff --git a/plugin/edit.py b/plugin/edit.py index 535b1e4fd..bfd3ccb31 100644 --- a/plugin/edit.py +++ b/plugin/edit.py @@ -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(): diff --git a/tests/test_edit.py b/tests/test_edit.py index 95566cd08..c0f36cbe3 100644 --- a/tests/test_edit.py +++ b/tests/test_edit.py @@ -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 = ( '\n'