Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions bundled/tool/lsp_edit_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# 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
45 changes: 44 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,26 @@ 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:
try:
subprocess.run(
[sys.executable, "-m", "pip", "uninstall", "-y"] + packages, check=True
)
except Exception:
pass


def get_server_info_defaults():
"""Returns server info from package.json"""
package_json_path = PROJECT_ROOT / "package.json"
Expand Down Expand Up @@ -66,3 +86,26 @@ 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