From 3b59a36e04e0e91c24bcdc611f851bcfe7cdc114 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Thu, 1 Feb 2024 11:48:05 +0100 Subject: [PATCH] Narrow text edits --- ruff_lsp/server.py | 39 +++++++++++++++++++++++++++++++++++---- tests/test_format.py | 6 +++++- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/ruff_lsp/server.py b/ruff_lsp/server.py index 5177df2..35a6ab7 100755 --- a/ruff_lsp/server.py +++ b/ruff_lsp/server.py @@ -16,7 +16,6 @@ from pathlib import Path from typing import Any, List, NamedTuple, Sequence, Union, cast -from lsprotocol import validators from lsprotocol.types import ( CODE_ACTION_RESOLVE, INITIALIZE, @@ -1379,13 +1378,45 @@ def _fixed_source_to_edits( if new_source == original_source: return [] + # Reduce the text edit by omitting the common suffix and postfix (lines) + # from the text edit. I chose this basic diffing because "proper" diffing has + # the downside that it can be very slow in some cases. Black uses a diffing approach + # that takes time into consideration, but it requires spawning a thread to stop + # the diffing after a given time, which feels very heavy weight. + # This basic "diffing" has a guaranteed `O(n)` runtime and is sufficient to + # prevent transmitting the entire source document when formatting a range + # or formatting a document where most of the code remains unchanged. + # + # https://github.com/microsoft/vscode-black-formatter/blob/main/bundled/tool/lsp_edit_utils.py + new_lines = new_source.splitlines(True) + original_lines = original_source.splitlines(True) + + start_offset = 0 + end_offset = 0 + + for new_line, original_line in zip(new_lines, original_lines): + if new_line == original_line: + start_offset += 1 + else: + break + + for new_line, original_line in zip( + reversed(new_lines[start_offset:]), reversed(original_lines[start_offset:]) + ): + if new_line == original_line: + end_offset += 1 + else: + break + + trimmed_new_source = "".join(new_lines[start_offset : len(new_lines) - end_offset]) + return [ TextEdit( range=Range( - start=Position(line=0, character=0), - end=Position(line=validators.UINTEGER_MAX_VALUE, character=0), + start=Position(line=start_offset, character=0), + end=Position(line=len(original_lines) - end_offset, character=0), ), - new_text=new_source, + new_text=trimmed_new_source, ) ] diff --git a/tests/test_format.py b/tests/test_format.py index 09a94b9..21ec62e 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -3,6 +3,7 @@ from contextlib import nullcontext import pytest +from lsprotocol.types import Position, Range from packaging.version import Version from pygls.workspace import Workspace @@ -46,7 +47,10 @@ async def test_format(tmp_path, ruff_version: Version): [edit] = _fixed_source_to_edits( original_source=document.source, fixed_source=result.stdout.decode("utf-8") ) - assert edit.new_text == expected + assert edit.new_text == "" + assert edit.range == Range( + start=Position(line=0, character=0), end=Position(line=1, character=0) + ) @pytest.mark.asyncio