From e57e9c7ddfd5ff79d86c558cefc14800366b57bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Furga=C5=82a?= <83299832+00200200@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:18:26 +0200 Subject: [PATCH] Fix rename replacement ranges for files without a final newline --- pylsp/plugins/jedi_rename.py | 17 ++++++++--------- test/plugins/test_jedi_rename.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/pylsp/plugins/jedi_rename.py b/pylsp/plugins/jedi_rename.py index b35e321a..12240388 100644 --- a/pylsp/plugins/jedi_rename.py +++ b/pylsp/plugins/jedi_rename.py @@ -29,6 +29,7 @@ def pylsp_rename(config, workspace, document, position, new_name): for file_path, changed_file in changed_files.items(): uri = uris.from_fs_path(str(file_path)) doc = workspace.get_maybe_document(uri) + source = workspace.get_document(uri).source changes.append( { "textDocument": {"uri": uri, "version": doc.version if doc else None}, @@ -36,10 +37,7 @@ def pylsp_rename(config, workspace, document, position, new_name): { "range": { "start": {"line": 0, "character": 0}, - "end": { - "line": _num_lines(changed_file.get_new_code()), - "character": 0, - }, + "end": _end_position(source), }, "newText": changed_file.get_new_code(), } @@ -49,8 +47,9 @@ def pylsp_rename(config, workspace, document, position, new_name): return {"documentChanges": changes} -def _num_lines(file_contents): - "Count the number of lines in the given string." - if _utils.get_eol_chars(file_contents): - return len(file_contents.splitlines()) - return 0 +def _end_position(file_contents): + lines = file_contents.replace("\r\n", "\n").replace("\r", "\n").split("\n") + return { + "line": len(lines) - 1, + "character": len(lines[-1].encode("utf-16-le")) // 2, + } diff --git a/test/plugins/test_jedi_rename.py b/test/plugins/test_jedi_rename.py index 349274be..e284642c 100644 --- a/test/plugins/test_jedi_rename.py +++ b/test/plugins/test_jedi_rename.py @@ -7,6 +7,7 @@ from pylsp import uris from pylsp.plugins.jedi_rename import pylsp_rename +from pylsp.text_edit import apply_text_edits from pylsp.workspace import Document DOC_NAME = "test1.py" @@ -33,6 +34,32 @@ def tmp_workspace(temp_workspace_factory): ) +@pytest.mark.parametrize( + ("source", "end"), + [ + ("foo = 12", {"line": 0, "character": 8}), + ("foo = 12\nfoo", {"line": 1, "character": 3}), + ("foo = 12\r\nfoo", {"line": 1, "character": 3}), + ("foo = 12\n", {"line": 1, "character": 0}), + ('foo = "\U0001f600\u2028"', {"line": 0, "character": 11}), + ], +) +def test_jedi_rename_replaces_entire_document( + temp_workspace_factory, config, source, end +): + workspace = temp_workspace_factory({"rename.py": source}) + uri = uris.from_fs_path(os.path.join(workspace.root_path, "rename.py")) + doc = workspace.get_document(uri) + + result = pylsp_rename( + config, workspace, doc, {"line": 0, "character": 0}, "renamed" + ) + + edits = result["documentChanges"][0]["edits"] + assert apply_text_edits(doc, edits) == source.replace("foo", "renamed") + assert edits[0]["range"]["end"] == end + + def test_jedi_rename(tmp_workspace, config) -> None: # rename the `Test1` class position = {"line": 0, "character": 6} @@ -98,7 +125,7 @@ def test_jedi_rename(tmp_workspace, config) -> None: { "range": { "start": {"line": 0, "character": 0}, - "end": {"line": 0, "character": 0}, + "end": {"line": 0, "character": 8}, }, "newText": "bar = 12", }