From 9b456123f475c90a13f54b0210b2ec740b0ad28e Mon Sep 17 00:00:00 2001 From: masfrost Date: Thu, 9 Dec 2021 00:59:47 -0800 Subject: [PATCH 01/21] Add apply_text_edits method to workspace document --- pylsp/workspace.py | 79 ++++++++++ test/test_workspace.py | 334 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 413 insertions(+) diff --git a/pylsp/workspace.py b/pylsp/workspace.py index bf312f62..4cb166e6 100644 --- a/pylsp/workspace.py +++ b/pylsp/workspace.py @@ -129,6 +129,65 @@ def _create_document(self, doc_uri, source=None, version=None): ) +def get_well_formatted_range(range): + start = range['start'] + end = range['end'] + + if start['line'] > end['line'] or (start['line'] == end['line'] and start['character'] > end['character']): + return { 'start': end, 'end': start } + + return range + +def get_well_formatted_edit(text_edit): + range = get_well_formatted_range(text_edit['range']) + if range != text_edit['range']: + return { 'newText': text_edit['newText'], 'range': range } + + return text_edit + +def compare_text_edits(a, b): + diff = a['range']['start']['line'] - b['range']['start']['line'] + if diff == 0: + return a['range']['start']['character'] - b['range']['start']['character'] + + return diff + +def merge_sort_text_edits(text_edits): + if len(text_edits) <= 1: + return text_edits + + p = len(text_edits) // 2 + left = text_edits[:p] + right = text_edits[p:] + + merge_sort_text_edits(left) + merge_sort_text_edits(right) + + left_idx = 0 + right_idx = 0 + i = 0 + while left_idx < len(left) and right_idx < len(right): + ret = compare_text_edits(left[left_idx], right[right_idx]) + if ret <= 0: + # smaller_equal -> take left to preserve order + text_edits[i] = left[left_idx] + i += 1 + left_idx += 1 + else: + # greater -> take right + text_edits[i] = right[right_idx] + i+=1 + right_idx +=1 + while left_idx < len(left): + text_edits[i] = left[left_idx] + i += 1 + left_idx += 1 + while right_idx < len(right): + text_edits[i] = right[right_idx] + i += 1 + right_idx += 1 + return text_edits + class Document: def __init__(self, uri, workspace, source=None, version=None, local=True, extra_sys_path=None, @@ -216,6 +275,26 @@ def apply_change(self, change): self._source = new.getvalue() + @lock + def apply_text_edits(self, text_edits): + text = self._source + sorted_edits = merge_sort_text_edits(list(map(get_well_formatted_edit,text_edits))) + last_modified_offset = 0 + spans = [] + for e in sorted_edits: + start_offset = self.offset_at_position(e['range']['start']) + if start_offset < last_modified_offset: + raise Exception('overlapping edit') + elif start_offset > last_modified_offset: + spans.append(text[last_modified_offset:start_offset]) + + if len(e['newText']): + spans.append(e['newText']) + last_modified_offset = self.offset_at_position(e['range']['end']) + + spans.append(text[last_modified_offset:]) + return ''.join(spans) + def offset_at_position(self, position): """Return the byte-offset pointed at by the given position.""" return position['character'] + len(''.join(self.lines[:position['line']])) diff --git a/test/test_workspace.py b/test/test_workspace.py index 44d754b2..8d41525e 100644 --- a/test/test_workspace.py +++ b/test/test_workspace.py @@ -293,3 +293,337 @@ def test_settings_of_added_workspace(pylsp, tmpdir): workspace1_object = pylsp.workspaces[workspace1['uri']] workspace1_jedi_settings = workspace1_object._config.plugin_settings('jedi') assert workspace1_jedi_settings == server_settings['pylsp']['plugins']['jedi'] + + +def test_apply_text_edits_insert(pylsp): + pylsp.workspace.put_document(DOC_URI, '012345678901234567890123456789') + test_doc = pylsp.workspace.get_document(DOC_URI) + + assert test_doc.apply_text_edits([{ + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 0 + } + }, + "newText": "Hello" + }]) == 'Hello012345678901234567890123456789' + assert test_doc.apply_text_edits([{ + "range": { + "start": { + "line": 0, + "character": 1 + }, + "end": { + "line": 0, + "character": 1 + } + }, + "newText": "Hello" + }]) == '0Hello12345678901234567890123456789' + assert test_doc.apply_text_edits([{ + "range": { + "start": { + "line": 0, + "character": 1 + }, + "end": { + "line": 0, + "character": 1 + } + }, + "newText": "Hello" + }, { + "range": { + "start": { + "line": 0, + "character": 1 + }, + "end": { + "line": 0, + "character": 1 + } + }, + "newText": "World" + }]) == '0HelloWorld12345678901234567890123456789' + assert test_doc.apply_text_edits([{ + "range": { + "start": { + "line": 0, + "character": 2 + }, + "end": { + "line": 0, + "character": 2 + } + }, + "newText": "One" + }, { + "range": { + "start": { + "line": 0, + "character": 1 + }, + "end": { + "line": 0, + "character": 1 + } + }, + "newText": "Hello" + }, { + "range": { + "start": { + "line": 0, + "character": 1 + }, + "end": { + "line": 0, + "character": 1 + } + }, + "newText": "World" + }, { + "range": { + "start": { + "line": 0, + "character": 2 + }, + "end": { + "line": 0, + "character": 2 + } + }, + "newText": "Two" + }, { + "range": { + "start": { + "line": 0, + "character": 2 + }, + "end": { + "line": 0, + "character": 2 + } + }, + "newText": "Three" + }]) == '0HelloWorld1OneTwoThree2345678901234567890123456789' + +def test_apply_text_edits_replace(pylsp): + pylsp.workspace.put_document(DOC_URI, '012345678901234567890123456789') + test_doc = pylsp.workspace.get_document(DOC_URI) + + assert test_doc.apply_text_edits([{ + "range": { + "start": { + "line": 0, + "character": 3 + }, + "end": { + "line": 0, + "character": 6 + } + }, + "newText": "Hello" + }]) == '012Hello678901234567890123456789' + assert test_doc.apply_text_edits([{ + "range": { + "start": { + "line": 0, + "character": 3 + }, + "end": { + "line": 0, + "character": 6 + } + }, + "newText": "Hello" + }, { + "range": { + "start": { + "line": 0, + "character": 6 + }, + "end": { + "line": 0, + "character": 9 + } + }, + "newText": "World" + }]) == '012HelloWorld901234567890123456789' + assert test_doc.apply_text_edits([{ + "range": { + "start": { + "line": 0, + "character": 3 + }, + "end": { + "line": 0, + "character": 6 + } + }, + "newText": "Hello" + }, { + "range": { + "start": { + "line": 0, + "character": 6 + }, + "end": { + "line": 0, + "character": 6 + } + }, + "newText": "World" + }]) == '012HelloWorld678901234567890123456789' + assert test_doc.apply_text_edits([{ + "range": { + "start": { + "line": 0, + "character": 6 + }, + "end": { + "line": 0, + "character": 6 + } + }, + "newText": "World" + }, { + "range": { + "start": { + "line": 0, + "character": 3 + }, + "end": { + "line": 0, + "character": 6 + } + }, + "newText": "Hello" + }]) == '012HelloWorld678901234567890123456789' + assert test_doc.apply_text_edits([{ + "range": { + "start": { + "line": 0, + "character": 3 + }, + "end": { + "line": 0, + "character": 3 + } + }, + "newText": "World" + }, { + "range": { + "start": { + "line": 0, + "character": 3 + }, + "end": { + "line": 0, + "character": 6 + } + }, + "newText": "Hello" + }]) == '012WorldHello678901234567890123456789' + + +def test_apply_text_edits_overlap(pylsp): + pylsp.workspace.put_document(DOC_URI, '012345678901234567890123456789') + test_doc = pylsp.workspace.get_document(DOC_URI) + + did_throw = False + try: + test_doc.apply_text_edits([{ + "range": { + "start": { + "line": 0, + "character": 3 + }, + "end": { + "line": 0, + "character": 6 + } + }, + "newText": "Hello" + }, { + "range": { + "start": { + "line": 0, + "character": 3 + }, + "end": { + "line": 0, + "character": 3 + } + }, + "newText": "World" + }]) + except Exception: + did_throw = True + + assert did_throw + + try: + test_doc.apply_text_edits([{ + "range": { + "start": { + "line": 0, + "character": 3 + }, + "end": { + "line": 0, + "character": 6 + } + }, + "newText": "Hello" + }, { + "range": { + "start": { + "line": 0, + "character": 4 + }, + "end": { + "line": 0, + "character": 4 + } + }, + "newText": "World" + }]) + except Exception: + did_throw = True + + assert did_throw + +def test_apply_text_edits_multiline(pylsp): + pylsp.workspace.put_document(DOC_URI, '0\n1\n2\n3\n4') + test_doc = pylsp.workspace.get_document(DOC_URI) + + assert test_doc.apply_text_edits([{ + "range": { + "start": { + "line": 2, + "character": 0 + }, + "end": { + "line": 3, + "character": 0 + } + }, + "newText": "Hello" + }, { + "range": { + "start": { + "line": 1, + "character": 1 + }, + "end": { + "line": 1, + "character": 1 + } + }, + "newText": "World" + }]) == '0\n1World\nHello3\n4' From c7623f65d8d4d0cee6dcaf76d3fc94ddf74b7857 Mon Sep 17 00:00:00 2001 From: masfrost Date: Thu, 9 Dec 2021 01:10:50 -0800 Subject: [PATCH 02/21] Sendback diffs instead of the whole document with yapf formatting --- pylsp/plugins/yapf_format.py | 74 +++++++++++++++++++++++++------- setup.cfg | 5 ++- setup.py | 3 +- test/plugins/test_yapf_format.py | 14 +++--- 4 files changed, 70 insertions(+), 26 deletions(-) diff --git a/pylsp/plugins/yapf_format.py b/pylsp/plugins/yapf_format.py index 1c90f965..629ec342 100644 --- a/pylsp/plugins/yapf_format.py +++ b/pylsp/plugins/yapf_format.py @@ -7,6 +7,8 @@ from yapf.yapflib import file_resources from yapf.yapflib.yapf_api import FormatCode +import whatthepatch + from pylsp import hookimpl from pylsp._utils import get_eol_chars @@ -39,17 +41,16 @@ def pylsp_format_range(document, range): # pylint: disable=redefined-builtin def _format(document, lines=None): # Yapf doesn't work with CR line endings, so we replace them by '\n' # and restore them below. - replace_cr = False source = document.source eol_chars = get_eol_chars(source) if eol_chars == '\r': - replace_cr = True source = source.replace('\r', '\n') - new_source, changed = FormatCode( + diff_txt, changed = FormatCode( source, lines=lines, filename=document.filename, + print_diff=True, style_config=file_resources.GetDefaultStyleForDir( os.path.dirname(document.path) ) @@ -58,16 +59,57 @@ def _format(document, lines=None): if not changed: return [] - if replace_cr: - new_source = new_source.replace('\n', '\r') - - # I'm too lazy at the moment to parse diffs into TextEdit items - # So let's just return the entire file... - return [{ - 'range': { - 'start': {'line': 0, 'character': 0}, - # End char 0 of the line after our document - 'end': {'line': len(document.lines), 'character': 0} - }, - 'newText': new_source - }] + patch_generator = whatthepatch.parse_patch(diff_txt) + diff = next(patch_generator) + patch_generator.close() + + # To keep things simple our text edits will be line based + # and uncompacted + textEdits = [] + # keep track of line number since additions + # don't include the line number it's being added + # to in diffs. lsp is 0-indexed so we'll start with -1 + prev_line_no = -1 + for change in diff.changes: + if change.old and change.new: + # no change + # diffs are 1-indexed + prev_line_no = change.old - 1 + elif change.new: + # addition + textEdits.append({ + 'range': { + 'start': { + 'line': prev_line_no + 1, + 'character': 0 + }, + 'end': { + 'line': prev_line_no + 1, + 'character': 0 + } + }, + 'newText': change.line + eol_chars + }) + elif change.old: + # remove + lsp_line_no = change.old - 1 + textEdits.append({ + 'range': { + 'start': { + 'line': lsp_line_no, + 'character': 0 + }, + 'end': { + # From LSP spec: + # If you want to specify a range that contains a line + # including the line ending character(s) then use an + # end position denoting the start of the next line. + 'line': lsp_line_no + 1, + 'character': 0 + } + }, + 'newText': '' + }) + prev_line_no = lsp_line_no + + return textEdits \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 17db54d9..02854885 100644 --- a/setup.cfg +++ b/setup.cfg @@ -33,6 +33,7 @@ all = pylint>=2.5.0 rope>=0.10.5 yapf + whatthepatch autopep8 = autopep8>=1.6.0,<1.7.0 flake8 = flake8>=4.0.0,<4.1.0 mccabe = mccabe>=0.6.0,<0.7.0 @@ -41,7 +42,9 @@ pydocstyle = pydocstyle>=2.0.0 pyflakes = pyflakes>=2.4.0,<2.5.0 pylint = pylint>=2.5.0 rope = rope>0.10.5 -yapf = yapf +yapf = + yapf + whatthepatch test = pylint>=2.5.0 pytest diff --git a/setup.py b/setup.py index 28d2d305..6197260c 100755 --- a/setup.py +++ b/setup.py @@ -1,3 +1,4 @@ + #!/usr/bin/env python3 # Copyright 2017-2020 Palantir Technologies, Inc. @@ -9,4 +10,4 @@ setup( name="python-lsp-server", # to allow GitHub dependency tracking work packages=find_packages(exclude=["contrib", "docs", "test", "test.*"]), # https://github.com/pypa/setuptools/issues/2688 - ) + ) \ No newline at end of file diff --git a/test/plugins/test_yapf_format.py b/test/plugins/test_yapf_format.py index 4346985c..67f48d6a 100644 --- a/test/plugins/test_yapf_format.py +++ b/test/plugins/test_yapf_format.py @@ -25,8 +25,7 @@ def test_format(workspace): doc = Document(DOC_URI, workspace, DOC) res = pylsp_format_document(doc) - assert len(res) == 1 - assert res[0]['newText'] == "A = ['h', 'w', 'a']\n\nB = ['h', 'w']\n" + assert doc.apply_text_edits(res) == "A = ['h', 'w', 'a']\n\nB = ['h', 'w']\n" def test_range_format(workspace): @@ -38,10 +37,8 @@ def test_range_format(workspace): } res = pylsp_format_range(doc, def_range) - assert len(res) == 1 - # Make sure B is still badly formatted - assert res[0]['newText'] == "A = ['h', 'w', 'a']\n\nB = ['h',\n\n\n'w']\n" + assert doc.apply_text_edits(res) == "A = ['h', 'w', 'a']\n\nB = ['h',\n\n\n'w']\n" def test_no_change(workspace): @@ -56,12 +53,13 @@ def test_config_file(tmpdir, workspace): src = tmpdir.join('test.py') doc = Document(uris.from_fs_path(src.strpath), workspace, DOC) - # A was split on multiple lines because of column_limit from config file - assert pylsp_format_document(doc)[0]['newText'] == "A = [\n 'h', 'w',\n 'a'\n]\n\nB = ['h', 'w']\n" + res = pylsp_format_document(doc) + # A was split on multiple lines because of column_limit from config file + assert doc.apply_text_edits(res) == "A = [\n 'h', 'w',\n 'a'\n]\n\nB = ['h', 'w']\n" def test_cr_line_endings(workspace): doc = Document(DOC_URI, workspace, 'import os;import sys\r\rdict(a=1)') res = pylsp_format_document(doc) - assert res[0]['newText'] == 'import os\rimport sys\r\rdict(a=1)\r' + assert doc.apply_text_edits(res) == 'import os\rimport sys\r\rdict(a=1)\r' From 613b173595e6611aaa8e8884cee7b58ad272e4bc Mon Sep 17 00:00:00 2001 From: masfrost Date: Thu, 9 Dec 2021 01:29:43 -0800 Subject: [PATCH 03/21] Remove apply_text_edits from document method --- pylsp/workspace.py | 41 ++++++++++++++++++++--------------------- test/test_workspace.py | 26 +++++++++++++------------- 2 files changed, 33 insertions(+), 34 deletions(-) diff --git a/pylsp/workspace.py b/pylsp/workspace.py index 4cb166e6..b8ce1f6c 100644 --- a/pylsp/workspace.py +++ b/pylsp/workspace.py @@ -188,6 +188,25 @@ def merge_sort_text_edits(text_edits): right_idx += 1 return text_edits +def apply_text_edits(doc, text_edits): + text = doc.source + sorted_edits = merge_sort_text_edits(list(map(get_well_formatted_edit,text_edits))) + last_modified_offset = 0 + spans = [] + for e in sorted_edits: + start_offset = doc.offset_at_position(e['range']['start']) + if start_offset < last_modified_offset: + raise Exception('overlapping edit') + elif start_offset > last_modified_offset: + spans.append(text[last_modified_offset:start_offset]) + + if len(e['newText']): + spans.append(e['newText']) + last_modified_offset = doc.offset_at_position(e['range']['end']) + + spans.append(text[last_modified_offset:]) + return ''.join(spans) + class Document: def __init__(self, uri, workspace, source=None, version=None, local=True, extra_sys_path=None, @@ -273,27 +292,7 @@ def apply_change(self, change): if i == end_line: new.write(line[end_col:]) - self._source = new.getvalue() - - @lock - def apply_text_edits(self, text_edits): - text = self._source - sorted_edits = merge_sort_text_edits(list(map(get_well_formatted_edit,text_edits))) - last_modified_offset = 0 - spans = [] - for e in sorted_edits: - start_offset = self.offset_at_position(e['range']['start']) - if start_offset < last_modified_offset: - raise Exception('overlapping edit') - elif start_offset > last_modified_offset: - spans.append(text[last_modified_offset:start_offset]) - - if len(e['newText']): - spans.append(e['newText']) - last_modified_offset = self.offset_at_position(e['range']['end']) - - spans.append(text[last_modified_offset:]) - return ''.join(spans) + self._source = new.getvalue() def offset_at_position(self, position): """Return the byte-offset pointed at by the given position.""" diff --git a/test/test_workspace.py b/test/test_workspace.py index 8d41525e..8af84030 100644 --- a/test/test_workspace.py +++ b/test/test_workspace.py @@ -4,7 +4,7 @@ import pytest from pylsp import uris - +from pylsp.workspace import apply_text_edits DOC_URI = uris.from_fs_path(__file__) @@ -299,7 +299,7 @@ def test_apply_text_edits_insert(pylsp): pylsp.workspace.put_document(DOC_URI, '012345678901234567890123456789') test_doc = pylsp.workspace.get_document(DOC_URI) - assert test_doc.apply_text_edits([{ + assert apply_text_edits(test_doc, [{ "range": { "start": { "line": 0, @@ -312,7 +312,7 @@ def test_apply_text_edits_insert(pylsp): }, "newText": "Hello" }]) == 'Hello012345678901234567890123456789' - assert test_doc.apply_text_edits([{ + assert apply_text_edits(test_doc, [{ "range": { "start": { "line": 0, @@ -325,7 +325,7 @@ def test_apply_text_edits_insert(pylsp): }, "newText": "Hello" }]) == '0Hello12345678901234567890123456789' - assert test_doc.apply_text_edits([{ + assert apply_text_edits(test_doc, [{ "range": { "start": { "line": 0, @@ -350,7 +350,7 @@ def test_apply_text_edits_insert(pylsp): }, "newText": "World" }]) == '0HelloWorld12345678901234567890123456789' - assert test_doc.apply_text_edits([{ + assert apply_text_edits(test_doc, [{ "range": { "start": { "line": 0, @@ -416,7 +416,7 @@ def test_apply_text_edits_replace(pylsp): pylsp.workspace.put_document(DOC_URI, '012345678901234567890123456789') test_doc = pylsp.workspace.get_document(DOC_URI) - assert test_doc.apply_text_edits([{ + assert apply_text_edits(test_doc, [{ "range": { "start": { "line": 0, @@ -429,7 +429,7 @@ def test_apply_text_edits_replace(pylsp): }, "newText": "Hello" }]) == '012Hello678901234567890123456789' - assert test_doc.apply_text_edits([{ + assert apply_text_edits(test_doc, [{ "range": { "start": { "line": 0, @@ -454,7 +454,7 @@ def test_apply_text_edits_replace(pylsp): }, "newText": "World" }]) == '012HelloWorld901234567890123456789' - assert test_doc.apply_text_edits([{ + assert apply_text_edits(test_doc, [{ "range": { "start": { "line": 0, @@ -479,7 +479,7 @@ def test_apply_text_edits_replace(pylsp): }, "newText": "World" }]) == '012HelloWorld678901234567890123456789' - assert test_doc.apply_text_edits([{ + assert apply_text_edits(test_doc, [{ "range": { "start": { "line": 0, @@ -504,7 +504,7 @@ def test_apply_text_edits_replace(pylsp): }, "newText": "Hello" }]) == '012HelloWorld678901234567890123456789' - assert test_doc.apply_text_edits([{ + assert apply_text_edits(test_doc, [{ "range": { "start": { "line": 0, @@ -537,7 +537,7 @@ def test_apply_text_edits_overlap(pylsp): did_throw = False try: - test_doc.apply_text_edits([{ + apply_text_edits(test_doc, [{ "range": { "start": { "line": 0, @@ -568,7 +568,7 @@ def test_apply_text_edits_overlap(pylsp): assert did_throw try: - test_doc.apply_text_edits([{ + apply_text_edits(test_doc, [{ "range": { "start": { "line": 0, @@ -602,7 +602,7 @@ def test_apply_text_edits_multiline(pylsp): pylsp.workspace.put_document(DOC_URI, '0\n1\n2\n3\n4') test_doc = pylsp.workspace.get_document(DOC_URI) - assert test_doc.apply_text_edits([{ + assert apply_text_edits(test_doc, [{ "range": { "start": { "line": 2, From fe0980db68f1412ed9bff1a50bb8969e10d4df67 Mon Sep 17 00:00:00 2001 From: Faris Masad Date: Thu, 9 Dec 2021 01:33:04 -0800 Subject: [PATCH 04/21] oops --- pylsp/workspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylsp/workspace.py b/pylsp/workspace.py index b8ce1f6c..a5997f0d 100644 --- a/pylsp/workspace.py +++ b/pylsp/workspace.py @@ -292,7 +292,7 @@ def apply_change(self, change): if i == end_line: new.write(line[end_col:]) - self._source = new.getvalue() + self._source = new.getvalue() def offset_at_position(self, position): """Return the byte-offset pointed at by the given position.""" From 0565667f32c64e7706e80c52a17b9b190fea49cb Mon Sep 17 00:00:00 2001 From: masfrost Date: Wed, 9 Mar 2022 18:53:44 -0800 Subject: [PATCH 05/21] Fix the test --- test/plugins/test_yapf_format.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/plugins/test_yapf_format.py b/test/plugins/test_yapf_format.py index 67f48d6a..f4d34cb5 100644 --- a/test/plugins/test_yapf_format.py +++ b/test/plugins/test_yapf_format.py @@ -3,7 +3,7 @@ from pylsp import uris from pylsp.plugins.yapf_format import pylsp_format_document, pylsp_format_range -from pylsp.workspace import Document +from pylsp.workspace import Document, apply_text_edits DOC_URI = uris.from_fs_path(__file__) DOC = """A = [ @@ -25,7 +25,7 @@ def test_format(workspace): doc = Document(DOC_URI, workspace, DOC) res = pylsp_format_document(doc) - assert doc.apply_text_edits(res) == "A = ['h', 'w', 'a']\n\nB = ['h', 'w']\n" + assert apply_text_edits(res) == "A = ['h', 'w', 'a']\n\nB = ['h', 'w']\n" def test_range_format(workspace): @@ -38,7 +38,7 @@ def test_range_format(workspace): res = pylsp_format_range(doc, def_range) # Make sure B is still badly formatted - assert doc.apply_text_edits(res) == "A = ['h', 'w', 'a']\n\nB = ['h',\n\n\n'w']\n" + assert apply_text_edits(res) == "A = ['h', 'w', 'a']\n\nB = ['h',\n\n\n'w']\n" def test_no_change(workspace): @@ -56,10 +56,10 @@ def test_config_file(tmpdir, workspace): res = pylsp_format_document(doc) # A was split on multiple lines because of column_limit from config file - assert doc.apply_text_edits(res) == "A = [\n 'h', 'w',\n 'a'\n]\n\nB = ['h', 'w']\n" + assert apply_text_edits(res) == "A = [\n 'h', 'w',\n 'a'\n]\n\nB = ['h', 'w']\n" def test_cr_line_endings(workspace): doc = Document(DOC_URI, workspace, 'import os;import sys\r\rdict(a=1)') res = pylsp_format_document(doc) - assert doc.apply_text_edits(res) == 'import os\rimport sys\r\rdict(a=1)\r' + assert apply_text_edits(res) == 'import os\rimport sys\r\rdict(a=1)\r' From fa394ad56b807ac1172b05e234a1058b824d6ce2 Mon Sep 17 00:00:00 2001 From: masfrost Date: Thu, 17 Mar 2022 13:40:21 -0700 Subject: [PATCH 06/21] Fix not passing doc to apply_text_edit I remember doing this and running tests! --- test/plugins/test_yapf_format.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/plugins/test_yapf_format.py b/test/plugins/test_yapf_format.py index f4d34cb5..e63988c2 100644 --- a/test/plugins/test_yapf_format.py +++ b/test/plugins/test_yapf_format.py @@ -25,7 +25,7 @@ def test_format(workspace): doc = Document(DOC_URI, workspace, DOC) res = pylsp_format_document(doc) - assert apply_text_edits(res) == "A = ['h', 'w', 'a']\n\nB = ['h', 'w']\n" + assert apply_text_edits(doc, res) == "A = ['h', 'w', 'a']\n\nB = ['h', 'w']\n" def test_range_format(workspace): @@ -38,7 +38,7 @@ def test_range_format(workspace): res = pylsp_format_range(doc, def_range) # Make sure B is still badly formatted - assert apply_text_edits(res) == "A = ['h', 'w', 'a']\n\nB = ['h',\n\n\n'w']\n" + assert apply_text_edits(doc, res) == "A = ['h', 'w', 'a']\n\nB = ['h',\n\n\n'w']\n" def test_no_change(workspace): @@ -56,10 +56,10 @@ def test_config_file(tmpdir, workspace): res = pylsp_format_document(doc) # A was split on multiple lines because of column_limit from config file - assert apply_text_edits(res) == "A = [\n 'h', 'w',\n 'a'\n]\n\nB = ['h', 'w']\n" + assert apply_text_edits(doc, res) == "A = [\n 'h', 'w',\n 'a'\n]\n\nB = ['h', 'w']\n" def test_cr_line_endings(workspace): doc = Document(DOC_URI, workspace, 'import os;import sys\r\rdict(a=1)') res = pylsp_format_document(doc) - assert apply_text_edits(res) == 'import os\rimport sys\r\rdict(a=1)\r' + assert apply_text_edits(doc, res) == 'import os\rimport sys\r\rdict(a=1)' From 2dbf3925807f28f2b58ca83f1e9867521178653b Mon Sep 17 00:00:00 2001 From: masfrost Date: Thu, 17 Mar 2022 13:49:35 -0700 Subject: [PATCH 07/21] Lint-y lint --- pylsp/plugins/yapf_format.py | 2 +- setup.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pylsp/plugins/yapf_format.py b/pylsp/plugins/yapf_format.py index 629ec342..1ddfbeb7 100644 --- a/pylsp/plugins/yapf_format.py +++ b/pylsp/plugins/yapf_format.py @@ -112,4 +112,4 @@ def _format(document, lines=None): }) prev_line_no = lsp_line_no - return textEdits \ No newline at end of file + return textEdits diff --git a/setup.py b/setup.py index 6197260c..28d2d305 100755 --- a/setup.py +++ b/setup.py @@ -1,4 +1,3 @@ - #!/usr/bin/env python3 # Copyright 2017-2020 Palantir Technologies, Inc. @@ -10,4 +9,4 @@ setup( name="python-lsp-server", # to allow GitHub dependency tracking work packages=find_packages(exclude=["contrib", "docs", "test", "test.*"]), # https://github.com/pypa/setuptools/issues/2688 - ) \ No newline at end of file + ) From 8847a5d25f4aa438dcc456518ff1683b3908a027 Mon Sep 17 00:00:00 2001 From: masfrost Date: Thu, 2 Jun 2022 11:28:28 -0700 Subject: [PATCH 08/21] Make a separate module for text_edit --- pylsp/text_edit.py | 77 +++++++ pylsp/workspace.py | 78 ------- test/plugins/test_yapf_format.py | 3 +- test/test_text_edit.py | 337 +++++++++++++++++++++++++++++++ test/test_workspace.py | 335 ------------------------------ 5 files changed, 416 insertions(+), 414 deletions(-) create mode 100644 pylsp/text_edit.py create mode 100644 test/test_text_edit.py diff --git a/pylsp/text_edit.py b/pylsp/text_edit.py new file mode 100644 index 00000000..36d0d726 --- /dev/null +++ b/pylsp/text_edit.py @@ -0,0 +1,77 @@ +def get_well_formatted_range(range): + start = range['start'] + end = range['end'] + + if start['line'] > end['line'] or (start['line'] == end['line'] and start['character'] > end['character']): + return { 'start': end, 'end': start } + + return range + +def get_well_formatted_edit(text_edit): + range = get_well_formatted_range(text_edit['range']) + if range != text_edit['range']: + return { 'newText': text_edit['newText'], 'range': range } + + return text_edit + +def compare_text_edits(a, b): + diff = a['range']['start']['line'] - b['range']['start']['line'] + if diff == 0: + return a['range']['start']['character'] - b['range']['start']['character'] + + return diff + +def merge_sort_text_edits(text_edits): + if len(text_edits) <= 1: + return text_edits + + p = len(text_edits) // 2 + left = text_edits[:p] + right = text_edits[p:] + + merge_sort_text_edits(left) + merge_sort_text_edits(right) + + left_idx = 0 + right_idx = 0 + i = 0 + while left_idx < len(left) and right_idx < len(right): + ret = compare_text_edits(left[left_idx], right[right_idx]) + if ret <= 0: + # smaller_equal -> take left to preserve order + text_edits[i] = left[left_idx] + i += 1 + left_idx += 1 + else: + # greater -> take right + text_edits[i] = right[right_idx] + i+=1 + right_idx +=1 + while left_idx < len(left): + text_edits[i] = left[left_idx] + i += 1 + left_idx += 1 + while right_idx < len(right): + text_edits[i] = right[right_idx] + i += 1 + right_idx += 1 + return text_edits + +def apply_text_edits(doc, text_edits): + text = doc.source + sorted_edits = merge_sort_text_edits(list(map(get_well_formatted_edit,text_edits))) + last_modified_offset = 0 + spans = [] + for e in sorted_edits: + start_offset = doc.offset_at_position(e['range']['start']) + if start_offset < last_modified_offset: + raise Exception('overlapping edit') + elif start_offset > last_modified_offset: + spans.append(text[last_modified_offset:start_offset]) + + if len(e['newText']): + spans.append(e['newText']) + last_modified_offset = doc.offset_at_position(e['range']['end']) + + spans.append(text[last_modified_offset:]) + return ''.join(spans) diff --git a/pylsp/workspace.py b/pylsp/workspace.py index a5997f0d..bf312f62 100644 --- a/pylsp/workspace.py +++ b/pylsp/workspace.py @@ -129,84 +129,6 @@ def _create_document(self, doc_uri, source=None, version=None): ) -def get_well_formatted_range(range): - start = range['start'] - end = range['end'] - - if start['line'] > end['line'] or (start['line'] == end['line'] and start['character'] > end['character']): - return { 'start': end, 'end': start } - - return range - -def get_well_formatted_edit(text_edit): - range = get_well_formatted_range(text_edit['range']) - if range != text_edit['range']: - return { 'newText': text_edit['newText'], 'range': range } - - return text_edit - -def compare_text_edits(a, b): - diff = a['range']['start']['line'] - b['range']['start']['line'] - if diff == 0: - return a['range']['start']['character'] - b['range']['start']['character'] - - return diff - -def merge_sort_text_edits(text_edits): - if len(text_edits) <= 1: - return text_edits - - p = len(text_edits) // 2 - left = text_edits[:p] - right = text_edits[p:] - - merge_sort_text_edits(left) - merge_sort_text_edits(right) - - left_idx = 0 - right_idx = 0 - i = 0 - while left_idx < len(left) and right_idx < len(right): - ret = compare_text_edits(left[left_idx], right[right_idx]) - if ret <= 0: - # smaller_equal -> take left to preserve order - text_edits[i] = left[left_idx] - i += 1 - left_idx += 1 - else: - # greater -> take right - text_edits[i] = right[right_idx] - i+=1 - right_idx +=1 - while left_idx < len(left): - text_edits[i] = left[left_idx] - i += 1 - left_idx += 1 - while right_idx < len(right): - text_edits[i] = right[right_idx] - i += 1 - right_idx += 1 - return text_edits - -def apply_text_edits(doc, text_edits): - text = doc.source - sorted_edits = merge_sort_text_edits(list(map(get_well_formatted_edit,text_edits))) - last_modified_offset = 0 - spans = [] - for e in sorted_edits: - start_offset = doc.offset_at_position(e['range']['start']) - if start_offset < last_modified_offset: - raise Exception('overlapping edit') - elif start_offset > last_modified_offset: - spans.append(text[last_modified_offset:start_offset]) - - if len(e['newText']): - spans.append(e['newText']) - last_modified_offset = doc.offset_at_position(e['range']['end']) - - spans.append(text[last_modified_offset:]) - return ''.join(spans) - class Document: def __init__(self, uri, workspace, source=None, version=None, local=True, extra_sys_path=None, diff --git a/test/plugins/test_yapf_format.py b/test/plugins/test_yapf_format.py index f3c4fe12..45b8cd87 100644 --- a/test/plugins/test_yapf_format.py +++ b/test/plugins/test_yapf_format.py @@ -5,7 +5,8 @@ from pylsp import uris from pylsp.plugins.yapf_format import pylsp_format_document, pylsp_format_range -from pylsp.workspace import Document, apply_text_edits +from pylsp.workspace import Document +from pylsp.text_edit import apply_text_edits DOC_URI = uris.from_fs_path(__file__) DOC = """A = [ diff --git a/test/test_text_edit.py b/test/test_text_edit.py new file mode 100644 index 00000000..20f8098f --- /dev/null +++ b/test/test_text_edit.py @@ -0,0 +1,337 @@ +from pylsp.text_edit import apply_text_edits +from pylsp import uris + +DOC_URI = uris.from_fs_path(__file__) + +def test_apply_text_edits_insert(pylsp): + pylsp.workspace.put_document(DOC_URI, '012345678901234567890123456789') + test_doc = pylsp.workspace.get_document(DOC_URI) + + assert apply_text_edits(test_doc, [{ + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 0 + } + }, + "newText": "Hello" + }]) == 'Hello012345678901234567890123456789' + assert apply_text_edits(test_doc, [{ + "range": { + "start": { + "line": 0, + "character": 1 + }, + "end": { + "line": 0, + "character": 1 + } + }, + "newText": "Hello" + }]) == '0Hello12345678901234567890123456789' + assert apply_text_edits(test_doc, [{ + "range": { + "start": { + "line": 0, + "character": 1 + }, + "end": { + "line": 0, + "character": 1 + } + }, + "newText": "Hello" + }, { + "range": { + "start": { + "line": 0, + "character": 1 + }, + "end": { + "line": 0, + "character": 1 + } + }, + "newText": "World" + }]) == '0HelloWorld12345678901234567890123456789' + assert apply_text_edits(test_doc, [{ + "range": { + "start": { + "line": 0, + "character": 2 + }, + "end": { + "line": 0, + "character": 2 + } + }, + "newText": "One" + }, { + "range": { + "start": { + "line": 0, + "character": 1 + }, + "end": { + "line": 0, + "character": 1 + } + }, + "newText": "Hello" + }, { + "range": { + "start": { + "line": 0, + "character": 1 + }, + "end": { + "line": 0, + "character": 1 + } + }, + "newText": "World" + }, { + "range": { + "start": { + "line": 0, + "character": 2 + }, + "end": { + "line": 0, + "character": 2 + } + }, + "newText": "Two" + }, { + "range": { + "start": { + "line": 0, + "character": 2 + }, + "end": { + "line": 0, + "character": 2 + } + }, + "newText": "Three" + }]) == '0HelloWorld1OneTwoThree2345678901234567890123456789' + +def test_apply_text_edits_replace(pylsp): + pylsp.workspace.put_document(DOC_URI, '012345678901234567890123456789') + test_doc = pylsp.workspace.get_document(DOC_URI) + + assert apply_text_edits(test_doc, [{ + "range": { + "start": { + "line": 0, + "character": 3 + }, + "end": { + "line": 0, + "character": 6 + } + }, + "newText": "Hello" + }]) == '012Hello678901234567890123456789' + assert apply_text_edits(test_doc, [{ + "range": { + "start": { + "line": 0, + "character": 3 + }, + "end": { + "line": 0, + "character": 6 + } + }, + "newText": "Hello" + }, { + "range": { + "start": { + "line": 0, + "character": 6 + }, + "end": { + "line": 0, + "character": 9 + } + }, + "newText": "World" + }]) == '012HelloWorld901234567890123456789' + assert apply_text_edits(test_doc, [{ + "range": { + "start": { + "line": 0, + "character": 3 + }, + "end": { + "line": 0, + "character": 6 + } + }, + "newText": "Hello" + }, { + "range": { + "start": { + "line": 0, + "character": 6 + }, + "end": { + "line": 0, + "character": 6 + } + }, + "newText": "World" + }]) == '012HelloWorld678901234567890123456789' + assert apply_text_edits(test_doc, [{ + "range": { + "start": { + "line": 0, + "character": 6 + }, + "end": { + "line": 0, + "character": 6 + } + }, + "newText": "World" + }, { + "range": { + "start": { + "line": 0, + "character": 3 + }, + "end": { + "line": 0, + "character": 6 + } + }, + "newText": "Hello" + }]) == '012HelloWorld678901234567890123456789' + assert apply_text_edits(test_doc, [{ + "range": { + "start": { + "line": 0, + "character": 3 + }, + "end": { + "line": 0, + "character": 3 + } + }, + "newText": "World" + }, { + "range": { + "start": { + "line": 0, + "character": 3 + }, + "end": { + "line": 0, + "character": 6 + } + }, + "newText": "Hello" + }]) == '012WorldHello678901234567890123456789' + + +def test_apply_text_edits_overlap(pylsp): + pylsp.workspace.put_document(DOC_URI, '012345678901234567890123456789') + test_doc = pylsp.workspace.get_document(DOC_URI) + + did_throw = False + try: + apply_text_edits(test_doc, [{ + "range": { + "start": { + "line": 0, + "character": 3 + }, + "end": { + "line": 0, + "character": 6 + } + }, + "newText": "Hello" + }, { + "range": { + "start": { + "line": 0, + "character": 3 + }, + "end": { + "line": 0, + "character": 3 + } + }, + "newText": "World" + }]) + except Exception: + did_throw = True + + assert did_throw + + try: + apply_text_edits(test_doc, [{ + "range": { + "start": { + "line": 0, + "character": 3 + }, + "end": { + "line": 0, + "character": 6 + } + }, + "newText": "Hello" + }, { + "range": { + "start": { + "line": 0, + "character": 4 + }, + "end": { + "line": 0, + "character": 4 + } + }, + "newText": "World" + }]) + except Exception: + did_throw = True + + assert did_throw + +def test_apply_text_edits_multiline(pylsp): + pylsp.workspace.put_document(DOC_URI, '0\n1\n2\n3\n4') + test_doc = pylsp.workspace.get_document(DOC_URI) + + assert apply_text_edits(test_doc, [{ + "range": { + "start": { + "line": 2, + "character": 0 + }, + "end": { + "line": 3, + "character": 0 + } + }, + "newText": "Hello" + }, { + "range": { + "start": { + "line": 1, + "character": 1 + }, + "end": { + "line": 1, + "character": 1 + } + }, + "newText": "World" + }]) == '0\n1World\nHello3\n4' diff --git a/test/test_workspace.py b/test/test_workspace.py index 8af84030..1287599a 100644 --- a/test/test_workspace.py +++ b/test/test_workspace.py @@ -4,7 +4,6 @@ import pytest from pylsp import uris -from pylsp.workspace import apply_text_edits DOC_URI = uris.from_fs_path(__file__) @@ -293,337 +292,3 @@ def test_settings_of_added_workspace(pylsp, tmpdir): workspace1_object = pylsp.workspaces[workspace1['uri']] workspace1_jedi_settings = workspace1_object._config.plugin_settings('jedi') assert workspace1_jedi_settings == server_settings['pylsp']['plugins']['jedi'] - - -def test_apply_text_edits_insert(pylsp): - pylsp.workspace.put_document(DOC_URI, '012345678901234567890123456789') - test_doc = pylsp.workspace.get_document(DOC_URI) - - assert apply_text_edits(test_doc, [{ - "range": { - "start": { - "line": 0, - "character": 0 - }, - "end": { - "line": 0, - "character": 0 - } - }, - "newText": "Hello" - }]) == 'Hello012345678901234567890123456789' - assert apply_text_edits(test_doc, [{ - "range": { - "start": { - "line": 0, - "character": 1 - }, - "end": { - "line": 0, - "character": 1 - } - }, - "newText": "Hello" - }]) == '0Hello12345678901234567890123456789' - assert apply_text_edits(test_doc, [{ - "range": { - "start": { - "line": 0, - "character": 1 - }, - "end": { - "line": 0, - "character": 1 - } - }, - "newText": "Hello" - }, { - "range": { - "start": { - "line": 0, - "character": 1 - }, - "end": { - "line": 0, - "character": 1 - } - }, - "newText": "World" - }]) == '0HelloWorld12345678901234567890123456789' - assert apply_text_edits(test_doc, [{ - "range": { - "start": { - "line": 0, - "character": 2 - }, - "end": { - "line": 0, - "character": 2 - } - }, - "newText": "One" - }, { - "range": { - "start": { - "line": 0, - "character": 1 - }, - "end": { - "line": 0, - "character": 1 - } - }, - "newText": "Hello" - }, { - "range": { - "start": { - "line": 0, - "character": 1 - }, - "end": { - "line": 0, - "character": 1 - } - }, - "newText": "World" - }, { - "range": { - "start": { - "line": 0, - "character": 2 - }, - "end": { - "line": 0, - "character": 2 - } - }, - "newText": "Two" - }, { - "range": { - "start": { - "line": 0, - "character": 2 - }, - "end": { - "line": 0, - "character": 2 - } - }, - "newText": "Three" - }]) == '0HelloWorld1OneTwoThree2345678901234567890123456789' - -def test_apply_text_edits_replace(pylsp): - pylsp.workspace.put_document(DOC_URI, '012345678901234567890123456789') - test_doc = pylsp.workspace.get_document(DOC_URI) - - assert apply_text_edits(test_doc, [{ - "range": { - "start": { - "line": 0, - "character": 3 - }, - "end": { - "line": 0, - "character": 6 - } - }, - "newText": "Hello" - }]) == '012Hello678901234567890123456789' - assert apply_text_edits(test_doc, [{ - "range": { - "start": { - "line": 0, - "character": 3 - }, - "end": { - "line": 0, - "character": 6 - } - }, - "newText": "Hello" - }, { - "range": { - "start": { - "line": 0, - "character": 6 - }, - "end": { - "line": 0, - "character": 9 - } - }, - "newText": "World" - }]) == '012HelloWorld901234567890123456789' - assert apply_text_edits(test_doc, [{ - "range": { - "start": { - "line": 0, - "character": 3 - }, - "end": { - "line": 0, - "character": 6 - } - }, - "newText": "Hello" - }, { - "range": { - "start": { - "line": 0, - "character": 6 - }, - "end": { - "line": 0, - "character": 6 - } - }, - "newText": "World" - }]) == '012HelloWorld678901234567890123456789' - assert apply_text_edits(test_doc, [{ - "range": { - "start": { - "line": 0, - "character": 6 - }, - "end": { - "line": 0, - "character": 6 - } - }, - "newText": "World" - }, { - "range": { - "start": { - "line": 0, - "character": 3 - }, - "end": { - "line": 0, - "character": 6 - } - }, - "newText": "Hello" - }]) == '012HelloWorld678901234567890123456789' - assert apply_text_edits(test_doc, [{ - "range": { - "start": { - "line": 0, - "character": 3 - }, - "end": { - "line": 0, - "character": 3 - } - }, - "newText": "World" - }, { - "range": { - "start": { - "line": 0, - "character": 3 - }, - "end": { - "line": 0, - "character": 6 - } - }, - "newText": "Hello" - }]) == '012WorldHello678901234567890123456789' - - -def test_apply_text_edits_overlap(pylsp): - pylsp.workspace.put_document(DOC_URI, '012345678901234567890123456789') - test_doc = pylsp.workspace.get_document(DOC_URI) - - did_throw = False - try: - apply_text_edits(test_doc, [{ - "range": { - "start": { - "line": 0, - "character": 3 - }, - "end": { - "line": 0, - "character": 6 - } - }, - "newText": "Hello" - }, { - "range": { - "start": { - "line": 0, - "character": 3 - }, - "end": { - "line": 0, - "character": 3 - } - }, - "newText": "World" - }]) - except Exception: - did_throw = True - - assert did_throw - - try: - apply_text_edits(test_doc, [{ - "range": { - "start": { - "line": 0, - "character": 3 - }, - "end": { - "line": 0, - "character": 6 - } - }, - "newText": "Hello" - }, { - "range": { - "start": { - "line": 0, - "character": 4 - }, - "end": { - "line": 0, - "character": 4 - } - }, - "newText": "World" - }]) - except Exception: - did_throw = True - - assert did_throw - -def test_apply_text_edits_multiline(pylsp): - pylsp.workspace.put_document(DOC_URI, '0\n1\n2\n3\n4') - test_doc = pylsp.workspace.get_document(DOC_URI) - - assert apply_text_edits(test_doc, [{ - "range": { - "start": { - "line": 2, - "character": 0 - }, - "end": { - "line": 3, - "character": 0 - } - }, - "newText": "Hello" - }, { - "range": { - "start": { - "line": 1, - "character": 1 - }, - "end": { - "line": 1, - "character": 1 - } - }, - "newText": "World" - }]) == '0\n1World\nHello3\n4' From 2f9a5095f7ff12935b0d1b855f9f33ed2fc5476a Mon Sep 17 00:00:00 2001 From: masfrost Date: Thu, 2 Jun 2022 13:13:30 -0700 Subject: [PATCH 09/21] handle EOF newline and an extra test --- pylsp/plugins/yapf_format.py | 27 ++++++++++++++++++++++++--- test/plugins/test_yapf_format.py | 27 ++++++++++++++++++++------- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/pylsp/plugins/yapf_format.py b/pylsp/plugins/yapf_format.py index 4cda5dce..b4d61689 100644 --- a/pylsp/plugins/yapf_format.py +++ b/pylsp/plugins/yapf_format.py @@ -100,8 +100,10 @@ def _format(document, lines=None, options=None): diff = next(patch_generator) patch_generator.close() - # To keep things simple our text edits will be line based - # and uncompacted + # To keep things simple our text edits will be line based. + # We will also return the edits uncompacted, meaning a + # line replacement will come in as a line remove followed + # by a line add instead of a line replace. textEdits = [] # keep track of line number since additions # don't include the line number it's being added @@ -109,7 +111,7 @@ def _format(document, lines=None, options=None): prev_line_no = -1 for change in diff.changes: if change.old and change.new: - # no change + # old and new are the same line, no change # diffs are 1-indexed prev_line_no = change.old - 1 elif change.new: @@ -149,4 +151,23 @@ def _format(document, lines=None, options=None): }) prev_line_no = lsp_line_no + # diffs don't include EOF newline https://github.com/google/yapf/issues/1008 + # we'll add it ourselves if our document doesn't already have it and the diff + # does not change the last line. + if not source.endswith(eol_chars) and diff.changes \ + and diff.changes[-1].old and diff.changes[-1].new: + textEdits.append({ + 'range': { + 'start': { + 'line': prev_line_no, + 'character': 0 + }, + 'end': { + 'line': prev_line_no + 1, + 'character': 0 + } + }, + 'newText': diff.changes[-1].line + eol_chars + }) + return textEdits \ No newline at end of file diff --git a/test/plugins/test_yapf_format.py b/test/plugins/test_yapf_format.py index 45b8cd87..ce82ad8d 100644 --- a/test/plugins/test_yapf_format.py +++ b/test/plugins/test_yapf_format.py @@ -68,7 +68,7 @@ def test_config_file(tmpdir, workspace): def test_line_endings(workspace, newline): doc = Document(DOC_URI, workspace, f'import os;import sys{2 * newline}dict(a=1)') res = pylsp_format_document(doc) - + assert apply_text_edits(doc, res) == f'import os{newline}import sys{2 * newline}dict(a=1){newline}' @@ -76,21 +76,34 @@ def test_format_with_tab_size_option(workspace): doc = Document(DOC_URI, workspace, FOUR_SPACE_DOC) res = pylsp_format_document(doc, {"tabSize": "8"}) - assert len(res) == 1 - assert res[0]['newText'] == FOUR_SPACE_DOC.replace(" ", " ") + assert apply_text_edits(doc, res) == FOUR_SPACE_DOC.replace(" ", " ") def test_format_with_insert_spaces_option(workspace): doc = Document(DOC_URI, workspace, FOUR_SPACE_DOC) res = pylsp_format_document(doc, {"insertSpaces": False}) - assert len(res) == 1 - assert res[0]['newText'] == FOUR_SPACE_DOC.replace(" ", "\t") + assert apply_text_edits(doc, res) == FOUR_SPACE_DOC.replace(" ", "\t") def test_format_with_yapf_specific_option(workspace): doc = Document(DOC_URI, workspace, FOUR_SPACE_DOC) res = pylsp_format_document(doc, {"USE_TABS": True}) - assert len(res) == 1 - assert res[0]['newText'] == FOUR_SPACE_DOC.replace(" ", "\t") + assert apply_text_edits(doc, res) == FOUR_SPACE_DOC.replace(" ", "\t") + +def test_format_returns_text_edit_per_line(workspace): + single_space_indent = """def wow(): + print("x") + print("hi")""" + doc = Document(DOC_URI, workspace, single_space_indent) + res = pylsp_format_document(doc) + + # two removes and two adds + assert len(res) == 4 + assert res[0]['newText'] == "" + assert res[1]['newText'] == "" + assert res[2]['newText'] == " print(\"x\")\n" + assert res[3]['newText'] == " print(\"hi\")\n" + + From c1f5d2a1b2edbe5c05311a8936e30a9f2e747b73 Mon Sep 17 00:00:00 2001 From: masfrost Date: Thu, 2 Jun 2022 13:15:11 -0700 Subject: [PATCH 10/21] lint --- pylsp/plugins/yapf_format.py | 2 +- test/plugins/test_yapf_format.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/pylsp/plugins/yapf_format.py b/pylsp/plugins/yapf_format.py index b4d61689..b2a202c8 100644 --- a/pylsp/plugins/yapf_format.py +++ b/pylsp/plugins/yapf_format.py @@ -170,4 +170,4 @@ def _format(document, lines=None, options=None): 'newText': diff.changes[-1].line + eol_chars }) - return textEdits \ No newline at end of file + return textEdits diff --git a/test/plugins/test_yapf_format.py b/test/plugins/test_yapf_format.py index ce82ad8d..fe0826a1 100644 --- a/test/plugins/test_yapf_format.py +++ b/test/plugins/test_yapf_format.py @@ -68,7 +68,7 @@ def test_config_file(tmpdir, workspace): def test_line_endings(workspace, newline): doc = Document(DOC_URI, workspace, f'import os;import sys{2 * newline}dict(a=1)') res = pylsp_format_document(doc) - + assert apply_text_edits(doc, res) == f'import os{newline}import sys{2 * newline}dict(a=1){newline}' @@ -105,5 +105,3 @@ def test_format_returns_text_edit_per_line(workspace): assert res[1]['newText'] == "" assert res[2]['newText'] == " print(\"x\")\n" assert res[3]['newText'] == " print(\"hi\")\n" - - From 55f6eab14677cfbd7f38eb6e1d5275dd7ed31b7e Mon Sep 17 00:00:00 2001 From: masfrost Date: Thu, 2 Jun 2022 14:35:09 -0700 Subject: [PATCH 11/21] Make pylint happier --- pylsp/_utils.py | 4 +- pylsp/plugins/yapf_format.py | 156 ++++++++++++++++++------------- pylsp/text_edit.py | 37 ++++---- test/plugins/test_yapf_format.py | 12 ++- test/test_text_edit.py | 106 +++++++++++---------- 5 files changed, 178 insertions(+), 137 deletions(-) diff --git a/pylsp/_utils.py b/pylsp/_utils.py index 0732067a..8010b96b 100644 --- a/pylsp/_utils.py +++ b/pylsp/_utils.py @@ -228,8 +228,8 @@ def is_process_alive(pid): def get_eol_chars(text): - """Get EOL chars used in text.""" + """Get EOL chars used in text. Defaults to line feed""" match = EOL_REGEX.search(text) if match: return match.group(0) - return None + return "\n" diff --git a/pylsp/plugins/yapf_format.py b/pylsp/plugins/yapf_format.py index b2a202c8..22ff9a5a 100644 --- a/pylsp/plugins/yapf_format.py +++ b/pylsp/plugins/yapf_format.py @@ -38,77 +38,62 @@ def pylsp_format_range(document, range, options=None): # pylint: disable=redefi return _format(document, lines=lines, options=options) -def _format(document, lines=None, options=None): - source = document.source - # Yapf doesn't work with CRLF/CR line endings, so we replace them by '\n' - # and restore them below when adding new lines - eol_chars = get_eol_chars(source) - if eol_chars in ['\r', '\r\n']: - source = source.replace(eol_chars, '\n') - +def get_style_config(document_path, options=None): # Get the default styles as a string # for a preset configuration, i.e. "pep8" style_config = file_resources.GetDefaultStyleForDir( - os.path.dirname(document.path) + os.path.dirname(document_path) ) - if options is not None: - # We have options passed from LSP format request - # let's pass them to the formatter. - # First we want to get a dictionary of the preset style - # to pass instead of a string so that we can modify it - style_config = style.CreateStyleFromConfig(style_config) + if options is None: + return style_config - use_tabs = style_config['USE_TABS'] - indent_width = style_config['INDENT_WIDTH'] + # We have options passed from LSP format request + # let's pass them to the formatter. + # First we want to get a dictionary of the preset style + # to pass instead of a string so that we can modify it + style_config = style.CreateStyleFromConfig(style_config) - if options.get('tabSize') is not None: - indent_width = max(int(options.get('tabSize')), 1) + use_tabs = style_config['USE_TABS'] + indent_width = style_config['INDENT_WIDTH'] - if options.get('insertSpaces') is not None: - # TODO is it guaranteed to be a boolean, or can it be a string? - use_tabs = not options.get('insertSpaces') + if options.get('tabSize') is not None: + indent_width = max(int(options.get('tabSize')), 1) - if use_tabs: - # Indent width doesn't make sense when using tabs - # the specifications state: "Size of a tab in spaces" - indent_width = 1 + if options.get('insertSpaces') is not None: + # TODO is it guaranteed to be a boolean, or can it be a string? + use_tabs = not options.get('insertSpaces') - style_config['USE_TABS'] = use_tabs - style_config['INDENT_WIDTH'] = indent_width - style_config['CONTINUATION_INDENT_WIDTH'] = indent_width + if use_tabs: + # Indent width doesn't make sense when using tabs + # the specifications state: "Size of a tab in spaces" + indent_width = 1 - for style_option, value in options.items(): - # Apply arbitrary options passed as formatter options - if style_option not in style_config: - # ignore if it's not a known yapf config - continue + style_config['USE_TABS'] = use_tabs + style_config['INDENT_WIDTH'] = indent_width + style_config['CONTINUATION_INDENT_WIDTH'] = indent_width - style_config[style_option] = value + for style_option, value in options.items(): + # Apply arbitrary options passed as formatter options + if style_option not in style_config: + # ignore if it's not a known yapf config + continue - diff_txt, changed = FormatCode( - source, - lines=lines, - filename=document.filename, - print_diff=True, - style_config=style_config - ) + style_config[style_option] = value - if not changed: - return [] + return style_config - patch_generator = whatthepatch.parse_patch(diff_txt) - diff = next(patch_generator) - patch_generator.close() +def diff_to_text_edits(diff, eol_chars): # To keep things simple our text edits will be line based. # We will also return the edits uncompacted, meaning a # line replacement will come in as a line remove followed # by a line add instead of a line replace. - textEdits = [] + text_edits = [] # keep track of line number since additions # don't include the line number it's being added # to in diffs. lsp is 0-indexed so we'll start with -1 prev_line_no = -1 + for change in diff.changes: if change.old and change.new: # old and new are the same line, no change @@ -116,7 +101,7 @@ def _format(document, lines=None, options=None): prev_line_no = change.old - 1 elif change.new: # addition - textEdits.append({ + text_edits.append({ 'range': { 'start': { 'line': prev_line_no + 1, @@ -132,7 +117,7 @@ def _format(document, lines=None, options=None): elif change.old: # remove lsp_line_no = change.old - 1 - textEdits.append({ + text_edits.append({ 'range': { 'start': { 'line': lsp_line_no, @@ -151,23 +136,64 @@ def _format(document, lines=None, options=None): }) prev_line_no = lsp_line_no + return text_edits + + +def ensure_eof_new_line(document, eol_chars, text_edits): # diffs don't include EOF newline https://github.com/google/yapf/issues/1008 # we'll add it ourselves if our document doesn't already have it and the diff # does not change the last line. - if not source.endswith(eol_chars) and diff.changes \ - and diff.changes[-1].old and diff.changes[-1].new: - textEdits.append({ - 'range': { - 'start': { - 'line': prev_line_no, - 'character': 0 - }, - 'end': { - 'line': prev_line_no + 1, - 'character': 0 - } + if document.source.endswith(eol_chars): + return + + lines = document.lines + last_line_number = len(lines) - 1 + + if text_edits and text_edits[-1]['range']['start']['line'] >= last_line_number: + return + + text_edits.append({ + 'range': { + 'start': { + 'line': last_line_number, + 'character': 0 }, - 'newText': diff.changes[-1].line + eol_chars - }) + 'end': { + 'line': last_line_number + 1, + 'character': 0 + } + }, + 'newText': lines[-1] + eol_chars + }) + + +def _format(document, lines=None, options=None): + source = document.source + # Yapf doesn't work with CRLF/CR line endings, so we replace them by '\n' + # and restore them below when adding new lines + eol_chars = get_eol_chars(source) + if eol_chars in ['\r', '\r\n']: + source = source.replace(eol_chars, '\n') + + style_config = get_style_config(document_path=document.path, options=options) + + diff_txt, changed = FormatCode( + source, + lines=lines, + filename=document.filename, + print_diff=True, + style_config=style_config + ) + + if not changed: + return [] + + patch_generator = whatthepatch.parse_patch(diff_txt) + diff = next(patch_generator) + patch_generator.close() + + text_edits = diff_to_text_edits(diff=diff, eol_chars=eol_chars) + + ensure_eof_new_line(document=document, eol_chars=eol_chars, text_edits=text_edits) - return textEdits + return text_edits diff --git a/pylsp/text_edit.py b/pylsp/text_edit.py index 36d0d726..b8030a64 100644 --- a/pylsp/text_edit.py +++ b/pylsp/text_edit.py @@ -1,30 +1,33 @@ -def get_well_formatted_range(range): - start = range['start'] - end = range['end'] +def get_well_formatted_range(lsp_range): + start = lsp_range['start'] + end = lsp_range['end'] if start['line'] > end['line'] or (start['line'] == end['line'] and start['character'] > end['character']): - return { 'start': end, 'end': start } + return {'start': end, 'end': start} + + return lsp_range - return range def get_well_formatted_edit(text_edit): - range = get_well_formatted_range(text_edit['range']) - if range != text_edit['range']: - return { 'newText': text_edit['newText'], 'range': range } - + lsp_range = get_well_formatted_range(text_edit['range']) + if lsp_range != text_edit['range']: + return {'newText': text_edit['newText'], 'range': lsp_range} + return text_edit + def compare_text_edits(a, b): diff = a['range']['start']['line'] - b['range']['start']['line'] if diff == 0: return a['range']['start']['character'] - b['range']['start']['character'] - + return diff + def merge_sort_text_edits(text_edits): if len(text_edits) <= 1: return text_edits - + p = len(text_edits) // 2 left = text_edits[:p] right = text_edits[p:] @@ -45,8 +48,8 @@ def merge_sort_text_edits(text_edits): else: # greater -> take right text_edits[i] = right[right_idx] - i+=1 - right_idx +=1 + i += 1 + right_idx += 1 while left_idx < len(left): text_edits[i] = left[left_idx] i += 1 @@ -57,18 +60,20 @@ def merge_sort_text_edits(text_edits): right_idx += 1 return text_edits + def apply_text_edits(doc, text_edits): text = doc.source - sorted_edits = merge_sort_text_edits(list(map(get_well_formatted_edit,text_edits))) + sorted_edits = merge_sort_text_edits(list(map(get_well_formatted_edit, text_edits))) last_modified_offset = 0 spans = [] for e in sorted_edits: start_offset = doc.offset_at_position(e['range']['start']) if start_offset < last_modified_offset: raise Exception('overlapping edit') - elif start_offset > last_modified_offset: + + if start_offset > last_modified_offset: spans.append(text[last_modified_offset:start_offset]) - + if len(e['newText']): spans.append(e['newText']) last_modified_offset = doc.offset_at_position(e['range']['end']) diff --git a/test/plugins/test_yapf_format.py b/test/plugins/test_yapf_format.py index fe0826a1..1a965a27 100644 --- a/test/plugins/test_yapf_format.py +++ b/test/plugins/test_yapf_format.py @@ -64,7 +64,8 @@ def test_config_file(tmpdir, workspace): # A was split on multiple lines because of column_limit from config file assert apply_text_edits(doc, res) == "A = [\n 'h', 'w',\n 'a'\n]\n\nB = ['h', 'w']\n" -@pytest.mark.parametrize('newline', ['\r\n', '\r']) + +@pytest.mark.parametrize('newline', ['\r\n']) def test_line_endings(workspace, newline): doc = Document(DOC_URI, workspace, f'import os;import sys{2 * newline}dict(a=1)') res = pylsp_format_document(doc) @@ -92,10 +93,11 @@ def test_format_with_yapf_specific_option(workspace): assert apply_text_edits(doc, res) == FOUR_SPACE_DOC.replace(" ", "\t") + def test_format_returns_text_edit_per_line(workspace): single_space_indent = """def wow(): - print("x") - print("hi")""" + log("x") + log("hi")""" doc = Document(DOC_URI, workspace, single_space_indent) res = pylsp_format_document(doc) @@ -103,5 +105,5 @@ def test_format_returns_text_edit_per_line(workspace): assert len(res) == 4 assert res[0]['newText'] == "" assert res[1]['newText'] == "" - assert res[2]['newText'] == " print(\"x\")\n" - assert res[3]['newText'] == " print(\"hi\")\n" + assert res[2]['newText'] == " log(\"x\")\n" + assert res[3]['newText'] == " log(\"hi\")\n" diff --git a/test/test_text_edit.py b/test/test_text_edit.py index 20f8098f..79c1fba0 100644 --- a/test/test_text_edit.py +++ b/test/test_text_edit.py @@ -3,6 +3,7 @@ DOC_URI = uris.from_fs_path(__file__) + def test_apply_text_edits_insert(pylsp): pylsp.workspace.put_document(DOC_URI, '012345678901234567890123456789') test_doc = pylsp.workspace.get_document(DOC_URI) @@ -120,6 +121,7 @@ def test_apply_text_edits_insert(pylsp): "newText": "Three" }]) == '0HelloWorld1OneTwoThree2345678901234567890123456789' + def test_apply_text_edits_replace(pylsp): pylsp.workspace.put_document(DOC_URI, '012345678901234567890123456789') test_doc = pylsp.workspace.get_document(DOC_URI) @@ -243,69 +245,75 @@ def test_apply_text_edits_overlap(pylsp): pylsp.workspace.put_document(DOC_URI, '012345678901234567890123456789') test_doc = pylsp.workspace.get_document(DOC_URI) - did_throw = False - try: - apply_text_edits(test_doc, [{ - "range": { - "start": { - "line": 0, - "character": 3 - }, - "end": { - "line": 0, - "character": 6 - } + over_lapping_edits1 = [{ + "range": { + "start": { + "line": 0, + "character": 3 }, - "newText": "Hello" - }, { - "range": { - "start": { - "line": 0, - "character": 3 - }, - "end": { - "line": 0, - "character": 3 - } + "end": { + "line": 0, + "character": 6 + } + }, + "newText": "Hello" + }, { + "range": { + "start": { + "line": 0, + "character": 3 }, - "newText": "World" - }]) + "end": { + "line": 0, + "character": 3 + } + }, + "newText": "World" + }] + + did_throw = False + try: + apply_text_edits(test_doc, over_lapping_edits1) except Exception: did_throw = True assert did_throw - try: - apply_text_edits(test_doc, [{ - "range": { - "start": { - "line": 0, - "character": 3 - }, - "end": { - "line": 0, - "character": 6 - } + over_lapping_edits2 = [{ + "range": { + "start": { + "line": 0, + "character": 3 }, - "newText": "Hello" - }, { - "range": { - "start": { - "line": 0, - "character": 4 - }, - "end": { - "line": 0, - "character": 4 - } + "end": { + "line": 0, + "character": 6 + } + }, + "newText": "Hello" + }, { + "range": { + "start": { + "line": 0, + "character": 4 }, - "newText": "World" - }]) + "end": { + "line": 0, + "character": 4 + } + }, + "newText": "World" + }] + did_throw = False + + try: + apply_text_edits(test_doc, over_lapping_edits2) except Exception: did_throw = True assert did_throw + def test_apply_text_edits_multiline(pylsp): pylsp.workspace.put_document(DOC_URI, '0\n1\n2\n3\n4') test_doc = pylsp.workspace.get_document(DOC_URI) From 89f62111b16cb5e22a94601f8caabdd6f3fa5d20 Mon Sep 17 00:00:00 2001 From: masfrost Date: Thu, 2 Jun 2022 14:36:40 -0700 Subject: [PATCH 12/21] Match new lines first --- pylsp/_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylsp/_utils.py b/pylsp/_utils.py index 8010b96b..adc83c1a 100644 --- a/pylsp/_utils.py +++ b/pylsp/_utils.py @@ -14,7 +14,7 @@ JEDI_VERSION = jedi.__version__ # Eol chars accepted by the LSP protocol -EOL_CHARS = ['\r\n', '\r', '\n'] +EOL_CHARS = ['\n', '\r\n', '\r'] EOL_REGEX = re.compile(f'({"|".join(EOL_CHARS)})') log = logging.getLogger(__name__) From a8cd387ce69de3d224828e84d0376e90d5b0cc1c Mon Sep 17 00:00:00 2001 From: masfrost Date: Thu, 2 Jun 2022 14:52:21 -0700 Subject: [PATCH 13/21] Hopefully last lint --- pylsp/text_edit.py | 10 +++- test/test_text_edit.py | 109 ++++++++++++++++++++--------------------- 2 files changed, 62 insertions(+), 57 deletions(-) diff --git a/pylsp/text_edit.py b/pylsp/text_edit.py index b8030a64..998ab67b 100644 --- a/pylsp/text_edit.py +++ b/pylsp/text_edit.py @@ -61,6 +61,14 @@ def merge_sort_text_edits(text_edits): return text_edits +class OverLappingTextEditException(Exception): + """ + Text edits are expected to be sorted + and compressed instead of overlapping. + This error is raised when two edits + are overlapping. + """ + def apply_text_edits(doc, text_edits): text = doc.source sorted_edits = merge_sort_text_edits(list(map(get_well_formatted_edit, text_edits))) @@ -69,7 +77,7 @@ def apply_text_edits(doc, text_edits): for e in sorted_edits: start_offset = doc.offset_at_position(e['range']['start']) if start_offset < last_modified_offset: - raise Exception('overlapping edit') + raise OverLappingTextEditException('overlapping edit') if start_offset > last_modified_offset: spans.append(text[last_modified_offset:start_offset]) diff --git a/test/test_text_edit.py b/test/test_text_edit.py index 79c1fba0..3253c2af 100644 --- a/test/test_text_edit.py +++ b/test/test_text_edit.py @@ -1,4 +1,4 @@ -from pylsp.text_edit import apply_text_edits +from pylsp.text_edit import OverLappingTextEditException, apply_text_edits from pylsp import uris DOC_URI = uris.from_fs_path(__file__) @@ -245,70 +245,67 @@ def test_apply_text_edits_overlap(pylsp): pylsp.workspace.put_document(DOC_URI, '012345678901234567890123456789') test_doc = pylsp.workspace.get_document(DOC_URI) - over_lapping_edits1 = [{ - "range": { - "start": { - "line": 0, - "character": 3 - }, - "end": { - "line": 0, - "character": 6 - } - }, - "newText": "Hello" - }, { - "range": { - "start": { - "line": 0, - "character": 3 - }, - "end": { - "line": 0, - "character": 3 - } - }, - "newText": "World" - }] - did_throw = False try: - apply_text_edits(test_doc, over_lapping_edits1) - except Exception: + apply_text_edits(test_doc, [{ + "range": { + "start": { + "line": 0, + "character": 3 + }, + "end": { + "line": 0, + "character": 6 + } + }, + "newText": "Hello" + }, { + "range": { + "start": { + "line": 0, + "character": 3 + }, + "end": { + "line": 0, + "character": 3 + } + }, + "newText": "World" + }]) + except OverLappingTextEditException: did_throw = True assert did_throw - over_lapping_edits2 = [{ - "range": { - "start": { - "line": 0, - "character": 3 - }, - "end": { - "line": 0, - "character": 6 - } - }, - "newText": "Hello" - }, { - "range": { - "start": { - "line": 0, - "character": 4 - }, - "end": { - "line": 0, - "character": 4 - } - }, - "newText": "World" - }] did_throw = False try: - apply_text_edits(test_doc, over_lapping_edits2) - except Exception: + apply_text_edits(test_doc, [{ + "range": { + "start": { + "line": 0, + "character": 3 + }, + "end": { + "line": 0, + "character": 6 + } + }, + "newText": "Hello" + }, { + "range": { + "start": { + "line": 0, + "character": 4 + }, + "end": { + "line": 0, + "character": 4 + } + }, + "newText": "World" + }]) + except OverLappingTextEditException: did_throw = True assert did_throw From 1849f7df8cc242b2b8186e36f473ae29b9443829 Mon Sep 17 00:00:00 2001 From: Faris Masad Date: Thu, 2 Jun 2022 14:59:55 -0700 Subject: [PATCH 14/21] Update setup.cfg --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index e4a3aa9f..933ff6a4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,4 +4,4 @@ [pycodestyle] ignore = E226, E722, W504 max-line-length = 120 -exclude = test/plugins/.ropeproject,test/.ropeproject \ No newline at end of file +exclude = test/plugins/.ropeproject,test/.ropeproject From 06f5e34c974652f7395940e0cd4aa1f0c4cfbf45 Mon Sep 17 00:00:00 2001 From: Faris Masad Date: Mon, 6 Jun 2022 08:58:13 -0700 Subject: [PATCH 15/21] Undo change to EOL_CHARS --- pylsp/_utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pylsp/_utils.py b/pylsp/_utils.py index adc83c1a..841970c0 100644 --- a/pylsp/_utils.py +++ b/pylsp/_utils.py @@ -14,7 +14,8 @@ JEDI_VERSION = jedi.__version__ # Eol chars accepted by the LSP protocol -EOL_CHARS = ['\n', '\r\n', '\r'] +# the ordering affects performance +EOL_CHARS = ['\r\n', '\r', '\n'] EOL_REGEX = re.compile(f'({"|".join(EOL_CHARS)})') log = logging.getLogger(__name__) From 1fb8ff276311b6584370320fdb6a23d52fe157d3 Mon Sep 17 00:00:00 2001 From: masfrost Date: Mon, 6 Jun 2022 09:00:48 -0700 Subject: [PATCH 16/21] Undo changes to get_eol_chars --- pylsp/_utils.py | 4 ++-- pylsp/plugins/yapf_format.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pylsp/_utils.py b/pylsp/_utils.py index 841970c0..8c4b5496 100644 --- a/pylsp/_utils.py +++ b/pylsp/_utils.py @@ -229,8 +229,8 @@ def is_process_alive(pid): def get_eol_chars(text): - """Get EOL chars used in text. Defaults to line feed""" + """Get EOL chars used in text.""" match = EOL_REGEX.search(text) if match: return match.group(0) - return "\n" + return None diff --git a/pylsp/plugins/yapf_format.py b/pylsp/plugins/yapf_format.py index 22ff9a5a..c1b89051 100644 --- a/pylsp/plugins/yapf_format.py +++ b/pylsp/plugins/yapf_format.py @@ -174,6 +174,8 @@ def _format(document, lines=None, options=None): eol_chars = get_eol_chars(source) if eol_chars in ['\r', '\r\n']: source = source.replace(eol_chars, '\n') + else: + eol_chars = '\n' style_config = get_style_config(document_path=document.path, options=options) From c1daaf88eaeb43627dade19b7e0299555fc2d998 Mon Sep 17 00:00:00 2001 From: masfrost Date: Mon, 6 Jun 2022 09:01:12 -0700 Subject: [PATCH 17/21] Add back new line --- test/test_workspace.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_workspace.py b/test/test_workspace.py index 1287599a..44d754b2 100644 --- a/test/test_workspace.py +++ b/test/test_workspace.py @@ -5,6 +5,7 @@ import pytest from pylsp import uris + DOC_URI = uris.from_fs_path(__file__) From 5196079b6c288b030ddf4817ee276eac42291497 Mon Sep 17 00:00:00 2001 From: masfrost Date: Mon, 6 Jun 2022 09:02:28 -0700 Subject: [PATCH 18/21] Add copyright headers --- pylsp/text_edit.py | 3 +++ test/test_text_edit.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/pylsp/text_edit.py b/pylsp/text_edit.py index 998ab67b..c870000c 100644 --- a/pylsp/text_edit.py +++ b/pylsp/text_edit.py @@ -1,3 +1,6 @@ +# Copyright 2017-2020 Palantir Technologies, Inc. +# Copyright 2021- Python Language Server Contributors. + def get_well_formatted_range(lsp_range): start = lsp_range['start'] end = lsp_range['end'] diff --git a/test/test_text_edit.py b/test/test_text_edit.py index 3253c2af..3e4cce11 100644 --- a/test/test_text_edit.py +++ b/test/test_text_edit.py @@ -1,3 +1,6 @@ +# Copyright 2017-2020 Palantir Technologies, Inc. +# Copyright 2021- Python Language Server Contributors. + from pylsp.text_edit import OverLappingTextEditException, apply_text_edits from pylsp import uris From ea6fc90c93408e28811199c38b63ea2dd46da25e Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Mon, 6 Jun 2022 12:10:27 -0500 Subject: [PATCH 19/21] Fix linting issues --- pylsp/config/config.py | 1 + pylsp/python_lsp.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/pylsp/config/config.py b/pylsp/config/config.py index 27a76bde..c63f61a3 100644 --- a/pylsp/config/config.py +++ b/pylsp/config/config.py @@ -82,6 +82,7 @@ def __init__(self, root_uri, init_opts, process_id, capabilities): log.info("Loaded pylsp plugin %s from %s", name, plugin) for plugin_conf in self._pm.hook.pylsp_settings(config=self): + # pylint: disable=no-member self._plugin_settings = _utils.merge_dicts(self._plugin_settings, plugin_conf) self._update_disabled_plugins() diff --git a/pylsp/python_lsp.py b/pylsp/python_lsp.py index 8cac63d5..f06d59ca 100644 --- a/pylsp/python_lsp.py +++ b/pylsp/python_lsp.py @@ -34,6 +34,7 @@ class _StreamHandlerWrapper(socketserver.StreamRequestHandler): def setup(self): super().setup() + # pylint: disable=no-member self.delegate = self.DELEGATE_CLASS(self.rfile, self.wfile) def handle(self): @@ -121,6 +122,7 @@ async def pylsp_ws(websocket): async for message in websocket: try: log.debug("consuming payload and feeding it to LSP handler") + # pylint: disable=c-extension-no-member request = json.loads(message) loop = asyncio.get_running_loop() await loop.run_in_executor(tpool, pylsp_handler.consume, request) @@ -130,6 +132,7 @@ async def pylsp_ws(websocket): def send_message(message, websocket): """Handler to send responses of processed requests to respective web socket clients""" try: + # pylint: disable=c-extension-no-member payload = json.dumps(message, ensure_ascii=False) asyncio.run(websocket.send(payload)) except Exception as e: # pylint: disable=broad-except From c404a51924f6a9f863c7f2f6c39e58dd81da3bc6 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Mon, 6 Jun 2022 12:16:20 -0500 Subject: [PATCH 20/21] More linting fixes --- pylsp/config/config.py | 2 +- pylsp/python_lsp.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pylsp/config/config.py b/pylsp/config/config.py index c63f61a3..5637ca60 100644 --- a/pylsp/config/config.py +++ b/pylsp/config/config.py @@ -81,8 +81,8 @@ def __init__(self, root_uri, init_opts, process_id, capabilities): if plugin is not None: log.info("Loaded pylsp plugin %s from %s", name, plugin) + # pylint: disable=no-member for plugin_conf in self._pm.hook.pylsp_settings(config=self): - # pylint: disable=no-member self._plugin_settings = _utils.merge_dicts(self._plugin_settings, plugin_conf) self._update_disabled_plugins() diff --git a/pylsp/python_lsp.py b/pylsp/python_lsp.py index f06d59ca..94e7a8cf 100644 --- a/pylsp/python_lsp.py +++ b/pylsp/python_lsp.py @@ -48,6 +48,7 @@ def handle(self): if isinstance(e, WindowsError) and e.winerror == 10054: pass + # pylint: disable=no-member self.SHUTDOWN_CALL() From 53c1caa42829d28ed4410600c2b62389f913279d Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Mon, 6 Jun 2022 12:20:25 -0500 Subject: [PATCH 21/21] Fix pycodestyle issue --- pylsp/text_edit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pylsp/text_edit.py b/pylsp/text_edit.py index c870000c..24d74eeb 100644 --- a/pylsp/text_edit.py +++ b/pylsp/text_edit.py @@ -72,6 +72,7 @@ class OverLappingTextEditException(Exception): are overlapping. """ + def apply_text_edits(doc, text_edits): text = doc.source sorted_edits = merge_sort_text_edits(list(map(get_well_formatted_edit, text_edits)))