Skip to content

Commit

Permalink
Use narrow edits and range formatting warning
Browse files Browse the repository at this point in the history
  • Loading branch information
karthiknadig committed Oct 4, 2023
1 parent b19ba19 commit 5369db7
Show file tree
Hide file tree
Showing 10 changed files with 23,769 additions and 51 deletions.
59 changes: 59 additions & 0 deletions bundled/tool/lsp_edit_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
"""Utility functions for calculating edits."""

import bisect
import difflib
from threading import Thread
from typing import List, Optional

from lsprotocol import types as lsp

DIFF_TIMEOUT = 1 # 1 second

def _get_diff(old_text: str, new_text: str):
try:
import Levenshtein
return Levenshtein.opcodes(old_text, new_text)
except ImportError:
return difflib.SequenceMatcher(a=old_text, b=new_text).get_opcodes()


def get_text_edits(old_text: str, new_text: str, timeout: Optional[int] = None) -> List[lsp.TextEdit]:
"""Return a list of text edits to transform old_text into new_text."""

offsets = [0]
for line in old_text.splitlines(True):
offsets.append(offsets[-1] + len(line))

def from_offset(offset: int) -> lsp.Position:
line = bisect.bisect_right(offsets, offset) - 1
character = offset - offsets[line]
return lsp.Position(line=line, character=character)

sequences = []
try:
thread = Thread(target=lambda: sequences.extend(_get_diff(old_text, new_text)))
thread.start()
thread.join(timeout or DIFF_TIMEOUT)
except Exception:
pass

if sequences:
edits = [
lsp.TextEdit(
range=lsp.Range(start=from_offset(old_start), end=from_offset(old_end)),
new_text=new_text[new_start:new_end],
)
for opcode, old_start, old_end, new_start, new_end in sequences
if opcode != "equal"
]
return edits

# return single edit with whole document
return [
lsp.TextEdit(
range=lsp.Range(start=from_offset(0), end=from_offset(len(old_text))),
new_text=new_text,
)
]
32 changes: 16 additions & 16 deletions bundled/tool/lsp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ def update_environ_path() -> None:
# Imports needed for the language server goes below this.
# **********************************************************
# pylint: disable=wrong-import-position,import-error
import lsp_edit_utils as edit_utils
import lsp_jsonrpc as jsonrpc
import lsp_utils as utils
import lsprotocol.types as lsp
Expand Down Expand Up @@ -97,14 +98,17 @@ def update_environ_path() -> None:
def formatting(params: lsp.DocumentFormattingParams) -> list[lsp.TextEdit] | None:
"""LSP handler for textDocument/formatting request."""

document = LSP_SERVER.workspace.get_document(params.text_document.uri)
edits = _formatting_helper(document)
if edits:
return edits
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
return _formatting_helper(document)

# NOTE: If you provide [] array, VS Code will clear the file of all contents.
# To indicate no changes to file return None.
return None

@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_RANGE_FORMATTING)
def range_formatting(params: lsp.DocumentFormattingParams) -> list[lsp.TextEdit] | None:
"""LSP handler for textDocument/formatting request."""

log_warning("Black does not support range formatting. Formatting entire document.")
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
return _formatting_helper(document)


def is_python(code: str) -> bool:
Expand Down Expand Up @@ -143,15 +147,11 @@ def _formatting_helper(document: workspace.Document) -> list[lsp.TextEdit] | Non

# If code is already formatted, then no need to send any edits.
if new_source != document.source:
return [
lsp.TextEdit(
range=lsp.Range(
start=lsp.Position(line=0, character=0),
end=lsp.Position(line=len(document.lines), character=0),
),
new_text=new_source,
)
]
edits = edit_utils.get_text_edits(document.source, new_source)
if edits:
# NOTE: If you provide [] array, VS Code will clear the file of all contents.
# To indicate no changes to file return None.
return edits
return None


Expand Down
6 changes: 3 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,9 @@ platformdirs==3.11.0 \
--hash=sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3 \
--hash=sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e
# via black
pygls==1.0.2 \
--hash=sha256:6d278d29fa6559b0f7a448263c85cb64ec6e9369548b02f1a7944060848b21f9 \
--hash=sha256:888ed63d1f650b4fc64d603d73d37545386ec533c0caac921aed80f80ea946a4
pygls==1.1.0 \
--hash=sha256:70acb6fe0df1c8a17b7ce08daa0afdb4aedc6913a6a6696003e1434fda80a06e \
--hash=sha256:eb19b818039d3d705ec8adbcdf5809a93af925f30cd7a3f3b7573479079ba00e
# via -r ./requirements.in
tomli==2.0.1 \
--hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
Expand Down
38 changes: 37 additions & 1 deletion src/test/python_tests/lsp_test_client/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
import pathlib
import platform
import random
import subprocess
import sys
from typing import Any, List

import lsprotocol.converters as cv
import lsprotocol.types as lsp

from .constants import PROJECT_ROOT

Expand All @@ -33,12 +39,21 @@ def python_file(contents: str, root: pathlib.Path):
+ ".py"
)
fullpath = root / basename
fullpath.write_text(contents)
fullpath.write_text(contents, encoding="utf-8")
yield fullpath
finally:
os.unlink(str(fullpath))


@contextlib.contextmanager
def install_packages(packages: List[str]):
try:
subprocess.run([sys.executable, "-m", "pip", "install"] + packages, check=True)
yield
finally:
subprocess.run([sys.executable, "-m", "pip", "uninstall", "-y"] + packages, check=True)


def get_server_info_defaults():
"""Returns server info from package.json"""
package_json_path = PROJECT_ROOT / "package.json"
Expand Down Expand Up @@ -66,3 +81,24 @@ def get_initialization_options():
setting["cwd"] = str(PROJECT_ROOT)

return {"settings": [setting], "globalSettings": setting}


def apply_text_edits(text: str, text_edits: List[lsp.TextEdit]) -> str:
if not text_edits:
return text

offsets = [0]
for line in text.splitlines(keepends=True):
offsets.append(offsets[-1] + len(line))

for text_edit in reversed(text_edits):
start_offset = offsets[text_edit.range.start.line] + text_edit.range.start.character
end_offset = offsets[text_edit.range.end.line] + text_edit.range.end.character
text = text[:start_offset] + text_edit.new_text + text[end_offset:]
return text


def destructure_text_edits(text_edits: List[Any]) -> List[lsp.TextEdit]:
"""Converts text edits from the language server to the format used by the test client."""
converter = cv.get_converter()
return [converter.structure(text_edit, lsp.TextEdit) for text_edit in text_edits]
1 change: 1 addition & 0 deletions src/test/python_tests/requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# 1) python -m pip install pip-tools
# 2) pip-compile --generate-hashes --upgrade ./src/test/python_tests/requirements.in

pygls
pytest
PyHamcrest
python-jsonrpc-server
40 changes: 39 additions & 1 deletion src/test/python_tests/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,38 @@
#
# pip-compile --generate-hashes ./src/test/python_tests/requirements.in
#
attrs==23.1.0 \
--hash=sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04 \
--hash=sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015
# via
# cattrs
# lsprotocol
cattrs==23.1.2 \
--hash=sha256:b2bb14311ac17bed0d58785e5a60f022e5431aca3932e3fc5cc8ed8639de50a4 \
--hash=sha256:db1c821b8c537382b2c7c66678c3790091ca0275ac486c76f3c8f3920e83c657
# via lsprotocol
colorama==0.4.6 \
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
# via pytest
exceptiongroup==1.1.3 \
--hash=sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9 \
--hash=sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3
# via pytest
# via
# cattrs
# pytest
importlib-metadata==6.8.0 \
--hash=sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb \
--hash=sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743
# via typeguard
iniconfig==2.0.0 \
--hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \
--hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374
# via pytest
lsprotocol==2023.0.0b1 \
--hash=sha256:ade2cd0fa0ede7965698cb59cd05d3adbd19178fd73e83f72ef57a032fbb9d62 \
--hash=sha256:f7a2d4655cbd5639f373ddd1789807450c543341fa0a32b064ad30dbb9f510d4
# via pygls
packaging==23.2 \
--hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 \
--hash=sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7
Expand All @@ -24,6 +44,10 @@ pluggy==1.3.0 \
--hash=sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12 \
--hash=sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7
# via pytest
pygls==1.1.0 \
--hash=sha256:70acb6fe0df1c8a17b7ce08daa0afdb4aedc6913a6a6696003e1434fda80a06e \
--hash=sha256:eb19b818039d3d705ec8adbcdf5809a93af925f30cd7a3f3b7573479079ba00e
# via -r ./src/test/python_tests/requirements.in
pyhamcrest==2.0.4 \
--hash=sha256:60a41d4783b9d56c9ec8586635d2301db5072b3ea8a51c32dd03c408ae2b0f79 \
--hash=sha256:b5d9ce6b977696286cf232ce2adf8969b4d0b045975b0936ac9005e84e67e9c1
Expand All @@ -40,6 +64,16 @@ tomli==2.0.1 \
--hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
--hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
# via pytest
typeguard==3.0.2 \
--hash=sha256:bbe993854385284ab42fd5bd3bee6f6556577ce8b50696d6cb956d704f286c8e \
--hash=sha256:fee5297fdb28f8e9efcb8142b5ee219e02375509cd77ea9d270b5af826358d5a
# via pygls
typing-extensions==4.8.0 \
--hash=sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0 \
--hash=sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef
# via
# cattrs
# typeguard
ujson==5.8.0 \
--hash=sha256:07d459aca895eb17eb463b00441986b021b9312c6c8cc1d06880925c7f51009c \
--hash=sha256:0be81bae295f65a6896b0c9030b55a106fb2dec69ef877253a87bc7c9c5308f7 \
Expand Down Expand Up @@ -103,3 +137,7 @@ ujson==5.8.0 \
--hash=sha256:f504117a39cb98abba4153bf0b46b4954cc5d62f6351a14660201500ba31fe7f \
--hash=sha256:fb87decf38cc82bcdea1d7511e73629e651bdec3a43ab40985167ab8449b769c
# via python-jsonrpc-server
zipp==3.17.0 \
--hash=sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31 \
--hash=sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0
# via importlib-metadata
Loading

0 comments on commit 5369db7

Please sign in to comment.