From 59bce397b18ce953c49b09e8263dbcff012c4614 Mon Sep 17 00:00:00 2001 From: bagel897 Date: Mon, 10 Feb 2025 12:32:09 -0800 Subject: [PATCH 1/6] add lsp --- pyproject.toml | 5 +- src/codegen/extensions/lsp/lsp.py | 45 ++++ src/codegen/extensions/lsp/protocol.py | 14 ++ src/codegen/extensions/lsp/server.py | 29 +++ tests/unit/codegen/{sdk => }/conftest.py | 15 +- tests/unit/codegen/extensions/lsp/conftest.py | 34 +++ .../extensions/lsp/test_completions.py | 19 ++ .../codegen/extensions/lsp/test_rename.py | 42 ++++ uv.lock | 202 ++++++++++++++---- 9 files changed, 359 insertions(+), 46 deletions(-) create mode 100644 src/codegen/extensions/lsp/lsp.py create mode 100644 src/codegen/extensions/lsp/protocol.py create mode 100644 src/codegen/extensions/lsp/server.py rename tests/unit/codegen/{sdk => }/conftest.py (68%) create mode 100644 tests/unit/codegen/extensions/lsp/conftest.py create mode 100644 tests/unit/codegen/extensions/lsp/test_completions.py create mode 100644 tests/unit/codegen/extensions/lsp/test_rename.py diff --git a/pyproject.toml b/pyproject.toml index 2717a487d..8045b0596 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -113,6 +113,7 @@ types = [ "types-requests>=2.32.0.20241016", "types-toml>=0.10.8.20240310", ] +lsp = ["pygls>=2.0.0a2", "lsprotocol==2024.0.0b1"] [tool.uv] cache-keys = [{ git = { commit = true, tags = true } }] dev-dependencies = [ @@ -147,11 +148,12 @@ dev-dependencies = [ "isort>=5.13.2", "emoji>=2.14.0", "pytest-benchmark[histogram]>=5.1.0", - "pytest-asyncio<1.0.0,>=0.21.1", + "pytest-asyncio>=0.21.1,<1.0.0", "loguru>=0.7.3", "httpx<0.28.2,>=0.28.1", "jupyterlab>=4.3.5", "modal>=0.73.25", + "pytest-lsp>=1.0.0b1", ] @@ -210,6 +212,7 @@ xfail_strict = true junit_duration_report = "call" junit_logging = "all" tmp_path_retention_policy = "failed" +asyncio_mode = "auto" [build-system] requires = ["hatchling>=1.26.3", "hatch-vcs>=0.4.0", "setuptools-scm>=8.0.0"] build-backend = "hatchling.build" diff --git a/src/codegen/extensions/lsp/lsp.py b/src/codegen/extensions/lsp/lsp.py new file mode 100644 index 000000000..92e221a45 --- /dev/null +++ b/src/codegen/extensions/lsp/lsp.py @@ -0,0 +1,45 @@ +import logging + +from lsprotocol import types + +import codegen +from codegen.extensions.lsp.protocol import CodegenLanguageServerProtocol +from codegen.extensions.lsp.server import CodegenLanguageServer + +version = getattr(codegen, "__version__", "v0.1") +server = CodegenLanguageServer("codegen", version, protocol_cls=CodegenLanguageServerProtocol) +logger = logging.getLogger(__name__) + + +@server.feature( + types.TEXT_DOCUMENT_RENAME, +) +def rename(server: CodegenLanguageServer, params: types.RenameParams): + symbol = server.get_symbol(params.text_document.uri, params.position) + if symbol is None: + logger.warning(f"No symbol found at {params.text_document.uri}:{params.position}") + return + logger.info(f"Renaming symbol {symbol.name} to {params.new_name}") + symbol.rename(params.new_name) + server.codebase.commit() + + +# @server.feature( +# types.TEXT_DOCUMENT_RENAME, +# ) +# def completions(params: types.CompletionParams): +# document = server.workspace.get_document(params.text_document.uri) +# current_line = document.lines[params.position.line].strip() + +# if not current_line.endswith("hello."): +# return [] + +# return [ +# types.CompletionItem(label="world"), +# types.CompletionItem(label="friend"), +# ] + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + server.start_io() diff --git a/src/codegen/extensions/lsp/protocol.py b/src/codegen/extensions/lsp/protocol.py new file mode 100644 index 000000000..f79938bd2 --- /dev/null +++ b/src/codegen/extensions/lsp/protocol.py @@ -0,0 +1,14 @@ +import os + +from lsprotocol.types import INITIALIZE, InitializeParams, InitializeResult +from pygls.protocol import LanguageServerProtocol, lsp_method + +from codegen.sdk.core.codebase import Codebase + + +class CodegenLanguageServerProtocol(LanguageServerProtocol): + @lsp_method(INITIALIZE) + def lsp_initialize(self, params: InitializeParams) -> InitializeResult: + root = params.root_path or os.getcwd() + self._server.codebase = Codebase(repo_path=root) + return super().lsp_initialize(params) diff --git a/src/codegen/extensions/lsp/server.py b/src/codegen/extensions/lsp/server.py new file mode 100644 index 000000000..8005515e9 --- /dev/null +++ b/src/codegen/extensions/lsp/server.py @@ -0,0 +1,29 @@ +from pathlib import Path +from typing import Any, Optional + +from lsprotocol.types import Position +from pygls.lsp.server import LanguageServer + +from codegen.sdk.codebase.flagging.code_flag import Symbol +from codegen.sdk.core.codebase import Codebase +from codegen.sdk.core.file import File, SourceFile + + +class CodegenLanguageServer(LanguageServer): + codebase: Optional[Codebase] + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + def get_file(self, uri: str) -> SourceFile | File: + path = Path(uri) + return self.codebase.get_file(path.name) + + def get_symbol(self, uri: str, position: Position) -> Symbol | None: + file = self.get_file(uri) + line = position.line + char = position.character + for symbol in file.symbols: + if symbol.start_point.row >= line and symbol.start_point.column >= char: + return symbol + return None diff --git a/tests/unit/codegen/sdk/conftest.py b/tests/unit/codegen/conftest.py similarity index 68% rename from tests/unit/codegen/sdk/conftest.py rename to tests/unit/codegen/conftest.py index 03162cc4a..3877426ae 100644 --- a/tests/unit/codegen/sdk/conftest.py +++ b/tests/unit/codegen/conftest.py @@ -28,14 +28,17 @@ def codebase(tmp_path, original: dict[str, str], programming_language: Programmi @pytest.fixture def assert_expected(expected: dict[str, str], tmp_path): - def assert_expected(codebase: Codebase) -> None: - codebase.commit() + def assert_expected(codebase: Codebase, check_codebase: bool = True) -> None: + if check_codebase: + codebase.commit() for file in expected: assert tmp_path.joinpath(file).exists() assert tmp_path.joinpath(file).read_text() == expected[file] - assert codebase.get_file(file).content.strip() == expected[file].strip() - for file in codebase.files: - if file.file.path.exists(): - assert file.filepath in expected + if check_codebase: + assert codebase.get_file(file).content.strip() == expected[file].strip() + if check_codebase: + for file in codebase.files: + if file.file.path.exists(): + assert file.filepath in expected return assert_expected diff --git a/tests/unit/codegen/extensions/lsp/conftest.py b/tests/unit/codegen/extensions/lsp/conftest.py new file mode 100644 index 000000000..9a6660055 --- /dev/null +++ b/tests/unit/codegen/extensions/lsp/conftest.py @@ -0,0 +1,34 @@ +import sys + +import pytest_lsp +from lsprotocol.types import ( + InitializeParams, +) +from pytest_lsp import ( + ClientServerConfig, + LanguageClient, + client_capabilities, +) + +from codegen.sdk.core.codebase import Codebase + + +@pytest_lsp.fixture( + config=ClientServerConfig( + server_command=[sys.executable, "-m", "codegen.extensions.lsp.lsp"], + ), +) +async def client(lsp_client: LanguageClient, codebase: Codebase): + # Setup + response = await lsp_client.initialize_session( + InitializeParams( + capabilities=client_capabilities("visual-studio-code"), + root_uri="file://" + str(codebase.repo_path), + root_path=str(codebase.repo_path), + ) + ) + + yield + + # Teardown + await lsp_client.shutdown_session() diff --git a/tests/unit/codegen/extensions/lsp/test_completions.py b/tests/unit/codegen/extensions/lsp/test_completions.py new file mode 100644 index 000000000..99ca79af2 --- /dev/null +++ b/tests/unit/codegen/extensions/lsp/test_completions.py @@ -0,0 +1,19 @@ +from lsprotocol.types import ( + CompletionParams, + Position, + TextDocumentIdentifier, +) +from pytest_lsp import ( + LanguageClient, +) + + +async def test_completion(client: LanguageClient): + result = await client.text_document_completion_async( + params=CompletionParams( + position=Position(line=5, character=23), + text_document=TextDocumentIdentifier(uri="file:///path/to/test/project/root/test_file.rst"), + ) + ) + + assert len(result.items) > 0 diff --git a/tests/unit/codegen/extensions/lsp/test_rename.py b/tests/unit/codegen/extensions/lsp/test_rename.py new file mode 100644 index 000000000..7c75ea8b9 --- /dev/null +++ b/tests/unit/codegen/extensions/lsp/test_rename.py @@ -0,0 +1,42 @@ +import pytest +from lsprotocol.types import ( + Position, + RenameParams, + TextDocumentIdentifier, +) +from pytest_lsp import ( + LanguageClient, +) + +from codegen.sdk.core.codebase import Codebase + + +@pytest.mark.parametrize( + "original, expected", + [ + ( + { + "test.py": """ +def hello(): + pass + """.strip(), + }, + { + "test.py": """ +def world(): + pass + """.strip(), + }, + ) + ], +) +async def test_rename(client: LanguageClient, codebase: Codebase, assert_expected): + result = await client.text_document_rename_async( + params=RenameParams( + position=Position(line=0, character=0), + text_document=TextDocumentIdentifier(uri="file://test.py"), + new_name="world", + ) + ) + + assert_expected(codebase, check_codebase=False) diff --git a/uv.lock b/uv.lock index f5220bb2d..053e98587 100644 --- a/uv.lock +++ b/uv.lock @@ -602,6 +602,10 @@ dependencies = [ ] [package.optional-dependencies] +lsp = [ + { name = "lsprotocol" }, + { name = "pygls" }, +] types = [ { name = "types-networkx" }, { name = "types-requests" }, @@ -628,6 +632,7 @@ dev = [ { name = "jsbeautifier" }, { name = "jupyterlab" }, { name = "loguru" }, + { name = "modal" }, { name = "mypy", extra = ["faster-cache", "mypyc"] }, { name = "pre-commit" }, { name = "pre-commit-uv" }, @@ -635,6 +640,7 @@ dev = [ { name = "pytest-asyncio" }, { name = "pytest-benchmark", extra = ["histogram"] }, { name = "pytest-cov" }, + { name = "pytest-lsp" }, { name = "pytest-mock" }, { name = "pytest-timeout" }, { name = "pytest-xdist" }, @@ -666,6 +672,7 @@ requires-dist = [ { name = "langchain-core" }, { name = "langchain-openai" }, { name = "lazy-object-proxy", specifier = ">=0.0.0" }, + { name = "lsprotocol", marker = "extra == 'lsp'", specifier = "==2024.0.0b1" }, { name = "mini-racer", specifier = ">=0.12.4" }, { name = "networkx", specifier = ">=3.4.1" }, { name = "numpy", specifier = ">=2.2.2" }, @@ -677,6 +684,7 @@ requires-dist = [ { name = "pydantic-core", specifier = ">=2.23.4" }, { name = "pygit2", specifier = ">=1.16.0" }, { name = "pygithub", specifier = "==2.5.0" }, + { name = "pygls", marker = "extra == 'lsp'", specifier = ">=2.0.0a2" }, { name = "pyinstrument", specifier = ">=5.0.0" }, { name = "pyjson5", specifier = "==1.6.8" }, { name = "pyright", specifier = ">=1.1.372,<2.0.0" }, @@ -732,6 +740,7 @@ dev = [ { name = "jsbeautifier", specifier = ">=1.15.1,<2.0.0" }, { name = "jupyterlab", specifier = ">=4.3.5" }, { name = "loguru", specifier = ">=0.7.3" }, + { name = "modal", specifier = ">=0.73.25" }, { name = "mypy", extras = ["mypyc", "faster-cache"], specifier = ">=1.13.0" }, { name = "pre-commit", specifier = ">=4.0.1" }, { name = "pre-commit-uv", specifier = ">=4.1.4" }, @@ -739,6 +748,7 @@ dev = [ { name = "pytest-asyncio", specifier = ">=0.21.1,<1.0.0" }, { name = "pytest-benchmark", extras = ["histogram"], specifier = ">=5.1.0" }, { name = "pytest-cov", specifier = ">=6.0.0,<6.0.1" }, + { name = "pytest-lsp", specifier = ">=1.0.0b1" }, { name = "pytest-mock", specifier = ">=3.14.0,<4.0.0" }, { name = "pytest-timeout", specifier = ">=2.3.1" }, { name = "pytest-xdist", specifier = ">=3.6.1,<4.0.0" }, @@ -1294,6 +1304,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ac/38/08cc303ddddc4b3d7c628c3039a61a3aae36c241ed01393d00c2fd663473/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", size = 1142112 }, ] +[[package]] +name = "grpclib" +version = "0.4.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h2" }, + { name = "multidict" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/b9/55936e462a5925190d7427e880b3033601d1effd13809b483d13a926061a/grpclib-0.4.7.tar.gz", hash = "sha256:2988ef57c02b22b7a2e8e961792c41ccf97efc2ace91ae7a5b0de03c363823c3", size = 61254 } + [[package]] name = "h11" version = "0.14.0" @@ -1303,6 +1323,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, ] +[[package]] +name = "h2" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hpack" }, + { name = "hyperframe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/38/d7f80fd13e6582fb8e0df8c9a653dcc02b03ca34f4d72f34869298c5baf8/h2-4.2.0.tar.gz", hash = "sha256:c8a52129695e88b1a0578d8d2cc6842bbd79128ac685463b887ee278126ad01f", size = 2150682 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/9e/984486f2d0a0bd2b024bf4bc1c62688fcafa9e61991f041fb0e2def4a982/h2-4.2.0-py3-none-any.whl", hash = "sha256:479a53ad425bb29af087f3458a61d30780bc818e4ebcf01f0b536ba916462ed0", size = 60957 }, +] + [[package]] name = "hatch-vcs" version = "0.4.0" @@ -1331,6 +1364,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/08/e7/ae38d7a6dfba0533684e0b2136817d667588ae3ec984c1a4e5df5eb88482/hatchling-1.27.0-py3-none-any.whl", hash = "sha256:d3a2f3567c4f926ea39849cdf924c7e99e6686c9c8e288ae1037c8fa2a5d937b", size = 75794 }, ] +[[package]] +name = "hpack" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357 }, +] + [[package]] name = "httpcore" version = "1.0.7" @@ -1408,6 +1450,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/92/75/4bc3e242ad13f2e6c12e0b0401ab2c5e5c6f0d7da37ec69bc808e24e0ccb/humanize-4.11.0-py3-none-any.whl", hash = "sha256:b53caaec8532bcb2fff70c8826f904c35943f8cecaca29d272d9df38092736c0", size = 128055 }, ] +[[package]] +name = "hyperframe" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007 }, +] + [[package]] name = "identify" version = "2.6.7" @@ -1600,15 +1651,15 @@ wheels = [ [[package]] name = "jsbeautifier" -version = "1.15.2" +version = "1.15.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "editorconfig" }, { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8c/fb/309b9b87222957a1314087e8ac5103463444c692b2a082532a463641d4a1/jsbeautifier-1.15.2.tar.gz", hash = "sha256:6aff11af2c6cb9a2ce135f33a5b223cf5ee676ab7ff5da0edac01e23734f5755", size = 75266 } +sdist = { url = "https://files.pythonhosted.org/packages/cf/74/a37cc6fe8ab3f217657d345dfba0612758807536dca5ca5d2f6a81e3623d/jsbeautifier-1.15.3.tar.gz", hash = "sha256:5f1baf3d4ca6a615bb5417ee861b34b77609eeb12875555f8bbfabd9bf2f3457", size = 75261 } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/20/40b00db549c49766c0d499acedf93ba3a17072c44bad3b097e7fd90f8e80/jsbeautifier-1.15.2-py3-none-any.whl", hash = "sha256:d599aed6dcb0d5431190e5ad7335900d5fdc67236082fe6b6d3fb61d568d7417", size = 94708 }, + { url = "https://files.pythonhosted.org/packages/c5/09/cabdf2ed67ba8281ed2025e322c2fc9502a135f756c0a3a3eb60c12eb4fa/jsbeautifier-1.15.3-py3-none-any.whl", hash = "sha256:b207a15ab7529eee4a35ae7790e9ec4e32a2b5026d51e2d0386c3a65e6ecfc91", size = 94706 }, ] [[package]] @@ -1994,15 +2045,15 @@ wheels = [ [[package]] name = "lsprotocol" -version = "2023.0.1" +version = "2024.0.0b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "cattrs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/f6/6e80484ec078d0b50699ceb1833597b792a6c695f90c645fbaf54b947e6f/lsprotocol-2023.0.1.tar.gz", hash = "sha256:cc5c15130d2403c18b734304339e51242d3018a05c4f7d0f198ad6e0cd21861d", size = 69434 } +sdist = { url = "https://files.pythonhosted.org/packages/dc/21/0282716d19591e573d20564ee4df65cb5cd8911bfdff35fcde1de2b54072/lsprotocol-2024.0.0b1.tar.gz", hash = "sha256:d3667fb70894d361aa6c495c5c8a1b2e6a44be65ff84c21a9cbb67ebfb4830fd", size = 75358 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/37/2351e48cb3309673492d3a8c59d407b75fb6630e560eb27ecd4da03adc9a/lsprotocol-2023.0.1-py3-none-any.whl", hash = "sha256:c75223c9e4af2f24272b14c6375787438279369236cd568f596d4951052a60f2", size = 70826 }, + { url = "https://files.pythonhosted.org/packages/4d/1b/526af91cd43eba22ac7d9dbdec729dd9d91c2ad335085a61dd42307a7b35/lsprotocol-2024.0.0b1-py3-none-any.whl", hash = "sha256:93785050ac155ae2be16b1ebfbd74c214feb3d3ef77b10399ce941e5ccef6ebd", size = 76600 }, ] [[package]] @@ -2112,6 +2163,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c6/02/c66bdfdadbb021adb642ca4e8a5ed32ada0b4a3e4b39c5d076d19543452f/mistune-3.1.1-py3-none-any.whl", hash = "sha256:02106ac2aa4f66e769debbfa028509a275069dcffce0dfa578edd7b991ee700a", size = 53696 }, ] +[[package]] +name = "modal" +version = "0.73.30" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "certifi" }, + { name = "click" }, + { name = "fastapi" }, + { name = "grpclib" }, + { name = "protobuf" }, + { name = "rich" }, + { name = "synchronicity" }, + { name = "toml" }, + { name = "typer" }, + { name = "types-certifi" }, + { name = "types-toml" }, + { name = "typing-extensions" }, + { name = "watchfiles" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/3b/4c025db02b17c3fcb9c7f3cff2003b4a955955b3bdc54cb6d1be08109688/modal-0.73.30-py3-none-any.whl", hash = "sha256:5173cc41d8bd42220b249889f0e826659581299bb4f4c928cc71627d7c7c2fb3", size = 533076 }, +] + [[package]] name = "multidict" version = "6.1.0" @@ -2753,15 +2828,15 @@ wheels = [ [[package]] name = "pygls" -version = "1.3.1" +version = "2.0.0a2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cattrs" }, { name = "lsprotocol" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/86/b9/41d173dad9eaa9db9c785a85671fc3d68961f08d67706dc2e79011e10b5c/pygls-1.3.1.tar.gz", hash = "sha256:140edceefa0da0e9b3c533547c892a42a7d2fd9217ae848c330c53d266a55018", size = 45527 } +sdist = { url = "https://files.pythonhosted.org/packages/68/a9/2110bbc90fde62ab7b8f21164caacb5288c06d98486cc569526ec6c0c9ca/pygls-2.0.0a2.tar.gz", hash = "sha256:03e00634ed8d989918268aaa4b4a0c3ab857ea2d4ee94514a52efa5ddd6d5d9f", size = 46279 } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/19/b74a10dd24548e96e8c80226cbacb28b021bc3a168a7d2709fb0d0185348/pygls-1.3.1-py3-none-any.whl", hash = "sha256:6e00f11efc56321bdeb6eac04f6d86131f654c7d49124344a9ebb968da3dd91e", size = 56031 }, + { url = "https://files.pythonhosted.org/packages/f8/47/7d7b3911fbd27153ee38a1a15e3977c72733a41ee8d7f6ce6dca65843fe9/pygls-2.0.0a2-py3-none-any.whl", hash = "sha256:b202369321409343aa6440d73111d9fa0c22e580466ff1c7696b8358bb91f243", size = 58504 }, ] [[package]] @@ -2952,6 +3027,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, ] +[[package]] +name = "pytest-lsp" +version = "1.0.0b2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pygls" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/47/1207bf70218c9cbb6e8a184a1957f699c35d9bf8b43dfa2be5885d35c283/pytest_lsp-1.0.0b2.tar.gz", hash = "sha256:459f62d578d700b63c4ea0b500b5a621461eb2c60d0fd941c3583b0d7930a1ea", size = 26634 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/cc/2f46f5a3db66e50e813cba64da0fed2c517c28b80877585461534c953f22/pytest_lsp-1.0.0b2-py3-none-any.whl", hash = "sha256:d989c69e134ac66e297f0e0eae5edb13470059d7028e50fb06c01674b067fc14", size = 24115 }, +] + [[package]] name = "pytest-mock" version = "3.14.0" @@ -3067,7 +3157,7 @@ wheels = [ [[package]] name = "python-semantic-release" -version = "9.18.1" +version = "9.19.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -3083,9 +3173,9 @@ dependencies = [ { name = "shellingham" }, { name = "tomlkit" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/76/39/9dc86e52c679fdf8cb92b677310de34b1dae25836e8cccb760664c840292/python_semantic_release-9.18.1.tar.gz", hash = "sha256:80be4f1ef9625e9d0fed355abdd1a57da79d4371dc4a3abbe17cba4bde6d769f", size = 299100 } +sdist = { url = "https://files.pythonhosted.org/packages/78/60/ca6f63f302325093137afc1b83bba60c0e717a51977f7ba65bf3dab33949/python_semantic_release-9.19.0.tar.gz", hash = "sha256:6b5a560ce263258c1f2918f6124bb92f8efcf5e8cadbf2b7ced9f0cb5a6e8566", size = 299801 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/11/31c56596e597a78533cbb722dd31d2fa391249b41d87aacbfcab44b08ec7/python_semantic_release-9.18.1-py3-none-any.whl", hash = "sha256:76b6b4b02b77acaab1dfe6942ba13fe17aaa7240219a1954c7fb15e5aee935d0", size = 126415 }, + { url = "https://files.pythonhosted.org/packages/4f/2c/1d4b13166c4629e0001406a9eb90adcaccacff325aab33b37a615da4cf83/python_semantic_release-9.19.0-py3-none-any.whl", hash = "sha256:711edd1650fc59008209ba5058660306e2e365d64f3d03fc51d5de27badf6cfa", size = 127132 }, ] [[package]] @@ -3435,32 +3525,32 @@ wheels = [ [[package]] name = "ruff" -version = "0.9.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/02/74/6c359f6b9ed85b88df6ef31febce18faeb852f6c9855651dfb1184a46845/ruff-0.9.5.tar.gz", hash = "sha256:11aecd7a633932875ab3cb05a484c99970b9d52606ce9ea912b690b02653d56c", size = 3634177 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/17/4b/82b7c9ac874e72b82b19fd7eab57d122e2df44d2478d90825854f9232d02/ruff-0.9.5-py3-none-linux_armv6l.whl", hash = "sha256:d466d2abc05f39018d53f681fa1c0ffe9570e6d73cde1b65d23bb557c846f442", size = 11681264 }, - { url = "https://files.pythonhosted.org/packages/27/5c/f5ae0a9564e04108c132e1139d60491c0abc621397fe79a50b3dc0bd704b/ruff-0.9.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:38840dbcef63948657fa7605ca363194d2fe8c26ce8f9ae12eee7f098c85ac8a", size = 11657554 }, - { url = "https://files.pythonhosted.org/packages/2a/83/c6926fa3ccb97cdb3c438bb56a490b395770c750bf59f9bc1fe57ae88264/ruff-0.9.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d56ba06da53536b575fbd2b56517f6f95774ff7be0f62c80b9e67430391eeb36", size = 11088959 }, - { url = "https://files.pythonhosted.org/packages/af/a7/42d1832b752fe969ffdbfcb1b4cb477cb271bed5835110fb0a16ef31ab81/ruff-0.9.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f7cb2a01da08244c50b20ccfaeb5972e4228c3c3a1989d3ece2bc4b1f996001", size = 11902041 }, - { url = "https://files.pythonhosted.org/packages/53/cf/1fffa09fb518d646f560ccfba59f91b23c731e461d6a4dedd21a393a1ff1/ruff-0.9.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:96d5c76358419bc63a671caac70c18732d4fd0341646ecd01641ddda5c39ca0b", size = 11421069 }, - { url = "https://files.pythonhosted.org/packages/09/27/bb8f1b7304e2a9431f631ae7eadc35550fe0cf620a2a6a0fc4aa3d736f94/ruff-0.9.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:deb8304636ed394211f3a6d46c0e7d9535b016f53adaa8340139859b2359a070", size = 12625095 }, - { url = "https://files.pythonhosted.org/packages/d7/ce/ab00bc9d3df35a5f1b64f5117458160a009f93ae5caf65894ebb63a1842d/ruff-0.9.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:df455000bf59e62b3e8c7ba5ed88a4a2bc64896f900f311dc23ff2dc38156440", size = 13257797 }, - { url = "https://files.pythonhosted.org/packages/88/81/c639a082ae6d8392bc52256058ec60f493c6a4d06d5505bccface3767e61/ruff-0.9.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de92170dfa50c32a2b8206a647949590e752aca8100a0f6b8cefa02ae29dce80", size = 12763793 }, - { url = "https://files.pythonhosted.org/packages/b3/d0/0a3d8f56d1e49af466dc770eeec5c125977ba9479af92e484b5b0251ce9c/ruff-0.9.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d28532d73b1f3f627ba88e1456f50748b37f3a345d2be76e4c653bec6c3e393", size = 14386234 }, - { url = "https://files.pythonhosted.org/packages/04/70/e59c192a3ad476355e7f45fb3a87326f5219cc7c472e6b040c6c6595c8f0/ruff-0.9.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c746d7d1df64f31d90503ece5cc34d7007c06751a7a3bbeee10e5f2463d52d2", size = 12437505 }, - { url = "https://files.pythonhosted.org/packages/55/4e/3abba60a259d79c391713e7a6ccabf7e2c96e5e0a19100bc4204f1a43a51/ruff-0.9.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:11417521d6f2d121fda376f0d2169fb529976c544d653d1d6044f4c5562516ee", size = 11884799 }, - { url = "https://files.pythonhosted.org/packages/a3/db/b0183a01a9f25b4efcae919c18fb41d32f985676c917008620ad692b9d5f/ruff-0.9.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b9d71c3879eb32de700f2f6fac3d46566f644a91d3130119a6378f9312a38e1", size = 11527411 }, - { url = "https://files.pythonhosted.org/packages/0a/e4/3ebfcebca3dff1559a74c6becff76e0b64689cea02b7aab15b8b32ea245d/ruff-0.9.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2e36c61145e70febcb78483903c43444c6b9d40f6d2f800b5552fec6e4a7bb9a", size = 12078868 }, - { url = "https://files.pythonhosted.org/packages/ec/b2/5ab808833e06c0a1b0d046a51c06ec5687b73c78b116e8d77687dc0cd515/ruff-0.9.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2f71d09aeba026c922aa7aa19a08d7bd27c867aedb2f74285a2639644c1c12f5", size = 12524374 }, - { url = "https://files.pythonhosted.org/packages/e0/51/1432afcc3b7aa6586c480142caae5323d59750925c3559688f2a9867343f/ruff-0.9.5-py3-none-win32.whl", hash = "sha256:134f958d52aa6fdec3b294b8ebe2320a950d10c041473c4316d2e7d7c2544723", size = 9853682 }, - { url = "https://files.pythonhosted.org/packages/b7/ad/c7a900591bd152bb47fc4882a27654ea55c7973e6d5d6396298ad3fd6638/ruff-0.9.5-py3-none-win_amd64.whl", hash = "sha256:78cc6067f6d80b6745b67498fb84e87d32c6fc34992b52bffefbdae3442967d6", size = 10865744 }, - { url = "https://files.pythonhosted.org/packages/75/d9/fde7610abd53c0c76b6af72fc679cb377b27c617ba704e25da834e0a0608/ruff-0.9.5-py3-none-win_arm64.whl", hash = "sha256:18a29f1a005bddb229e580795627d297dfa99f16b30c7039e73278cf6b5f9fa9", size = 10064595 }, +version = "0.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/e1/e265aba384343dd8ddd3083f5e33536cd17e1566c41453a5517b5dd443be/ruff-0.9.6.tar.gz", hash = "sha256:81761592f72b620ec8fa1068a6fd00e98a5ebee342a3642efd84454f3031dca9", size = 3639454 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/e3/3d2c022e687e18cf5d93d6bfa2722d46afc64eaa438c7fbbdd603b3597be/ruff-0.9.6-py3-none-linux_armv6l.whl", hash = "sha256:2f218f356dd2d995839f1941322ff021c72a492c470f0b26a34f844c29cdf5ba", size = 11714128 }, + { url = "https://files.pythonhosted.org/packages/e1/22/aff073b70f95c052e5c58153cba735748c9e70107a77d03420d7850710a0/ruff-0.9.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b908ff4df65dad7b251c9968a2e4560836d8f5487c2f0cc238321ed951ea0504", size = 11682539 }, + { url = "https://files.pythonhosted.org/packages/75/a7/f5b7390afd98a7918582a3d256cd3e78ba0a26165a467c1820084587cbf9/ruff-0.9.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b109c0ad2ececf42e75fa99dc4043ff72a357436bb171900714a9ea581ddef83", size = 11132512 }, + { url = "https://files.pythonhosted.org/packages/a6/e3/45de13ef65047fea2e33f7e573d848206e15c715e5cd56095589a7733d04/ruff-0.9.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1de4367cca3dac99bcbd15c161404e849bb0bfd543664db39232648dc00112dc", size = 11929275 }, + { url = "https://files.pythonhosted.org/packages/7d/f2/23d04cd6c43b2e641ab961ade8d0b5edb212ecebd112506188c91f2a6e6c/ruff-0.9.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac3ee4d7c2c92ddfdaedf0bf31b2b176fa7aa8950efc454628d477394d35638b", size = 11466502 }, + { url = "https://files.pythonhosted.org/packages/b5/6f/3a8cf166f2d7f1627dd2201e6cbc4cb81f8b7d58099348f0c1ff7b733792/ruff-0.9.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5dc1edd1775270e6aa2386119aea692039781429f0be1e0949ea5884e011aa8e", size = 12676364 }, + { url = "https://files.pythonhosted.org/packages/f5/c4/db52e2189983c70114ff2b7e3997e48c8318af44fe83e1ce9517570a50c6/ruff-0.9.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4a091729086dffa4bd070aa5dab7e39cc6b9d62eb2bef8f3d91172d30d599666", size = 13335518 }, + { url = "https://files.pythonhosted.org/packages/66/44/545f8a4d136830f08f4d24324e7db957c5374bf3a3f7a6c0bc7be4623a37/ruff-0.9.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1bbc6808bf7b15796cef0815e1dfb796fbd383e7dbd4334709642649625e7c5", size = 12823287 }, + { url = "https://files.pythonhosted.org/packages/c5/26/8208ef9ee7431032c143649a9967c3ae1aae4257d95e6f8519f07309aa66/ruff-0.9.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:589d1d9f25b5754ff230dce914a174a7c951a85a4e9270613a2b74231fdac2f5", size = 14592374 }, + { url = "https://files.pythonhosted.org/packages/31/70/e917781e55ff39c5b5208bda384fd397ffd76605e68544d71a7e40944945/ruff-0.9.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc61dd5131742e21103fbbdcad683a8813be0e3c204472d520d9a5021ca8b217", size = 12500173 }, + { url = "https://files.pythonhosted.org/packages/84/f5/e4ddee07660f5a9622a9c2b639afd8f3104988dc4f6ba0b73ffacffa9a8c/ruff-0.9.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5e2d9126161d0357e5c8f30b0bd6168d2c3872372f14481136d13de9937f79b6", size = 11906555 }, + { url = "https://files.pythonhosted.org/packages/f1/2b/6ff2fe383667075eef8656b9892e73dd9b119b5e3add51298628b87f6429/ruff-0.9.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:68660eab1a8e65babb5229a1f97b46e3120923757a68b5413d8561f8a85d4897", size = 11538958 }, + { url = "https://files.pythonhosted.org/packages/3c/db/98e59e90de45d1eb46649151c10a062d5707b5b7f76f64eb1e29edf6ebb1/ruff-0.9.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c4cae6c4cc7b9b4017c71114115db0445b00a16de3bcde0946273e8392856f08", size = 12117247 }, + { url = "https://files.pythonhosted.org/packages/ec/bc/54e38f6d219013a9204a5a2015c09e7a8c36cedcd50a4b01ac69a550b9d9/ruff-0.9.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19f505b643228b417c1111a2a536424ddde0db4ef9023b9e04a46ed8a1cb4656", size = 12554647 }, + { url = "https://files.pythonhosted.org/packages/a5/7d/7b461ab0e2404293c0627125bb70ac642c2e8d55bf590f6fce85f508f1b2/ruff-0.9.6-py3-none-win32.whl", hash = "sha256:194d8402bceef1b31164909540a597e0d913c0e4952015a5b40e28c146121b5d", size = 9949214 }, + { url = "https://files.pythonhosted.org/packages/ee/30/c3cee10f915ed75a5c29c1e57311282d1a15855551a64795c1b2bbe5cf37/ruff-0.9.6-py3-none-win_amd64.whl", hash = "sha256:03482d5c09d90d4ee3f40d97578423698ad895c87314c4de39ed2af945633caa", size = 10999914 }, + { url = "https://files.pythonhosted.org/packages/e8/a8/d71f44b93e3aa86ae232af1f2126ca7b95c0f515ec135462b3e1f351441c/ruff-0.9.6-py3-none-win_arm64.whl", hash = "sha256:0e2bb706a2be7ddfea4a4af918562fdc1bcb16df255e5fa595bbd800ce322a5a", size = 10177499 }, ] [[package]] name = "ruff-lsp" -version = "0.0.61" +version = "0.0.62" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "lsprotocol" }, @@ -3469,9 +3559,9 @@ dependencies = [ { name = "ruff" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8c/36/d1dae5ae5cc7b9da0d4aebb0f579dc9154f588e76280dbbe51141f1ffdfc/ruff_lsp-0.0.61.tar.gz", hash = "sha256:4a1704dc96dc1353557b5edd0733768f3948cfc92042fd332927648e080754bc", size = 41225 } +sdist = { url = "https://files.pythonhosted.org/packages/18/11/8e445dc55753efd45e09882ad0468f4a5650f33aecdbd15c7a52e8e0c3c4/ruff_lsp-0.0.62.tar.gz", hash = "sha256:6db2a39375973ecb16c64d3c8dc37e23e1e191dcb7aebcf525b1f85ebd338c0d", size = 41182 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/89/ef30cbf45090b8524a1f8a2d37349a26c412df7061946d9ce9147dd6e72d/ruff_lsp-0.0.61-py3-none-any.whl", hash = "sha256:c84988a2016066e5f808ba43b7b84e961cfd0339321fd986f35caf6f2c95334a", size = 21009 }, + { url = "https://files.pythonhosted.org/packages/31/5e/d3a6fdf61f6373e53bfb45d6819a72dfef741bc8a9ff31c64496688e7c39/ruff_lsp-0.0.62-py3-none-any.whl", hash = "sha256:fb6c04a0cb09bb3ae316121b084ff09497edd01df58b36fa431f14515c63029e", size = 20980 }, ] [[package]] @@ -3548,6 +3638,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, ] +[[package]] +name = "sigtools" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/db/669ca14166814da187b3087b908ca924cf83f5b504fe23b3859a3ef67d4f/sigtools-4.0.1.tar.gz", hash = "sha256:4b8e135a9cd4d2ea00da670c093372d74e672ba3abb87f4c98d8e73dea54445c", size = 71910 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/91/853dbf6ec096197dba9cd5fd0c836c5fc19142038b7db60ebe6332b1bab1/sigtools-4.0.1-py2.py3-none-any.whl", hash = "sha256:d216b4cf920bbab0fce636ddc429ed8463a5b533d9e1492acb45a2a1bc36ac6c", size = 76419 }, +] + [[package]] name = "six" version = "1.17.0" @@ -3662,6 +3764,19 @@ pytest = [ { name = "pytest" }, ] +[[package]] +name = "synchronicity" +version = "0.9.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sigtools" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/52/f34a9ab6d514e0808d0f572affb360411d596b3439107318c00889277dd6/synchronicity-0.9.11.tar.gz", hash = "sha256:cb5dbbcb43d637e516ae50db05a776da51a705d1e1a9c0e301f6049afc3c2cae", size = 50323 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/d5/7675cd9b8e18f05b9ea261acad5d197fcb8027d2a65b1a750427ec084593/synchronicity-0.9.11-py3-none-any.whl", hash = "sha256:231129654d2f56b1aa148e85ebd8545231be135771f6d2196d414175b1594ef6", size = 36827 }, +] + [[package]] name = "tabulate" version = "0.9.0" @@ -3921,6 +4036,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/cc/0a838ba5ca64dc832aa43f727bd586309846b0ffb2ce52422543e6075e8a/typer-0.15.1-py3-none-any.whl", hash = "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847", size = 44908 }, ] +[[package]] +name = "types-certifi" +version = "2021.10.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/68/943c3aeaf14624712a0357c4a67814dba5cea36d194f5c764dad7959a00c/types-certifi-2021.10.8.3.tar.gz", hash = "sha256:72cf7798d165bc0b76e1c10dd1ea3097c7063c42c21d664523b928e88b554a4f", size = 2095 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/63/2463d89481e811f007b0e1cd0a91e52e141b47f9de724d20db7b861dcfec/types_certifi-2021.10.8.3-py3-none-any.whl", hash = "sha256:b2d1e325e69f71f7c78e5943d410e650b4707bb0ef32e4ddf3da37f54176e88a", size = 2136 }, +] + [[package]] name = "types-networkx" version = "3.4.2.20241227" @@ -4101,16 +4225,16 @@ wheels = [ [[package]] name = "virtualenv" -version = "20.29.1" +version = "20.29.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a7/ca/f23dcb02e161a9bba141b1c08aa50e8da6ea25e6d780528f1d385a3efe25/virtualenv-20.29.1.tar.gz", hash = "sha256:b8b8970138d32fb606192cb97f6cd4bb644fa486be9308fb9b63f81091b5dc35", size = 7658028 } +sdist = { url = "https://files.pythonhosted.org/packages/f1/88/dacc875dd54a8acadb4bcbfd4e3e86df8be75527116c91d8f9784f5e9cab/virtualenv-20.29.2.tar.gz", hash = "sha256:fdaabebf6d03b5ba83ae0a02cfe96f48a716f4fae556461d180825866f75b728", size = 4320272 } wheels = [ - { url = "https://files.pythonhosted.org/packages/89/9b/599bcfc7064fbe5740919e78c5df18e5dceb0887e676256a1061bb5ae232/virtualenv-20.29.1-py3-none-any.whl", hash = "sha256:4e4cb403c0b0da39e13b46b1b2476e505cb0046b25f242bee80f62bf990b2779", size = 4282379 }, + { url = "https://files.pythonhosted.org/packages/93/fa/849483d56773ae29740ae70043ad88e068f98a6401aa819b5d6bee604683/virtualenv-20.29.2-py3-none-any.whl", hash = "sha256:febddfc3d1ea571bdb1dc0f98d7b45d24def7428214d4fb73cc486c9568cce6a", size = 4301478 }, ] [[package]] From ea759db9734fa30ad096168bc6eb5b4d2383ece2 Mon Sep 17 00:00:00 2001 From: bagel897 Date: Mon, 10 Feb 2025 13:23:03 -0800 Subject: [PATCH 2/6] document symbols support --- pyproject.toml | 1 + src/codegen/extensions/lsp/document_symbol.py | 26 ++ src/codegen/extensions/lsp/kind.py | 29 +++ src/codegen/extensions/lsp/lsp.py | 44 +++- src/codegen/extensions/lsp/range.py | 17 ++ .../extensions/lsp/test_completions.py | 27 +- .../codegen/extensions/lsp/test_definition.py | 54 ++++ .../extensions/lsp/test_document_symbols.py | 239 ++++++++++++++++++ .../unit/codegen/extensions/lsp/test_hover.py | 69 +++++ 9 files changed, 492 insertions(+), 14 deletions(-) create mode 100644 src/codegen/extensions/lsp/document_symbol.py create mode 100644 src/codegen/extensions/lsp/kind.py create mode 100644 src/codegen/extensions/lsp/range.py create mode 100644 tests/unit/codegen/extensions/lsp/test_definition.py create mode 100644 tests/unit/codegen/extensions/lsp/test_document_symbols.py create mode 100644 tests/unit/codegen/extensions/lsp/test_hover.py diff --git a/pyproject.toml b/pyproject.toml index 8045b0596..77c410c0a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -213,6 +213,7 @@ junit_duration_report = "call" junit_logging = "all" tmp_path_retention_policy = "failed" asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" [build-system] requires = ["hatchling>=1.26.3", "hatch-vcs>=0.4.0", "setuptools-scm>=8.0.0"] build-backend = "hatchling.build" diff --git a/src/codegen/extensions/lsp/document_symbol.py b/src/codegen/extensions/lsp/document_symbol.py new file mode 100644 index 000000000..01000755a --- /dev/null +++ b/src/codegen/extensions/lsp/document_symbol.py @@ -0,0 +1,26 @@ +from lsprotocol.types import DocumentSymbol + +from codegen.extensions.lsp.kind import get_kind +from codegen.extensions.lsp.range import get_range +from codegen.sdk.core.class_definition import Class +from codegen.sdk.core.interfaces.editable import Editable +from codegen.sdk.extensions.sort import sort_editables + + +def get_document_symbol(node: Editable) -> DocumentSymbol: + children = [] + nodes = [] + if isinstance(node, Class): + nodes.extend(node.methods) + nodes.extend(node.attributes) + nodes.extend(node.nested_classes) + nodes = sort_editables(nodes) + for child in nodes: + children.append(get_document_symbol(child)) + return DocumentSymbol( + name=node.name, + kind=get_kind(node), + range=get_range(node), + selection_range=get_range(node.get_name()), + children=children, + ) diff --git a/src/codegen/extensions/lsp/kind.py b/src/codegen/extensions/lsp/kind.py new file mode 100644 index 000000000..cff49e847 --- /dev/null +++ b/src/codegen/extensions/lsp/kind.py @@ -0,0 +1,29 @@ +from lsprotocol.types import SymbolKind + +from codegen.sdk.core.assignment import Assignment +from codegen.sdk.core.class_definition import Class +from codegen.sdk.core.file import File +from codegen.sdk.core.function import Function +from codegen.sdk.core.interface import Interface +from codegen.sdk.core.interfaces.editable import Editable +from codegen.sdk.typescript.namespace import TSNamespace + +kinds = { + File: SymbolKind.File, + Class: SymbolKind.Class, + Function: SymbolKind.Function, + Assignment: SymbolKind.Variable, + Interface: SymbolKind.Interface, + TSNamespace: SymbolKind.Namespace, +} + + +def get_kind(node: Editable) -> SymbolKind: + if isinstance(node, Function): + if node.is_method: + return SymbolKind.Method + for kind in kinds: + if isinstance(node, kind): + return kinds[kind] + msg = f"No kind found for {node}" + raise ValueError(msg) diff --git a/src/codegen/extensions/lsp/lsp.py b/src/codegen/extensions/lsp/lsp.py index 92e221a45..68caf2968 100644 --- a/src/codegen/extensions/lsp/lsp.py +++ b/src/codegen/extensions/lsp/lsp.py @@ -3,6 +3,7 @@ from lsprotocol import types import codegen +from codegen.extensions.lsp.document_symbol import get_document_symbol from codegen.extensions.lsp.protocol import CodegenLanguageServerProtocol from codegen.extensions.lsp.server import CodegenLanguageServer @@ -24,20 +25,39 @@ def rename(server: CodegenLanguageServer, params: types.RenameParams): server.codebase.commit() -# @server.feature( -# types.TEXT_DOCUMENT_RENAME, -# ) -# def completions(params: types.CompletionParams): -# document = server.workspace.get_document(params.text_document.uri) -# current_line = document.lines[params.position.line].strip() +@server.feature( + types.TEXT_DOCUMENT_DOCUMENT_SYMBOL, +) +def document_symbol(server: CodegenLanguageServer, params: types.DocumentSymbolParams) -> types.DocumentSymbolResult: + file = server.get_file(params.text_document.uri) + symbols = [] + for symbol in file.symbols: + symbols.append(get_document_symbol(symbol)) + return symbols + + +get_document_symbol + + +@server.feature( + types.TEXT_DOCUMENT_HOVER, +) +def hover(server: CodegenLanguageServer, params: types.HoverParams) -> types.HoverResponse: + pass + -# if not current_line.endswith("hello."): -# return [] +@server.feature( + types.TEXT_DOCUMENT_COMPLETION, +) +def completion(server: CodegenLanguageServer, params: types.CompletionParams): + pass -# return [ -# types.CompletionItem(label="world"), -# types.CompletionItem(label="friend"), -# ] + +@server.feature( + types.TEXT_DOCUMENT_DEFINITION, +) +def definition(server: CodegenLanguageServer, params: types.DefinitionParams): + pass if __name__ == "__main__": diff --git a/src/codegen/extensions/lsp/range.py b/src/codegen/extensions/lsp/range.py new file mode 100644 index 000000000..d7ac49d13 --- /dev/null +++ b/src/codegen/extensions/lsp/range.py @@ -0,0 +1,17 @@ +from lsprotocol.types import Position, Range + +from codegen.sdk.core.interfaces.editable import Editable + + +def get_range(node: Editable) -> Range: + start_point = node.start_point + end_point = node.end_point + for extended_node in node.extended_nodes: + if extended_node.start_point.row < start_point.row: + start_point = extended_node.start_point + if extended_node.end_point.row > end_point.row: + end_point = extended_node.end_point + return Range( + start=Position(line=start_point.row, character=start_point.column), + end=Position(line=end_point.row, character=end_point.column), + ) diff --git a/tests/unit/codegen/extensions/lsp/test_completions.py b/tests/unit/codegen/extensions/lsp/test_completions.py index 99ca79af2..1c56c8de8 100644 --- a/tests/unit/codegen/extensions/lsp/test_completions.py +++ b/tests/unit/codegen/extensions/lsp/test_completions.py @@ -1,3 +1,4 @@ +import pytest from lsprotocol.types import ( CompletionParams, Position, @@ -7,13 +8,35 @@ LanguageClient, ) +from codegen.sdk.core.codebase import Codebase -async def test_completion(client: LanguageClient): + +@pytest.mark.parametrize( + "original, expected", + [ + ( + { + "test.py": """ +def hello(): + pass + """.strip(), + }, + { + "test.py": """ +def hello(): + pass + """.strip(), + }, + ) + ], +) +async def test_completion(client: LanguageClient, codebase: Codebase, assert_expected): result = await client.text_document_completion_async( params=CompletionParams( position=Position(line=5, character=23), - text_document=TextDocumentIdentifier(uri="file:///path/to/test/project/root/test_file.rst"), + text_document=TextDocumentIdentifier(uri="file://test.py"), ) ) assert len(result.items) > 0 + assert_expected(codebase, check_codebase=False) diff --git a/tests/unit/codegen/extensions/lsp/test_definition.py b/tests/unit/codegen/extensions/lsp/test_definition.py new file mode 100644 index 000000000..c52cce17f --- /dev/null +++ b/tests/unit/codegen/extensions/lsp/test_definition.py @@ -0,0 +1,54 @@ +import pytest +from lsprotocol.types import ( + DefinitionParams, + Location, + Position, + Range, + TextDocumentIdentifier, +) +from pytest_lsp import LanguageClient + +from codegen.sdk.core.codebase import Codebase + + +@pytest.mark.parametrize( + "original, position, expected_location", + [ + ( + { + "test.py": """ +def example_function(): + pass + +def main(): + example_function() + """.strip(), + }, + Position(line=4, character=4), # Position of example_function call + Location( + uri="file://test.py", + range=Range( + start=Position(line=0, character=4), + end=Position(line=0, character=19), + ), + ), + ) + ], +) +async def test_go_to_definition( + client: LanguageClient, + codebase: Codebase, + original: dict, + position: Position, + expected_location: Location, +): + result = await client.text_document_definition_async( + params=DefinitionParams( + text_document=TextDocumentIdentifier(uri="file://test.py"), + position=position, + ) + ) + + assert isinstance(result, Location) + assert result.uri == expected_location.uri + assert result.range == expected_location.range diff --git a/tests/unit/codegen/extensions/lsp/test_document_symbols.py b/tests/unit/codegen/extensions/lsp/test_document_symbols.py new file mode 100644 index 000000000..4340c2594 --- /dev/null +++ b/tests/unit/codegen/extensions/lsp/test_document_symbols.py @@ -0,0 +1,239 @@ +from collections.abc import Sequence +from typing import cast + +import pytest +from lsprotocol.types import ( + DocumentSymbol, + DocumentSymbolParams, + Position, + Range, + SymbolKind, + TextDocumentIdentifier, +) +from pytest_lsp import LanguageClient + +from codegen.sdk.core.codebase import Codebase + + +@pytest.mark.parametrize( + "original, expected_symbols", + [ + ( + { + "test.py": """ +class TestClass: + def test_method(self): + pass + +def top_level_function(): + pass + """.strip(), + }, + [ + DocumentSymbol( + name="TestClass", + kind=SymbolKind.Class, + range=Range( + start=Position(line=0, character=0), + end=Position(line=2, character=12), + ), + selection_range=Range( + start=Position(line=0, character=6), + end=Position(line=0, character=15), + ), + children=[ + DocumentSymbol( + name="test_method", + kind=SymbolKind.Method, + range=Range( + start=Position(line=1, character=4), + end=Position(line=2, character=12), + ), + selection_range=Range( + start=Position(line=1, character=8), + end=Position(line=1, character=19), + ), + children=[], + ) + ], + ), + DocumentSymbol( + name="top_level_function", + kind=SymbolKind.Function, + range=Range( + start=Position(line=4, character=0), + end=Position(line=5, character=8), + ), + selection_range=Range( + start=Position(line=4, character=4), + end=Position(line=4, character=22), + ), + children=[], + ), + ], + ), + ( + { + "test.py": """ +@decorator +class OuterClass: + class InnerClass: + @property + def inner_method(self): + pass + + async def outer_method(self): + pass + +@decorator +async def async_function(): + pass + """.strip(), + }, + [ + DocumentSymbol( + name="OuterClass", + kind=SymbolKind.Class, + range=Range( + start=Position(line=0, character=0), + end=Position(line=8, character=12), + ), + selection_range=Range( + start=Position(line=1, character=6), + end=Position(line=1, character=16), + ), + children=[ + DocumentSymbol( + name="InnerClass", + kind=SymbolKind.Class, + range=Range( + start=Position(line=2, character=4), + end=Position(line=5, character=16), + ), + selection_range=Range( + start=Position(line=2, character=10), + end=Position(line=2, character=20), + ), + children=[ + DocumentSymbol( + name="inner_method", + kind=SymbolKind.Method, + range=Range( + start=Position(line=3, character=8), + end=Position(line=5, character=16), + ), + selection_range=Range( + start=Position(line=4, character=12), + end=Position(line=4, character=24), + ), + children=[], + ) + ], + ), + DocumentSymbol( + name="outer_method", + kind=SymbolKind.Method, + range=Range( + start=Position(line=7, character=4), + end=Position(line=8, character=12), + ), + selection_range=Range( + start=Position(line=7, character=14), + end=Position(line=7, character=26), + ), + children=[], + ), + ], + ), + DocumentSymbol( + name="async_function", + kind=SymbolKind.Function, + range=Range( + start=Position(line=10, character=0), + end=Position(line=12, character=8), + ), + selection_range=Range( + start=Position(line=11, character=10), + end=Position(line=11, character=24), + ), + children=[], + ), + ], + ), + ( + { + "test.py": """ +def function_with_args(arg1: str, arg2: int = 42): + pass + +class ClassWithDocstring: + \"\"\"This is a docstring.\"\"\" + def method_with_docstring(self): + \"\"\"Method docstring.\"\"\" + pass + """.strip(), + }, + [ + DocumentSymbol( + name="function_with_args", + kind=SymbolKind.Function, + range=Range( + start=Position(line=0, character=0), + end=Position(line=1, character=8), + ), + selection_range=Range( + start=Position(line=0, character=4), + end=Position(line=0, character=22), + ), + children=[], + ), + DocumentSymbol( + name="ClassWithDocstring", + kind=SymbolKind.Class, + range=Range( + start=Position(line=3, character=0), + end=Position(line=7, character=12), + ), + selection_range=Range( + start=Position(line=3, character=6), + end=Position(line=3, character=24), + ), + children=[ + DocumentSymbol( + name="method_with_docstring", + kind=SymbolKind.Method, + range=Range( + start=Position(line=5, character=4), + end=Position(line=7, character=12), + ), + selection_range=Range( + start=Position(line=5, character=8), + end=Position(line=5, character=29), + ), + children=[], + ), + ], + ), + ], + ), + ], +) +async def test_document_symbols( + client: LanguageClient, + codebase: Codebase, + original: dict, + expected_symbols: list[DocumentSymbol], +): + result = await client.text_document_document_symbol_async(params=DocumentSymbolParams(text_document=TextDocumentIdentifier(uri="file://test.py"))) + + assert result is not None + symbols = cast(Sequence[DocumentSymbol], result) + assert len(symbols) == len(expected_symbols) + for actual, expected in zip(symbols, expected_symbols): + assert actual.name == expected.name + assert actual.kind == expected.kind + assert actual.range == expected.range + assert actual.selection_range == expected.selection_range + assert actual.children == expected.children + assert actual == expected + assert symbols == expected_symbols diff --git a/tests/unit/codegen/extensions/lsp/test_hover.py b/tests/unit/codegen/extensions/lsp/test_hover.py new file mode 100644 index 000000000..42e824b61 --- /dev/null +++ b/tests/unit/codegen/extensions/lsp/test_hover.py @@ -0,0 +1,69 @@ +from typing import Any, Union + +import pytest +from lsprotocol.types import ( + Hover, + HoverParams, + MarkupContent, + Position, + TextDocumentIdentifier, +) +from pytest_lsp import LanguageClient + +from codegen.sdk.core.codebase import Codebase + + +@pytest.mark.parametrize( + "original, position, expected_content", + [ + ( + { + "test.py": """ +def example_function(param: str) -> str: + \"\"\"Example function that returns a greeting. + + Args: + param: Name to greet + + Returns: + A greeting message + \"\"\" + return f"Hello, {param}!" + +example_function("world") + """.strip(), + }, + Position(line=11, character=0), # Position of example_function call + "```python\ndef example_function(param: str) -> str\n```\n\nExample function that returns a greeting.\n\nArgs:\n param: Name to greet\n\nReturns:\n A greeting message", + ) + ], +) +async def test_hover( + client: LanguageClient, + codebase: Codebase, + original: dict, + position: Position, + expected_content: str, +): + result = await client.text_document_hover_async( + params=HoverParams( + text_document=TextDocumentIdentifier(uri="file://test.py"), + position=position, + ) + ) + + assert isinstance(result, Hover) + + def extract_content(hover_content: Union[MarkupContent, str, list[dict[str, Any]], dict[str, Any]]) -> str: + if isinstance(hover_content, MarkupContent): + return hover_content.value + elif isinstance(hover_content, str): + return hover_content + elif isinstance(hover_content, dict): + return hover_content.get("value", "") + elif isinstance(hover_content, list): + return "\n".join(item.get("value", str(item)) if isinstance(item, dict) else str(item) for item in hover_content) + return "" + + actual_content = extract_content(result.contents) + assert actual_content == expected_content From 390b928852a872e1aeb8f74fddc1016ddf9a46aa Mon Sep 17 00:00:00 2001 From: bagel897 Date: Mon, 10 Feb 2025 14:02:40 -0800 Subject: [PATCH 3/6] go to definition support --- src/codegen/extensions/lsp/lsp.py | 32 +++++- src/codegen/extensions/lsp/protocol.py | 4 +- src/codegen/extensions/lsp/range.py | 15 +++ src/codegen/extensions/lsp/server.py | 29 +++++- tests/unit/codegen/extensions/lsp/conftest.py | 4 +- .../codegen/extensions/lsp/test_definition.py | 98 ++++++++++++++++++- 6 files changed, 173 insertions(+), 9 deletions(-) diff --git a/src/codegen/extensions/lsp/lsp.py b/src/codegen/extensions/lsp/lsp.py index 68caf2968..9461a0515 100644 --- a/src/codegen/extensions/lsp/lsp.py +++ b/src/codegen/extensions/lsp/lsp.py @@ -5,7 +5,14 @@ import codegen from codegen.extensions.lsp.document_symbol import get_document_symbol from codegen.extensions.lsp.protocol import CodegenLanguageServerProtocol +from codegen.extensions.lsp.range import get_range from codegen.extensions.lsp.server import CodegenLanguageServer +from codegen.sdk.core.assignment import Assignment +from codegen.sdk.core.detached_symbols.function_call import FunctionCall +from codegen.sdk.core.expressions.chained_attribute import ChainedAttribute +from codegen.sdk.core.expressions.expression import Expression +from codegen.sdk.core.expressions.name import Name +from codegen.sdk.core.interfaces.has_name import HasName version = getattr(codegen, "__version__", "v0.1") server = CodegenLanguageServer("codegen", version, protocol_cls=CodegenLanguageServerProtocol) @@ -57,7 +64,30 @@ def completion(server: CodegenLanguageServer, params: types.CompletionParams): types.TEXT_DOCUMENT_DEFINITION, ) def definition(server: CodegenLanguageServer, params: types.DefinitionParams): - pass + node = server.get_node_under_cursor(params.text_document.uri, params.position) + if node is None or not isinstance(node, (Expression)): + logger.warning(f"No node found at {params.text_document.uri}:{params.position}") + return None + if isinstance(node, Name) and isinstance(node.parent, ChainedAttribute) and node.parent.attribute == node: + node = node.parent + if isinstance(node.parent, FunctionCall) and node.parent.get_name() == node: + node = node.parent + logger.info(f"Resolving definition for {node}") + if isinstance(node, FunctionCall): + resolved = node.function_definition + else: + resolved = node.resolved_value + if resolved is None: + logger.warning(f"No resolved value found for {node.name} at {params.text_document.uri}:{params.position}") + return None + if isinstance(resolved, (HasName,)): + resolved = resolved.get_name() + if isinstance(resolved.parent, Assignment) and resolved.parent.value == resolved: + resolved = resolved.parent.get_name() + return types.Location( + uri=resolved.file.path.as_uri(), + range=get_range(resolved), + ) if __name__ == "__main__": diff --git a/src/codegen/extensions/lsp/protocol.py b/src/codegen/extensions/lsp/protocol.py index f79938bd2..02a7fe4d5 100644 --- a/src/codegen/extensions/lsp/protocol.py +++ b/src/codegen/extensions/lsp/protocol.py @@ -3,6 +3,7 @@ from lsprotocol.types import INITIALIZE, InitializeParams, InitializeResult from pygls.protocol import LanguageServerProtocol, lsp_method +from codegen.sdk.codebase.config import CodebaseConfig, GSFeatureFlags from codegen.sdk.core.codebase import Codebase @@ -10,5 +11,6 @@ class CodegenLanguageServerProtocol(LanguageServerProtocol): @lsp_method(INITIALIZE) def lsp_initialize(self, params: InitializeParams) -> InitializeResult: root = params.root_path or os.getcwd() - self._server.codebase = Codebase(repo_path=root) + config = CodebaseConfig(feature_flags=GSFeatureFlags(full_range_index=True)) + self._server.codebase = Codebase(repo_path=root, config=config) return super().lsp_initialize(params) diff --git a/src/codegen/extensions/lsp/range.py b/src/codegen/extensions/lsp/range.py index d7ac49d13..9762e9d00 100644 --- a/src/codegen/extensions/lsp/range.py +++ b/src/codegen/extensions/lsp/range.py @@ -1,4 +1,6 @@ +import tree_sitter from lsprotocol.types import Position, Range +from pygls.workspace import TextDocument from codegen.sdk.core.interfaces.editable import Editable @@ -15,3 +17,16 @@ def get_range(node: Editable) -> Range: start=Position(line=start_point.row, character=start_point.column), end=Position(line=end_point.row, character=end_point.column), ) + + +def get_tree_sitter_range(range: Range, document: TextDocument) -> tree_sitter.Range: + start_pos = tree_sitter.Point(row=range.start.line, column=range.start.character) + end_pos = tree_sitter.Point(row=range.end.line, column=range.end.character) + start_byte = document.offset_at_position(range.start) + end_byte = document.offset_at_position(range.end) + return tree_sitter.Range( + start_point=start_pos, + end_point=end_pos, + start_byte=start_byte, + end_byte=end_byte, + ) diff --git a/src/codegen/extensions/lsp/server.py b/src/codegen/extensions/lsp/server.py index 8005515e9..04be5d049 100644 --- a/src/codegen/extensions/lsp/server.py +++ b/src/codegen/extensions/lsp/server.py @@ -1,12 +1,17 @@ +import logging from pathlib import Path from typing import Any, Optional -from lsprotocol.types import Position +from lsprotocol.types import Position, Range from pygls.lsp.server import LanguageServer +from codegen.extensions.lsp.range import get_tree_sitter_range from codegen.sdk.codebase.flagging.code_flag import Symbol from codegen.sdk.core.codebase import Codebase from codegen.sdk.core.file import File, SourceFile +from codegen.sdk.core.interfaces.editable import Editable + +logger = logging.getLogger(__name__) class CodegenLanguageServer(LanguageServer): @@ -27,3 +32,25 @@ def get_symbol(self, uri: str, position: Position) -> Symbol | None: if symbol.start_point.row >= line and symbol.start_point.column >= char: return symbol return None + + def get_node_under_cursor(self, uri: str, position: Position) -> Editable | None: + file = self.get_file(uri) + resolved_uri = file.path.absolute().as_uri() + logger.info(f"Getting node under cursor for {resolved_uri} at {position}") + document = self.workspace.get_text_document(resolved_uri) + candidates = [] + target_byte = document.offset_at_position(position) + for node in file._range_index.nodes: + if node.start_byte <= target_byte and node.end_byte >= target_byte: + candidates.append(node) + if not candidates: + return None + return min(candidates, key=lambda node: abs(node.end_byte - node.start_byte)) + + def get_node_for_range(self, uri: str, range: Range) -> Editable | None: + file = self.get_file(uri) + document = self.workspace.get_text_document(uri) + ts_range = get_tree_sitter_range(range, document) + for node in file._range_index.get_all_for_range(ts_range): + return node + return None diff --git a/tests/unit/codegen/extensions/lsp/conftest.py b/tests/unit/codegen/extensions/lsp/conftest.py index 9a6660055..099329327 100644 --- a/tests/unit/codegen/extensions/lsp/conftest.py +++ b/tests/unit/codegen/extensions/lsp/conftest.py @@ -23,8 +23,8 @@ async def client(lsp_client: LanguageClient, codebase: Codebase): response = await lsp_client.initialize_session( InitializeParams( capabilities=client_capabilities("visual-studio-code"), - root_uri="file://" + str(codebase.repo_path), - root_path=str(codebase.repo_path), + root_uri="file://" + str(codebase.repo_path.resolve()), + root_path=str(codebase.repo_path.resolve()), ) ) diff --git a/tests/unit/codegen/extensions/lsp/test_definition.py b/tests/unit/codegen/extensions/lsp/test_definition.py index c52cce17f..955e9041d 100644 --- a/tests/unit/codegen/extensions/lsp/test_definition.py +++ b/tests/unit/codegen/extensions/lsp/test_definition.py @@ -26,13 +26,103 @@ def main(): }, Position(line=4, character=4), # Position of example_function call Location( - uri="file://test.py", + uri="file://{workspaceFolder}/test.py", range=Range( start=Position(line=0, character=4), - end=Position(line=0, character=19), + end=Position(line=0, character=20), ), ), - ) + ), + ( + { + "test.py": """ +class MyClass: + def method(self): + pass + +obj = MyClass() +obj.method() + """.strip(), + }, + Position(line=5, character=4), # Position of method call + Location( + uri="file://{workspaceFolder}/test.py", + range=Range( + start=Position(line=1, character=8), + end=Position(line=1, character=14), + ), + ), + ), + ( + { + "module/utils.py": """ +def utility_function(): + pass + """.strip(), + "test.py": """ +from module.utils import utility_function + +def main(): + utility_function() + """.strip(), + }, + Position(line=3, character=4), # Position of utility_function call in test.py + Location( + uri="file://{workspaceFolder}/module/utils.py", + range=Range( + start=Position(line=0, character=4), + end=Position(line=0, character=20), # Adjusted to end before () + ), + ), + ), + ( + { + "models.py": """ +class DatabaseModel: + def save(self): + pass + """.strip(), + "test.py": """ +from models import DatabaseModel + +def main(): + model = DatabaseModel() + model.save() + """.strip(), + }, + Position(line=4, character=10), # Position of save() call in test.py + Location( + uri="file://{workspaceFolder}/models.py", + range=Range( + start=Position(line=1, character=8), + end=Position(line=1, character=12), # Adjusted to end before () + ), + ), + ), + ( + { + "module/__init__.py": """ +from .constants import DEFAULT_TIMEOUT + """.strip(), + "module/constants.py": """ +DEFAULT_TIMEOUT = 30 + """.strip(), + "test.py": """ +from module import DEFAULT_TIMEOUT + +def main(): + timeout = DEFAULT_TIMEOUT + """.strip(), + }, + Position(line=3, character=14), # Position of DEFAULT_TIMEOUT reference in test.py + Location( + uri="file://{workspaceFolder}/module/constants.py", + range=Range( + start=Position(line=0, character=0), + end=Position(line=0, character=15), # Adjusted to end before = + ), + ), + ), ], ) async def test_go_to_definition( @@ -50,5 +140,5 @@ async def test_go_to_definition( ) assert isinstance(result, Location) - assert result.uri == expected_location.uri + assert result.uri == expected_location.uri.format(workspaceFolder=str(codebase.repo_path)) assert result.range == expected_location.range From ed11c9f75deeaa0bcb34203fa6f5dbe9cbb0aedf Mon Sep 17 00:00:00 2001 From: bagel897 Date: Mon, 10 Feb 2025 17:43:59 -0800 Subject: [PATCH 4/6] rename sync support --- src/codegen/extensions/lsp/completion.py | 0 src/codegen/extensions/lsp/definition.py | 36 +++ src/codegen/extensions/lsp/io.py | 64 +++++ src/codegen/extensions/lsp/lsp.py | 74 ++++-- src/codegen/extensions/lsp/protocol.py | 8 +- src/codegen/extensions/lsp/server.py | 6 +- src/codegen/sdk/codebase/codebase_context.py | 3 +- src/codegen/sdk/codebase/io/io.py | 4 - src/codegen/sdk/core/codebase.py | 6 +- .../extensions/lsp/test_completions.py | 24 +- .../extensions/lsp/test_workspace_sync.py | 250 ++++++++++++++++++ uv.lock | 153 ++++++++++- 12 files changed, 570 insertions(+), 58 deletions(-) create mode 100644 src/codegen/extensions/lsp/completion.py create mode 100644 src/codegen/extensions/lsp/definition.py create mode 100644 src/codegen/extensions/lsp/io.py create mode 100644 tests/unit/codegen/extensions/lsp/test_workspace_sync.py diff --git a/src/codegen/extensions/lsp/completion.py b/src/codegen/extensions/lsp/completion.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/codegen/extensions/lsp/definition.py b/src/codegen/extensions/lsp/definition.py new file mode 100644 index 000000000..318587c0d --- /dev/null +++ b/src/codegen/extensions/lsp/definition.py @@ -0,0 +1,36 @@ +import logging + +from lsprotocol.types import Position + +from codegen.sdk.core.assignment import Assignment +from codegen.sdk.core.detached_symbols.function_call import FunctionCall +from codegen.sdk.core.expressions.chained_attribute import ChainedAttribute +from codegen.sdk.core.expressions.expression import Expression +from codegen.sdk.core.expressions.name import Name +from codegen.sdk.core.interfaces.editable import Editable +from codegen.sdk.core.interfaces.has_name import HasName + +logger = logging.getLogger(__name__) + + +def go_to_definition(node: Editable | None, uri: str, position: Position) -> Editable | None: + if node is None or not isinstance(node, (Expression)): + logger.warning(f"No node found at {uri}:{position}") + return None + if isinstance(node, Name) and isinstance(node.parent, ChainedAttribute) and node.parent.attribute == node: + node = node.parent + if isinstance(node.parent, FunctionCall) and node.parent.get_name() == node: + node = node.parent + logger.info(f"Resolving definition for {node}") + if isinstance(node, FunctionCall): + resolved = node.function_definition + else: + resolved = node.resolved_value + if resolved is None: + logger.warning(f"No resolved value found for {node.name} at {uri}:{position}") + return None + if isinstance(resolved, (HasName,)): + resolved = resolved.get_name() + if isinstance(resolved.parent, Assignment) and resolved.parent.value == resolved: + resolved = resolved.parent.get_name() + return resolved diff --git a/src/codegen/extensions/lsp/io.py b/src/codegen/extensions/lsp/io.py new file mode 100644 index 000000000..0cec6713b --- /dev/null +++ b/src/codegen/extensions/lsp/io.py @@ -0,0 +1,64 @@ +import logging +from pathlib import Path + +from lsprotocol.types import TextDocumentContentChangeWholeDocument +from pygls.workspace import TextDocument, Workspace + +from codegen.sdk.codebase.io.file_io import FileIO +from codegen.sdk.codebase.io.io import IO + +logger = logging.getLogger(__name__) + + +class LSPIO(IO): + base_io: FileIO + workspace: Workspace + + def __init__(self, workspace: Workspace): + self.workspace = workspace + self.base_io = FileIO() + + def _get_doc(self, path: Path) -> TextDocument | None: + uri = path.as_uri() + logger.info(f"Getting document for {uri}") + return self.workspace.get_text_document(uri) + + def read_text(self, path: Path) -> str: + if doc := self._get_doc(path): + return doc.source + return self.base_io.read_text(path) + + def read_bytes(self, path: Path) -> bytes: + if doc := self._get_doc(path): + return doc.source.encode("utf-8") + return self.base_io.read_bytes(path) + + def write_bytes(self, path: Path, content: bytes) -> None: + change = TextDocumentContentChangeWholeDocument( + text=content.decode("utf-8"), + ) + if doc := self._get_doc(path): + doc.apply_change(change) + else: + self.base_io.write_bytes(path, content) + + def save_files(self, files: set[Path] | None = None) -> None: + self.base_io.save_files(files) + + def check_changes(self) -> None: + self.base_io.check_changes() + + def delete_file(self, path: Path) -> None: + self.base_io.delete_file(path) + + def file_exists(self, path: Path) -> bool: + if doc := self._get_doc(path): + try: + doc.source + except FileNotFoundError: + return False + return True + return self.base_io.file_exists(path) + + def untrack_file(self, path: Path) -> None: + self.base_io.untrack_file(path) diff --git a/src/codegen/extensions/lsp/lsp.py b/src/codegen/extensions/lsp/lsp.py index 9461a0515..10992cca9 100644 --- a/src/codegen/extensions/lsp/lsp.py +++ b/src/codegen/extensions/lsp/lsp.py @@ -3,22 +3,61 @@ from lsprotocol import types import codegen +from codegen.extensions.lsp.definition import go_to_definition from codegen.extensions.lsp.document_symbol import get_document_symbol from codegen.extensions.lsp.protocol import CodegenLanguageServerProtocol from codegen.extensions.lsp.range import get_range from codegen.extensions.lsp.server import CodegenLanguageServer -from codegen.sdk.core.assignment import Assignment -from codegen.sdk.core.detached_symbols.function_call import FunctionCall -from codegen.sdk.core.expressions.chained_attribute import ChainedAttribute -from codegen.sdk.core.expressions.expression import Expression -from codegen.sdk.core.expressions.name import Name -from codegen.sdk.core.interfaces.has_name import HasName +from codegen.sdk.codebase.diff_lite import ChangeType, DiffLite version = getattr(codegen, "__version__", "v0.1") server = CodegenLanguageServer("codegen", version, protocol_cls=CodegenLanguageServerProtocol) logger = logging.getLogger(__name__) +@server.feature(types.TEXT_DOCUMENT_DID_OPEN) +def did_open(server: CodegenLanguageServer, params: types.DidOpenTextDocumentParams) -> None: + """Handle document open notification.""" + logger.info(f"Document opened: {params.text_document.uri}") + # The document is automatically added to the workspace by pygls + # We can perform any additional processing here if needed + + +@server.feature(types.TEXT_DOCUMENT_DID_CHANGE) +def did_change(server: CodegenLanguageServer, params: types.DidChangeTextDocumentParams) -> None: + """Handle document change notification.""" + logger.info(f"Document changed: {params.text_document.uri}") + # The document is automatically updated in the workspace by pygls + # We can perform any additional processing here if needed + path = server.get_path(params.text_document.uri) + sync = DiffLite(change_type=ChangeType.Modified, path=path) + server.codebase.ctx.apply_diffs([sync]) + + +@server.feature(types.WORKSPACE_TEXT_DOCUMENT_CONTENT) +def workspace_text_document_content(server: CodegenLanguageServer, params: types.TextDocumentContentParams) -> types.TextDocumentContentResult: + """Handle workspace text document content notification.""" + logger.info(f"Workspace text document content: {params.uri}") + path = server.get_path(params.uri) + if not server.io.file_exists(path): + logger.warning(f"File does not exist: {path}") + return types.TextDocumentContentResult( + text="", + ) + content = server.io.read_text(path) + return types.TextDocumentContentResult( + text=content, + ) + + +@server.feature(types.TEXT_DOCUMENT_DID_CLOSE) +def did_close(server: CodegenLanguageServer, params: types.DidCloseTextDocumentParams) -> None: + """Handle document close notification.""" + logger.info(f"Document closed: {params.text_document.uri}") + # The document is automatically removed from the workspace by pygls + # We can perform any additional cleanup here if needed + + @server.feature( types.TEXT_DOCUMENT_RENAME, ) @@ -43,9 +82,6 @@ def document_symbol(server: CodegenLanguageServer, params: types.DocumentSymbolP return symbols -get_document_symbol - - @server.feature( types.TEXT_DOCUMENT_HOVER, ) @@ -65,25 +101,7 @@ def completion(server: CodegenLanguageServer, params: types.CompletionParams): ) def definition(server: CodegenLanguageServer, params: types.DefinitionParams): node = server.get_node_under_cursor(params.text_document.uri, params.position) - if node is None or not isinstance(node, (Expression)): - logger.warning(f"No node found at {params.text_document.uri}:{params.position}") - return None - if isinstance(node, Name) and isinstance(node.parent, ChainedAttribute) and node.parent.attribute == node: - node = node.parent - if isinstance(node.parent, FunctionCall) and node.parent.get_name() == node: - node = node.parent - logger.info(f"Resolving definition for {node}") - if isinstance(node, FunctionCall): - resolved = node.function_definition - else: - resolved = node.resolved_value - if resolved is None: - logger.warning(f"No resolved value found for {node.name} at {params.text_document.uri}:{params.position}") - return None - if isinstance(resolved, (HasName,)): - resolved = resolved.get_name() - if isinstance(resolved.parent, Assignment) and resolved.parent.value == resolved: - resolved = resolved.parent.get_name() + resolved = go_to_definition(node, params.text_document.uri, params.position) return types.Location( uri=resolved.file.path.as_uri(), range=get_range(resolved), diff --git a/src/codegen/extensions/lsp/protocol.py b/src/codegen/extensions/lsp/protocol.py index 02a7fe4d5..aa745faf8 100644 --- a/src/codegen/extensions/lsp/protocol.py +++ b/src/codegen/extensions/lsp/protocol.py @@ -3,6 +3,7 @@ from lsprotocol.types import INITIALIZE, InitializeParams, InitializeResult from pygls.protocol import LanguageServerProtocol, lsp_method +from codegen.extensions.lsp.io import LSPIO from codegen.sdk.codebase.config import CodebaseConfig, GSFeatureFlags from codegen.sdk.core.codebase import Codebase @@ -12,5 +13,8 @@ class CodegenLanguageServerProtocol(LanguageServerProtocol): def lsp_initialize(self, params: InitializeParams) -> InitializeResult: root = params.root_path or os.getcwd() config = CodebaseConfig(feature_flags=GSFeatureFlags(full_range_index=True)) - self._server.codebase = Codebase(repo_path=root, config=config) - return super().lsp_initialize(params) + ret = super().lsp_initialize(params) + io = LSPIO(self.workspace) + self._server.codebase = Codebase(repo_path=root, config=config, io=io) + self._server.io = io + return ret diff --git a/src/codegen/extensions/lsp/server.py b/src/codegen/extensions/lsp/server.py index 04be5d049..40aefa003 100644 --- a/src/codegen/extensions/lsp/server.py +++ b/src/codegen/extensions/lsp/server.py @@ -4,6 +4,7 @@ from lsprotocol.types import Position, Range from pygls.lsp.server import LanguageServer +from pygls.uris import to_fs_path from codegen.extensions.lsp.range import get_tree_sitter_range from codegen.sdk.codebase.flagging.code_flag import Symbol @@ -20,8 +21,11 @@ class CodegenLanguageServer(LanguageServer): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) + def get_path(self, uri: str) -> Path: + return Path(to_fs_path(uri)).absolute() + def get_file(self, uri: str) -> SourceFile | File: - path = Path(uri) + path = self.get_path(uri) return self.codebase.get_file(path.name) def get_symbol(self, uri: str, position: Position) -> Symbol | None: diff --git a/src/codegen/sdk/codebase/codebase_context.py b/src/codegen/sdk/codebase/codebase_context.py index 6214c4227..806225f96 100644 --- a/src/codegen/sdk/codebase/codebase_context.py +++ b/src/codegen/sdk/codebase/codebase_context.py @@ -115,6 +115,7 @@ def __init__( self, projects: list[ProjectConfig], config: CodebaseConfig = DefaultConfig, + io: IO | None = None, ) -> None: """Initializes codebase graph and TransactionManager""" from codegen.sdk.core.parser import Parser @@ -134,7 +135,7 @@ def __init__( # =====[ __init__ attributes ]===== self.projects = projects - self.io = FileIO() + self.io = io or FileIO() context = projects[0] self.node_classes = get_node_classes(context.programming_language) self.config = config diff --git a/src/codegen/sdk/codebase/io/io.py b/src/codegen/sdk/codebase/io/io.py index 3321f072b..710474aab 100644 --- a/src/codegen/sdk/codebase/io/io.py +++ b/src/codegen/sdk/codebase/io/io.py @@ -18,10 +18,6 @@ def write_file(self, path: Path, content: str | bytes | None) -> None: def write_text(self, path: Path, content: str) -> None: self.write_bytes(path, content.encode("utf-8")) - @abstractmethod - def untrack_file(self, path: Path) -> None: - pass - @abstractmethod def write_bytes(self, path: Path, content: bytes) -> None: pass diff --git a/src/codegen/sdk/core/codebase.py b/src/codegen/sdk/core/codebase.py index ca2a6087b..cb109c8e0 100644 --- a/src/codegen/sdk/core/codebase.py +++ b/src/codegen/sdk/core/codebase.py @@ -33,6 +33,7 @@ from codegen.sdk.codebase.flagging.code_flag import CodeFlag from codegen.sdk.codebase.flagging.enums import FlagKwargs from codegen.sdk.codebase.flagging.group import Group +from codegen.sdk.codebase.io.io import IO from codegen.sdk.codebase.span import Span from codegen.sdk.core.assignment import Assignment from codegen.sdk.core.class_definition import Class @@ -126,6 +127,7 @@ def __init__( programming_language: None = None, projects: list[ProjectConfig] | ProjectConfig, config: CodebaseConfig = DefaultConfig, + io: IO | None = None, ) -> None: ... @overload @@ -136,6 +138,7 @@ def __init__( programming_language: ProgrammingLanguage | None = None, projects: None = None, config: CodebaseConfig = DefaultConfig, + io: IO | None = None, ) -> None: ... def __init__( @@ -145,6 +148,7 @@ def __init__( programming_language: ProgrammingLanguage | None = None, projects: list[ProjectConfig] | ProjectConfig | None = None, config: CodebaseConfig = DefaultConfig, + io: IO | None = None, ) -> None: # Sanity check inputs if repo_path is not None and projects is not None: @@ -174,7 +178,7 @@ def __init__( self._op = main_project.repo_operator self.viz = VisualizationManager(op=self._op) self.repo_path = Path(self._op.repo_path) - self.ctx = CodebaseContext(projects, config=config) + self.ctx = CodebaseContext(projects, config=config, io=io) self.console = Console(record=True, soft_wrap=True) @noapidoc diff --git a/tests/unit/codegen/extensions/lsp/test_completions.py b/tests/unit/codegen/extensions/lsp/test_completions.py index 1c56c8de8..d393495f3 100644 --- a/tests/unit/codegen/extensions/lsp/test_completions.py +++ b/tests/unit/codegen/extensions/lsp/test_completions.py @@ -1,9 +1,7 @@ +from typing import Callable + import pytest -from lsprotocol.types import ( - CompletionParams, - Position, - TextDocumentIdentifier, -) +from lsprotocol.types import CompletionList, CompletionParams, Position, TextDocumentIdentifier from pytest_lsp import ( LanguageClient, ) @@ -12,31 +10,29 @@ @pytest.mark.parametrize( - "original, expected", + "original, position, expected_completions", [ ( { "test.py": """ def hello(): pass +hel """.strip(), }, - { - "test.py": """ -def hello(): - pass - """.strip(), - }, + Position(line=5, character=23), + ["hello"], ) ], ) -async def test_completion(client: LanguageClient, codebase: Codebase, assert_expected): +async def test_completion(client: LanguageClient, codebase: Codebase, position: Position, expected_completions: list[str], assert_expected: Callable): result = await client.text_document_completion_async( params=CompletionParams( - position=Position(line=5, character=23), + position=position, text_document=TextDocumentIdentifier(uri="file://test.py"), ) ) + assert isinstance(result, CompletionList) assert len(result.items) > 0 assert_expected(codebase, check_codebase=False) diff --git a/tests/unit/codegen/extensions/lsp/test_workspace_sync.py b/tests/unit/codegen/extensions/lsp/test_workspace_sync.py new file mode 100644 index 000000000..20839a0ef --- /dev/null +++ b/tests/unit/codegen/extensions/lsp/test_workspace_sync.py @@ -0,0 +1,250 @@ +import pytest +from lsprotocol.types import ( + DidChangeTextDocumentParams, + DidCloseTextDocumentParams, + DidOpenTextDocumentParams, + Position, + Range, + RenameParams, + TextDocumentContentChangeEvent, + TextDocumentContentChangePartial, + TextDocumentContentParams, + TextDocumentIdentifier, + TextDocumentItem, + VersionedTextDocumentIdentifier, +) +from pytest_lsp import LanguageClient + +from codegen.sdk.core.codebase import Codebase + + +@pytest.fixture() +def document_uri(codebase: Codebase, request) -> str: + return request.param.format(workspaceFolder=str(codebase.repo_path)) + + +@pytest.mark.parametrize( + "original, document_uri", + [ + ( + { + "test.py": """ +def example_function(): + pass + """.strip(), + }, + "file://{workspaceFolder}/test.py", + ), + ], + indirect=True, +) +async def test_did_open( + client: LanguageClient, + codebase: Codebase, + original: dict, + document_uri: str, +): + # Send didOpen notification + client.text_document_did_open( + params=DidOpenTextDocumentParams( + text_document=TextDocumentItem( + uri=document_uri, + language_id="python", + version=1, + text=original["test.py"], + ) + ) + ) + + # Verify the file is in the workspace + document = await client.workspace_text_document_content_async(TextDocumentContentParams(uri=document_uri)) + assert document is not None + assert document.text == original["test.py"] + + +@pytest.mark.parametrize( + "original, document_uri, changes, expected_text", + [ + ( + { + "test.py": """ +def example_function(): + pass + """.strip(), + }, + "file://{workspaceFolder}/test.py", + [ + TextDocumentContentChangePartial( + range=Range( + start=Position(line=1, character=4), + end=Position(line=1, character=8), + ), + text="return True", + ), + ], + """ +def example_function(): + return True + """.strip(), + ), + ], + indirect=["document_uri", "original"], +) +async def test_did_change( + client: LanguageClient, + codebase: Codebase, + original: dict, + document_uri: str, + changes: list[TextDocumentContentChangeEvent], + expected_text: str, +): + # First open the document + client.text_document_did_open( + params=DidOpenTextDocumentParams( + text_document=TextDocumentItem( + uri=document_uri, + language_id="python", + version=1, + text=original["test.py"], + ) + ) + ) + + # Send didChange notification + client.text_document_did_change( + params=DidChangeTextDocumentParams( + text_document=VersionedTextDocumentIdentifier( + uri=document_uri, + version=2, + ), + content_changes=changes, + ) + ) + + # Verify the changes were applied + document = await client.workspace_text_document_content_async(TextDocumentContentParams(uri=document_uri)) + assert document is not None + assert document.text == expected_text + + +@pytest.mark.parametrize( + "original, document_uri", + [ + ( + { + "test.py": """ +def example_function(): + pass + """.strip(), + }, + "file://{worskpaceFolder}test.py", + ), + ], +) +async def test_did_close( + client: LanguageClient, + codebase: Codebase, + original: dict, + document_uri: str, +): + document_uri = document_uri.format(worskpaceFolder=str(codebase.repo_path)) + # First open the document + client.text_document_did_open( + params=DidOpenTextDocumentParams( + text_document=TextDocumentItem( + uri=document_uri, + language_id="python", + version=1, + text=original["test.py"], + ) + ) + ) + + # Send didClose notification + client.text_document_did_close(params=DidCloseTextDocumentParams(text_document=TextDocumentIdentifier(uri=document_uri))) + + # Verify the document is removed from the workspace + document = await client.workspace_text_document_content_async(TextDocumentContentParams(uri=document_uri)) + assert document.text == "" + + +@pytest.mark.parametrize( + "original, document_uri, position, new_name, expected_text", + [ + ( + { + "test.py": """ +def example_function(): + pass + +def main(): + example_function() + """.strip(), + }, + "file://{workspaceFolder}/test.py", + Position(line=0, character=0), # Position of 'example_function' + "renamed_function", + """ +def renamed_function(): + pass # modified + +def main(): + renamed_function() + """.strip(), + ), + ], + indirect=["document_uri", "original"], +) +async def test_rename_after_sync( + client: LanguageClient, + codebase: Codebase, + original: dict, + document_uri: str, + position: Position, + new_name: str, + expected_text: str, +): + # First open the document + client.text_document_did_open( + params=DidOpenTextDocumentParams( + text_document=TextDocumentItem( + uri=document_uri, + language_id="python", + version=1, + text=original["test.py"], + ) + ) + ) + + # Make a change to the document + client.text_document_did_change( + params=DidChangeTextDocumentParams( + text_document=VersionedTextDocumentIdentifier( + uri=document_uri, + version=2, + ), + content_changes=[ + TextDocumentContentChangePartial( + range=Range( + start=Position(line=1, character=4), + end=Position(line=1, character=8), + ), + text="pass # modified", + ), + ], + ) + ) + + # Perform rename operation + result = await client.text_document_rename_async( + params=RenameParams( + text_document=TextDocumentIdentifier(uri=document_uri), + position=position, + new_name=new_name, + ) + ) + + # Verify the rename was successful + document = await client.workspace_text_document_content_async(TextDocumentContentParams(uri=document_uri)) + assert document is not None + assert document.text == expected_text diff --git a/uv.lock b/uv.lock index f5220bb2d..85b3e7992 100644 --- a/uv.lock +++ b/uv.lock @@ -567,6 +567,7 @@ dependencies = [ { name = "psutil" }, { name = "pydantic" }, { name = "pydantic-core" }, + { name = "pydantic-settings" }, { name = "pygit2" }, { name = "pygithub" }, { name = "pyinstrument" }, @@ -602,6 +603,10 @@ dependencies = [ ] [package.optional-dependencies] +lsp = [ + { name = "lsprotocol" }, + { name = "pygls" }, +] types = [ { name = "types-networkx" }, { name = "types-requests" }, @@ -628,6 +633,7 @@ dev = [ { name = "jsbeautifier" }, { name = "jupyterlab" }, { name = "loguru" }, + { name = "modal" }, { name = "mypy", extra = ["faster-cache", "mypyc"] }, { name = "pre-commit" }, { name = "pre-commit-uv" }, @@ -635,6 +641,7 @@ dev = [ { name = "pytest-asyncio" }, { name = "pytest-benchmark", extra = ["histogram"] }, { name = "pytest-cov" }, + { name = "pytest-lsp" }, { name = "pytest-mock" }, { name = "pytest-timeout" }, { name = "pytest-xdist" }, @@ -666,17 +673,20 @@ requires-dist = [ { name = "langchain-core" }, { name = "langchain-openai" }, { name = "lazy-object-proxy", specifier = ">=0.0.0" }, + { name = "lsprotocol", marker = "extra == 'lsp'", specifier = "==2024.0.0b1" }, { name = "mini-racer", specifier = ">=0.12.4" }, { name = "networkx", specifier = ">=3.4.1" }, { name = "numpy", specifier = ">=2.2.2" }, { name = "openai", specifier = "==1.61.1" }, { name = "pip", specifier = ">=24.3.1" }, - { name = "plotly", specifier = ">=5.24.0,<6.0.0" }, + { name = "plotly", specifier = ">=5.24.0,<7.0.0" }, { name = "psutil", specifier = ">=5.8.0" }, { name = "pydantic", specifier = ">=2.9.2,<3.0.0" }, { name = "pydantic-core", specifier = ">=2.23.4" }, + { name = "pydantic-settings", specifier = ">=2.0.0" }, { name = "pygit2", specifier = ">=1.16.0" }, { name = "pygithub", specifier = "==2.5.0" }, + { name = "pygls", marker = "extra == 'lsp'", specifier = ">=2.0.0a2" }, { name = "pyinstrument", specifier = ">=5.0.0" }, { name = "pyjson5", specifier = "==1.6.8" }, { name = "pyright", specifier = ">=1.1.372,<2.0.0" }, @@ -732,6 +742,7 @@ dev = [ { name = "jsbeautifier", specifier = ">=1.15.1,<2.0.0" }, { name = "jupyterlab", specifier = ">=4.3.5" }, { name = "loguru", specifier = ">=0.7.3" }, + { name = "modal", specifier = ">=0.73.25" }, { name = "mypy", extras = ["mypyc", "faster-cache"], specifier = ">=1.13.0" }, { name = "pre-commit", specifier = ">=4.0.1" }, { name = "pre-commit-uv", specifier = ">=4.1.4" }, @@ -739,6 +750,7 @@ dev = [ { name = "pytest-asyncio", specifier = ">=0.21.1,<1.0.0" }, { name = "pytest-benchmark", extras = ["histogram"], specifier = ">=5.1.0" }, { name = "pytest-cov", specifier = ">=6.0.0,<6.0.1" }, + { name = "pytest-lsp", specifier = ">=1.0.0b1" }, { name = "pytest-mock", specifier = ">=3.14.0,<4.0.0" }, { name = "pytest-timeout", specifier = ">=2.3.1" }, { name = "pytest-xdist", specifier = ">=3.6.1,<4.0.0" }, @@ -1294,6 +1306,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ac/38/08cc303ddddc4b3d7c628c3039a61a3aae36c241ed01393d00c2fd663473/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", size = 1142112 }, ] +[[package]] +name = "grpclib" +version = "0.4.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h2" }, + { name = "multidict" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/b9/55936e462a5925190d7427e880b3033601d1effd13809b483d13a926061a/grpclib-0.4.7.tar.gz", hash = "sha256:2988ef57c02b22b7a2e8e961792c41ccf97efc2ace91ae7a5b0de03c363823c3", size = 61254 } + [[package]] name = "h11" version = "0.14.0" @@ -1303,6 +1325,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, ] +[[package]] +name = "h2" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hpack" }, + { name = "hyperframe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/38/d7f80fd13e6582fb8e0df8c9a653dcc02b03ca34f4d72f34869298c5baf8/h2-4.2.0.tar.gz", hash = "sha256:c8a52129695e88b1a0578d8d2cc6842bbd79128ac685463b887ee278126ad01f", size = 2150682 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/9e/984486f2d0a0bd2b024bf4bc1c62688fcafa9e61991f041fb0e2def4a982/h2-4.2.0-py3-none-any.whl", hash = "sha256:479a53ad425bb29af087f3458a61d30780bc818e4ebcf01f0b536ba916462ed0", size = 60957 }, +] + [[package]] name = "hatch-vcs" version = "0.4.0" @@ -1331,6 +1366,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/08/e7/ae38d7a6dfba0533684e0b2136817d667588ae3ec984c1a4e5df5eb88482/hatchling-1.27.0-py3-none-any.whl", hash = "sha256:d3a2f3567c4f926ea39849cdf924c7e99e6686c9c8e288ae1037c8fa2a5d937b", size = 75794 }, ] +[[package]] +name = "hpack" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357 }, +] + [[package]] name = "httpcore" version = "1.0.7" @@ -1408,6 +1452,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/92/75/4bc3e242ad13f2e6c12e0b0401ab2c5e5c6f0d7da37ec69bc808e24e0ccb/humanize-4.11.0-py3-none-any.whl", hash = "sha256:b53caaec8532bcb2fff70c8826f904c35943f8cecaca29d272d9df38092736c0", size = 128055 }, ] +[[package]] +name = "hyperframe" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007 }, +] + [[package]] name = "identify" version = "2.6.7" @@ -1994,15 +2047,15 @@ wheels = [ [[package]] name = "lsprotocol" -version = "2023.0.1" +version = "2024.0.0b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "cattrs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/f6/6e80484ec078d0b50699ceb1833597b792a6c695f90c645fbaf54b947e6f/lsprotocol-2023.0.1.tar.gz", hash = "sha256:cc5c15130d2403c18b734304339e51242d3018a05c4f7d0f198ad6e0cd21861d", size = 69434 } +sdist = { url = "https://files.pythonhosted.org/packages/dc/21/0282716d19591e573d20564ee4df65cb5cd8911bfdff35fcde1de2b54072/lsprotocol-2024.0.0b1.tar.gz", hash = "sha256:d3667fb70894d361aa6c495c5c8a1b2e6a44be65ff84c21a9cbb67ebfb4830fd", size = 75358 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/37/2351e48cb3309673492d3a8c59d407b75fb6630e560eb27ecd4da03adc9a/lsprotocol-2023.0.1-py3-none-any.whl", hash = "sha256:c75223c9e4af2f24272b14c6375787438279369236cd568f596d4951052a60f2", size = 70826 }, + { url = "https://files.pythonhosted.org/packages/4d/1b/526af91cd43eba22ac7d9dbdec729dd9d91c2ad335085a61dd42307a7b35/lsprotocol-2024.0.0b1-py3-none-any.whl", hash = "sha256:93785050ac155ae2be16b1ebfbd74c214feb3d3ef77b10399ce941e5ccef6ebd", size = 76600 }, ] [[package]] @@ -2112,6 +2165,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c6/02/c66bdfdadbb021adb642ca4e8a5ed32ada0b4a3e4b39c5d076d19543452f/mistune-3.1.1-py3-none-any.whl", hash = "sha256:02106ac2aa4f66e769debbfa028509a275069dcffce0dfa578edd7b991ee700a", size = 53696 }, ] +[[package]] +name = "modal" +version = "0.73.31" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "certifi" }, + { name = "click" }, + { name = "fastapi" }, + { name = "grpclib" }, + { name = "protobuf" }, + { name = "rich" }, + { name = "synchronicity" }, + { name = "toml" }, + { name = "typer" }, + { name = "types-certifi" }, + { name = "types-toml" }, + { name = "typing-extensions" }, + { name = "watchfiles" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/54/4102a1dbea8da32537606c2e084001eee30e0c2ad1dc336bdb48310d8500/modal-0.73.31-py3-none-any.whl", hash = "sha256:03d7dce03729976d6d3ef80cd79be90ffb74e28bbfbb20a60878e61e820e92e6", size = 533251 }, +] + [[package]] name = "multidict" version = "6.1.0" @@ -2679,6 +2756,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, ] +[[package]] +name = "pydantic-settings" +version = "2.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/7b/c58a586cd7d9ac66d2ee4ba60ca2d241fa837c02bca9bea80a9a8c3d22a9/pydantic_settings-2.7.1.tar.gz", hash = "sha256:10c9caad35e64bfb3c2fbf70a078c0e25cc92499782e5200747f942a065dec93", size = 79920 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/46/93416fdae86d40879714f72956ac14df9c7b76f7d41a4d68aa9f71a0028b/pydantic_settings-2.7.1-py3-none-any.whl", hash = "sha256:590be9e6e24d06db33a4262829edef682500ef008565a969c73d39d5f8bfb3fd", size = 29718 }, +] + [[package]] name = "pyflakes" version = "3.2.0" @@ -2753,15 +2843,15 @@ wheels = [ [[package]] name = "pygls" -version = "1.3.1" +version = "2.0.0a2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cattrs" }, { name = "lsprotocol" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/86/b9/41d173dad9eaa9db9c785a85671fc3d68961f08d67706dc2e79011e10b5c/pygls-1.3.1.tar.gz", hash = "sha256:140edceefa0da0e9b3c533547c892a42a7d2fd9217ae848c330c53d266a55018", size = 45527 } +sdist = { url = "https://files.pythonhosted.org/packages/68/a9/2110bbc90fde62ab7b8f21164caacb5288c06d98486cc569526ec6c0c9ca/pygls-2.0.0a2.tar.gz", hash = "sha256:03e00634ed8d989918268aaa4b4a0c3ab857ea2d4ee94514a52efa5ddd6d5d9f", size = 46279 } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/19/b74a10dd24548e96e8c80226cbacb28b021bc3a168a7d2709fb0d0185348/pygls-1.3.1-py3-none-any.whl", hash = "sha256:6e00f11efc56321bdeb6eac04f6d86131f654c7d49124344a9ebb968da3dd91e", size = 56031 }, + { url = "https://files.pythonhosted.org/packages/f8/47/7d7b3911fbd27153ee38a1a15e3977c72733a41ee8d7f6ce6dca65843fe9/pygls-2.0.0a2-py3-none-any.whl", hash = "sha256:b202369321409343aa6440d73111d9fa0c22e580466ff1c7696b8358bb91f243", size = 58504 }, ] [[package]] @@ -2952,6 +3042,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, ] +[[package]] +name = "pytest-lsp" +version = "1.0.0b2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pygls" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/47/1207bf70218c9cbb6e8a184a1957f699c35d9bf8b43dfa2be5885d35c283/pytest_lsp-1.0.0b2.tar.gz", hash = "sha256:459f62d578d700b63c4ea0b500b5a621461eb2c60d0fd941c3583b0d7930a1ea", size = 26634 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/cc/2f46f5a3db66e50e813cba64da0fed2c517c28b80877585461534c953f22/pytest_lsp-1.0.0b2-py3-none-any.whl", hash = "sha256:d989c69e134ac66e297f0e0eae5edb13470059d7028e50fb06c01674b067fc14", size = 24115 }, +] + [[package]] name = "pytest-mock" version = "3.14.0" @@ -3548,6 +3653,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, ] +[[package]] +name = "sigtools" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/db/669ca14166814da187b3087b908ca924cf83f5b504fe23b3859a3ef67d4f/sigtools-4.0.1.tar.gz", hash = "sha256:4b8e135a9cd4d2ea00da670c093372d74e672ba3abb87f4c98d8e73dea54445c", size = 71910 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/91/853dbf6ec096197dba9cd5fd0c836c5fc19142038b7db60ebe6332b1bab1/sigtools-4.0.1-py2.py3-none-any.whl", hash = "sha256:d216b4cf920bbab0fce636ddc429ed8463a5b533d9e1492acb45a2a1bc36ac6c", size = 76419 }, +] + [[package]] name = "six" version = "1.17.0" @@ -3662,6 +3779,19 @@ pytest = [ { name = "pytest" }, ] +[[package]] +name = "synchronicity" +version = "0.9.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sigtools" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/52/f34a9ab6d514e0808d0f572affb360411d596b3439107318c00889277dd6/synchronicity-0.9.11.tar.gz", hash = "sha256:cb5dbbcb43d637e516ae50db05a776da51a705d1e1a9c0e301f6049afc3c2cae", size = 50323 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/d5/7675cd9b8e18f05b9ea261acad5d197fcb8027d2a65b1a750427ec084593/synchronicity-0.9.11-py3-none-any.whl", hash = "sha256:231129654d2f56b1aa148e85ebd8545231be135771f6d2196d414175b1594ef6", size = 36827 }, +] + [[package]] name = "tabulate" version = "0.9.0" @@ -3921,6 +4051,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/cc/0a838ba5ca64dc832aa43f727bd586309846b0ffb2ce52422543e6075e8a/typer-0.15.1-py3-none-any.whl", hash = "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847", size = 44908 }, ] +[[package]] +name = "types-certifi" +version = "2021.10.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/68/943c3aeaf14624712a0357c4a67814dba5cea36d194f5c764dad7959a00c/types-certifi-2021.10.8.3.tar.gz", hash = "sha256:72cf7798d165bc0b76e1c10dd1ea3097c7063c42c21d664523b928e88b554a4f", size = 2095 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/63/2463d89481e811f007b0e1cd0a91e52e141b47f9de724d20db7b861dcfec/types_certifi-2021.10.8.3-py3-none-any.whl", hash = "sha256:b2d1e325e69f71f7c78e5943d410e650b4707bb0ef32e4ddf3da37f54176e88a", size = 2136 }, +] + [[package]] name = "types-networkx" version = "3.4.2.20241227" From fc86740a03832c182d303c81b10838ea03481c76 Mon Sep 17 00:00:00 2001 From: bagel897 Date: Tue, 11 Feb 2025 11:38:34 -0800 Subject: [PATCH 5/6] make rename work --- src/codegen/extensions/lsp/io.py | 29 +++++++++++------- src/codegen/extensions/lsp/kind.py | 4 ++- src/codegen/extensions/lsp/lsp.py | 18 ++++++++--- src/codegen/extensions/lsp/protocol.py | 41 ++++++++++++++++++++++---- src/codegen/extensions/lsp/server.py | 23 ++++++--------- src/codegen/extensions/lsp/utils.py | 7 +++++ src/codegen/sdk/core/codebase.py | 2 ++ 7 files changed, 89 insertions(+), 35 deletions(-) create mode 100644 src/codegen/extensions/lsp/utils.py diff --git a/src/codegen/extensions/lsp/io.py b/src/codegen/extensions/lsp/io.py index 0cec6713b..cc6152e24 100644 --- a/src/codegen/extensions/lsp/io.py +++ b/src/codegen/extensions/lsp/io.py @@ -1,7 +1,8 @@ import logging from pathlib import Path -from lsprotocol.types import TextDocumentContentChangeWholeDocument +from lsprotocol import types +from lsprotocol.types import Position, Range, TextEdit from pygls.workspace import TextDocument, Workspace from codegen.sdk.codebase.io.file_io import FileIO @@ -13,6 +14,7 @@ class LSPIO(IO): base_io: FileIO workspace: Workspace + changes: dict[str, TextEdit] = {} def __init__(self, workspace: Workspace): self.workspace = workspace @@ -23,24 +25,21 @@ def _get_doc(self, path: Path) -> TextDocument | None: logger.info(f"Getting document for {uri}") return self.workspace.get_text_document(uri) - def read_text(self, path: Path) -> str: - if doc := self._get_doc(path): - return doc.source - return self.base_io.read_text(path) - def read_bytes(self, path: Path) -> bytes: + if self.changes.get(path.as_uri()): + return self.changes[path.as_uri()].new_text.encode("utf-8") if doc := self._get_doc(path): return doc.source.encode("utf-8") return self.base_io.read_bytes(path) def write_bytes(self, path: Path, content: bytes) -> None: - change = TextDocumentContentChangeWholeDocument( - text=content.decode("utf-8"), - ) + logger.info(f"Writing bytes to {path}") + start = Position(line=0, character=0) if doc := self._get_doc(path): - doc.apply_change(change) + end = Position(line=len(doc.source), character=len(doc.source)) else: - self.base_io.write_bytes(path, content) + end = Position(line=0, character=0) + self.changes[path.as_uri()] = TextEdit(range=Range(start=start, end=end), new_text=content.decode("utf-8")) def save_files(self, files: set[Path] | None = None) -> None: self.base_io.save_files(files) @@ -62,3 +61,11 @@ def file_exists(self, path: Path) -> bool: def untrack_file(self, path: Path) -> None: self.base_io.untrack_file(path) + + def get_document_changes(self) -> list[types.TextDocumentEdit]: + ret = [] + for uri, change in self.changes.items(): + id = types.OptionalVersionedTextDocumentIdentifier(uri=uri) + ret.append(types.TextDocumentEdit(text_document=id, edits=[change])) + self.changes = {} + return ret diff --git a/src/codegen/extensions/lsp/kind.py b/src/codegen/extensions/lsp/kind.py index cff49e847..609885164 100644 --- a/src/codegen/extensions/lsp/kind.py +++ b/src/codegen/extensions/lsp/kind.py @@ -6,6 +6,7 @@ from codegen.sdk.core.function import Function from codegen.sdk.core.interface import Interface from codegen.sdk.core.interfaces.editable import Editable +from codegen.sdk.core.statements.attribute import Attribute from codegen.sdk.typescript.namespace import TSNamespace kinds = { @@ -15,6 +16,7 @@ Assignment: SymbolKind.Variable, Interface: SymbolKind.Interface, TSNamespace: SymbolKind.Namespace, + Attribute: SymbolKind.Variable, } @@ -25,5 +27,5 @@ def get_kind(node: Editable) -> SymbolKind: for kind in kinds: if isinstance(node, kind): return kinds[kind] - msg = f"No kind found for {node}" + msg = f"No kind found for {node}, {type(node)}" raise ValueError(msg) diff --git a/src/codegen/extensions/lsp/lsp.py b/src/codegen/extensions/lsp/lsp.py index 10992cca9..2fd056210 100644 --- a/src/codegen/extensions/lsp/lsp.py +++ b/src/codegen/extensions/lsp/lsp.py @@ -8,7 +8,9 @@ from codegen.extensions.lsp.protocol import CodegenLanguageServerProtocol from codegen.extensions.lsp.range import get_range from codegen.extensions.lsp.server import CodegenLanguageServer +from codegen.extensions.lsp.utils import get_path from codegen.sdk.codebase.diff_lite import ChangeType, DiffLite +from codegen.sdk.core.file import SourceFile version = getattr(codegen, "__version__", "v0.1") server = CodegenLanguageServer("codegen", version, protocol_cls=CodegenLanguageServerProtocol) @@ -21,6 +23,11 @@ def did_open(server: CodegenLanguageServer, params: types.DidOpenTextDocumentPar logger.info(f"Document opened: {params.text_document.uri}") # The document is automatically added to the workspace by pygls # We can perform any additional processing here if needed + path = get_path(params.text_document.uri) + file = server.codebase.get_file(str(path), optional=True) + if not isinstance(file, SourceFile) and path.suffix in server.codebase.ctx.extensions: + sync = DiffLite(change_type=ChangeType.Added, path=path) + server.codebase.ctx.apply_diffs([sync]) @server.feature(types.TEXT_DOCUMENT_DID_CHANGE) @@ -29,7 +36,7 @@ def did_change(server: CodegenLanguageServer, params: types.DidChangeTextDocumen logger.info(f"Document changed: {params.text_document.uri}") # The document is automatically updated in the workspace by pygls # We can perform any additional processing here if needed - path = server.get_path(params.text_document.uri) + path = get_path(params.text_document.uri) sync = DiffLite(change_type=ChangeType.Modified, path=path) server.codebase.ctx.apply_diffs([sync]) @@ -37,8 +44,8 @@ def did_change(server: CodegenLanguageServer, params: types.DidChangeTextDocumen @server.feature(types.WORKSPACE_TEXT_DOCUMENT_CONTENT) def workspace_text_document_content(server: CodegenLanguageServer, params: types.TextDocumentContentParams) -> types.TextDocumentContentResult: """Handle workspace text document content notification.""" - logger.info(f"Workspace text document content: {params.uri}") - path = server.get_path(params.uri) + logger.debug(f"Workspace text document content: {params.uri}") + path = get_path(params.uri) if not server.io.file_exists(path): logger.warning(f"File does not exist: {path}") return types.TextDocumentContentResult( @@ -61,7 +68,7 @@ def did_close(server: CodegenLanguageServer, params: types.DidCloseTextDocumentP @server.feature( types.TEXT_DOCUMENT_RENAME, ) -def rename(server: CodegenLanguageServer, params: types.RenameParams): +def rename(server: CodegenLanguageServer, params: types.RenameParams) -> types.RenameResult: symbol = server.get_symbol(params.text_document.uri, params.position) if symbol is None: logger.warning(f"No symbol found at {params.text_document.uri}:{params.position}") @@ -69,6 +76,9 @@ def rename(server: CodegenLanguageServer, params: types.RenameParams): logger.info(f"Renaming symbol {symbol.name} to {params.new_name}") symbol.rename(params.new_name) server.codebase.commit() + return types.WorkspaceEdit( + document_changes=server.io.get_document_changes(), + ) @server.feature( diff --git a/src/codegen/extensions/lsp/protocol.py b/src/codegen/extensions/lsp/protocol.py index aa745faf8..78e9bd945 100644 --- a/src/codegen/extensions/lsp/protocol.py +++ b/src/codegen/extensions/lsp/protocol.py @@ -1,20 +1,51 @@ import os +import threading +from pathlib import Path +from typing import TYPE_CHECKING -from lsprotocol.types import INITIALIZE, InitializeParams, InitializeResult +from lsprotocol.types import INITIALIZE, INITIALIZED, InitializedParams, InitializeParams, InitializeResult from pygls.protocol import LanguageServerProtocol, lsp_method from codegen.extensions.lsp.io import LSPIO +from codegen.extensions.lsp.utils import get_path from codegen.sdk.codebase.config import CodebaseConfig, GSFeatureFlags from codegen.sdk.core.codebase import Codebase +if TYPE_CHECKING: + from codegen.extensions.lsp.server import CodegenLanguageServer + class CodegenLanguageServerProtocol(LanguageServerProtocol): + _server: "CodegenLanguageServer" + + def _init_codebase(self, params: InitializeParams) -> None: + if params.root_path: + root = Path(params.root_path) + elif params.root_uri: + root = get_path(params.root_uri) + else: + root = os.getcwd() + config = CodebaseConfig(feature_flags=GSFeatureFlags(full_range_index=True)) + io = LSPIO(self.workspace) + self._server.codebase = Codebase(repo_path=str(root), config=config, io=io) + self._server.io = io + @lsp_method(INITIALIZE) def lsp_initialize(self, params: InitializeParams) -> InitializeResult: - root = params.root_path or os.getcwd() + if params.root_path: + root = Path(params.root_path) + elif params.root_uri: + root = get_path(params.root_uri) + else: + root = os.getcwd() config = CodebaseConfig(feature_flags=GSFeatureFlags(full_range_index=True)) ret = super().lsp_initialize(params) - io = LSPIO(self.workspace) - self._server.codebase = Codebase(repo_path=root, config=config, io=io) - self._server.io = io + + self._worker = threading.Thread(target=self._init_codebase, args=(params,)) + self._worker.start() return ret + + @lsp_method(INITIALIZED) + def lsp_initialized(self, params: InitializedParams) -> None: + self._worker.join() + super().lsp_initialized(params) diff --git a/src/codegen/extensions/lsp/server.py b/src/codegen/extensions/lsp/server.py index 40aefa003..bec090f89 100644 --- a/src/codegen/extensions/lsp/server.py +++ b/src/codegen/extensions/lsp/server.py @@ -1,12 +1,12 @@ import logging -from pathlib import Path from typing import Any, Optional from lsprotocol.types import Position, Range from pygls.lsp.server import LanguageServer -from pygls.uris import to_fs_path +from codegen.extensions.lsp.io import LSPIO from codegen.extensions.lsp.range import get_tree_sitter_range +from codegen.extensions.lsp.utils import get_path from codegen.sdk.codebase.flagging.code_flag import Symbol from codegen.sdk.core.codebase import Codebase from codegen.sdk.core.file import File, SourceFile @@ -17,25 +17,20 @@ class CodegenLanguageServer(LanguageServer): codebase: Optional[Codebase] + io: Optional[LSPIO] def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - def get_path(self, uri: str) -> Path: - return Path(to_fs_path(uri)).absolute() - def get_file(self, uri: str) -> SourceFile | File: - path = self.get_path(uri) - return self.codebase.get_file(path.name) + path = get_path(uri) + return self.codebase.get_file(str(path)) def get_symbol(self, uri: str, position: Position) -> Symbol | None: - file = self.get_file(uri) - line = position.line - char = position.character - for symbol in file.symbols: - if symbol.start_point.row >= line and symbol.start_point.column >= char: - return symbol - return None + node = self.get_node_under_cursor(uri, position) + if node is None: + return None + return node.parent_symbol def get_node_under_cursor(self, uri: str, position: Position) -> Editable | None: file = self.get_file(uri) diff --git a/src/codegen/extensions/lsp/utils.py b/src/codegen/extensions/lsp/utils.py new file mode 100644 index 000000000..3dce5f751 --- /dev/null +++ b/src/codegen/extensions/lsp/utils.py @@ -0,0 +1,7 @@ +from pathlib import Path + +from pygls.uris import to_fs_path + + +def get_path(uri: str) -> Path: + return Path(to_fs_path(uri)).absolute() diff --git a/src/codegen/sdk/core/codebase.py b/src/codegen/sdk/core/codebase.py index cb109c8e0..0469dd233 100644 --- a/src/codegen/sdk/core/codebase.py +++ b/src/codegen/sdk/core/codebase.py @@ -495,6 +495,8 @@ def get_file_from_path(path: Path) -> File | None: if file is not None: return file absolute_path = self.ctx.to_absolute(filepath) + if absolute_path.suffix in self.ctx.extensions: + return None if self.ctx.io.file_exists(absolute_path): return get_file_from_path(absolute_path) elif ignore_case: From c569aebafa09c55ca2c147c70f1af1a1924573ae Mon Sep 17 00:00:00 2001 From: bagel897 Date: Tue, 11 Feb 2025 11:40:09 -0800 Subject: [PATCH 6/6] remove extra apis --- src/codegen/extensions/lsp/lsp.py | 14 ---- .../extensions/lsp/test_completions.py | 38 ---------- .../unit/codegen/extensions/lsp/test_hover.py | 69 ------------------- 3 files changed, 121 deletions(-) delete mode 100644 tests/unit/codegen/extensions/lsp/test_completions.py delete mode 100644 tests/unit/codegen/extensions/lsp/test_hover.py diff --git a/src/codegen/extensions/lsp/lsp.py b/src/codegen/extensions/lsp/lsp.py index 2fd056210..2a7cd7aec 100644 --- a/src/codegen/extensions/lsp/lsp.py +++ b/src/codegen/extensions/lsp/lsp.py @@ -92,20 +92,6 @@ def document_symbol(server: CodegenLanguageServer, params: types.DocumentSymbolP return symbols -@server.feature( - types.TEXT_DOCUMENT_HOVER, -) -def hover(server: CodegenLanguageServer, params: types.HoverParams) -> types.HoverResponse: - pass - - -@server.feature( - types.TEXT_DOCUMENT_COMPLETION, -) -def completion(server: CodegenLanguageServer, params: types.CompletionParams): - pass - - @server.feature( types.TEXT_DOCUMENT_DEFINITION, ) diff --git a/tests/unit/codegen/extensions/lsp/test_completions.py b/tests/unit/codegen/extensions/lsp/test_completions.py deleted file mode 100644 index d393495f3..000000000 --- a/tests/unit/codegen/extensions/lsp/test_completions.py +++ /dev/null @@ -1,38 +0,0 @@ -from typing import Callable - -import pytest -from lsprotocol.types import CompletionList, CompletionParams, Position, TextDocumentIdentifier -from pytest_lsp import ( - LanguageClient, -) - -from codegen.sdk.core.codebase import Codebase - - -@pytest.mark.parametrize( - "original, position, expected_completions", - [ - ( - { - "test.py": """ -def hello(): - pass -hel - """.strip(), - }, - Position(line=5, character=23), - ["hello"], - ) - ], -) -async def test_completion(client: LanguageClient, codebase: Codebase, position: Position, expected_completions: list[str], assert_expected: Callable): - result = await client.text_document_completion_async( - params=CompletionParams( - position=position, - text_document=TextDocumentIdentifier(uri="file://test.py"), - ) - ) - assert isinstance(result, CompletionList) - - assert len(result.items) > 0 - assert_expected(codebase, check_codebase=False) diff --git a/tests/unit/codegen/extensions/lsp/test_hover.py b/tests/unit/codegen/extensions/lsp/test_hover.py deleted file mode 100644 index 42e824b61..000000000 --- a/tests/unit/codegen/extensions/lsp/test_hover.py +++ /dev/null @@ -1,69 +0,0 @@ -from typing import Any, Union - -import pytest -from lsprotocol.types import ( - Hover, - HoverParams, - MarkupContent, - Position, - TextDocumentIdentifier, -) -from pytest_lsp import LanguageClient - -from codegen.sdk.core.codebase import Codebase - - -@pytest.mark.parametrize( - "original, position, expected_content", - [ - ( - { - "test.py": """ -def example_function(param: str) -> str: - \"\"\"Example function that returns a greeting. - - Args: - param: Name to greet - - Returns: - A greeting message - \"\"\" - return f"Hello, {param}!" - -example_function("world") - """.strip(), - }, - Position(line=11, character=0), # Position of example_function call - "```python\ndef example_function(param: str) -> str\n```\n\nExample function that returns a greeting.\n\nArgs:\n param: Name to greet\n\nReturns:\n A greeting message", - ) - ], -) -async def test_hover( - client: LanguageClient, - codebase: Codebase, - original: dict, - position: Position, - expected_content: str, -): - result = await client.text_document_hover_async( - params=HoverParams( - text_document=TextDocumentIdentifier(uri="file://test.py"), - position=position, - ) - ) - - assert isinstance(result, Hover) - - def extract_content(hover_content: Union[MarkupContent, str, list[dict[str, Any]], dict[str, Any]]) -> str: - if isinstance(hover_content, MarkupContent): - return hover_content.value - elif isinstance(hover_content, str): - return hover_content - elif isinstance(hover_content, dict): - return hover_content.get("value", "") - elif isinstance(hover_content, list): - return "\n".join(item.get("value", str(item)) if isinstance(item, dict) else str(item) for item in hover_content) - return "" - - actual_content = extract_content(result.contents) - assert actual_content == expected_content