From 310b9c2d6f26ce95cc67ea57e6a053d86a63081f Mon Sep 17 00:00:00 2001 From: Alex Carney Date: Sun, 17 Mar 2024 18:22:58 +0000 Subject: [PATCH 1/8] fix: compute `resolve_provider` value for `CodeLensOptions` --- pygls/capabilities.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pygls/capabilities.py b/pygls/capabilities.py index 9db4744c..38bf7ba0 100644 --- a/pygls/capabilities.py +++ b/pygls/capabilities.py @@ -201,6 +201,7 @@ def _with_code_lens(self): types.TEXT_DOCUMENT_CODE_LENS, default=types.CodeLensOptions() ) if value is not None: + value.resolve_provider = types.CODE_LENS_RESOLVE in self.features self.server_cap.code_lens_provider = value return self From 6467b931660f524a0187b1b2a37805d7ebfb6751 Mon Sep 17 00:00:00 2001 From: Alex Carney Date: Sun, 17 Mar 2024 18:26:05 +0000 Subject: [PATCH 2/8] refactor: end-to-end test infrastructure This commit refactors the infrastructure for writing end-to-end tests. There are now three fixtures `get_client_for` and `path_for` and `uri_for`, as the names suggest these are functions for obtaining a `LanguageClient` connected to a server in `examples/servers` and filepaths or URIs for files in `examples/servers/workspace`. This also adds a custom cli flag to our test suite - `--lsp-runtime` for choosing the runtime that the end-to-end tests are run against. Currently, the only supported runtime is `cpython` but this paves the way for other runtimes in the future (such as Pyodide or WASI) --- tests/conftest.py | 140 +++++++++++++++++++----- tests/{lsp => e2e}/test_code_action.py | 20 +++- tests/{lsp => e2e}/test_completion.py | 21 +++- tests/{lsp => e2e}/test_inlay_hints.py | 22 +++- tests/{lsp => e2e}/test_inline_value.py | 21 +++- 5 files changed, 178 insertions(+), 46 deletions(-) rename tests/{lsp => e2e}/test_code_action.py (84%) rename tests/{lsp => e2e}/test_completion.py (80%) rename tests/{lsp => e2e}/test_inlay_hints.py (82%) rename tests/{lsp => e2e}/test_inline_value.py (83%) diff --git a/tests/conftest.py b/tests/conftest.py index 5dd2ecb3..f4cf9c66 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,12 +18,14 @@ ############################################################################ import asyncio import pathlib +import sys import pytest from lsprotocol import types, converters from pygls import uris, IS_PYODIDE from pygls.feature_manager import FeatureManager +from pygls.lsp.client import BaseLanguageClient from pygls.workspace import Workspace from .ls_setup import ( @@ -32,7 +34,6 @@ setup_ls_features, ) -from .client import create_client_for_server DOC = """document for @@ -41,6 +42,10 @@ """ DOC_URI = uris.from_fs_path(__file__) or "" +REPO_DIR = pathlib.Path(__file__, "..", "..").resolve() +SERVER_DIR = REPO_DIR / "examples" / "servers" +WORKSPACE_DIR = REPO_DIR / "examples" / "servers" / "workspace" + ClientServer = NativeClientServer if IS_PYODIDE: @@ -64,20 +69,6 @@ def client_server(request): client_server.stop() -@pytest.fixture(scope="session") -def uri_for(): - """Returns the uri corresponsing to a file in the example workspace.""" - base_dir = pathlib.Path( - __file__, "..", "..", "examples", "servers", "workspace" - ).resolve() - - def fn(*args): - fpath = pathlib.Path(base_dir, *args) - return uris.from_fs_path(str(fpath)) - - return fn - - @pytest.fixture() def event_loop(): """Redefine `pytest-asyncio's default event_loop fixture to match the scope @@ -95,18 +86,6 @@ def event_loop(): pass -@pytest.fixture(scope="session") -def server_dir(): - """Returns the directory where all the example language servers live""" - path = pathlib.Path(__file__) / ".." / ".." / "examples" / "servers" - return path.resolve() - - -code_action_client = create_client_for_server("code_actions.py") -inlay_hints_client = create_client_for_server("inlay_hints.py") -json_server_client = create_client_for_server("json_server.py") - - @pytest.fixture def feature_manager(): """Return a feature manager""" @@ -120,3 +99,110 @@ def workspace(tmpdir): uris.from_fs_path(str(tmpdir)), sync_kind=types.TextDocumentSyncKind.Incremental, ) + + +class LanguageClient(BaseLanguageClient): + """Language client to use for testing.""" + + async def server_exit(self, server: asyncio.subprocess.Process): + # -15: terminated (probably by the client) + # 0: all ok + if server.returncode not in {-15, 0}: + if server.stderr is not None: + err = await server.stderr.read() + print(f"stderr: {err.decode('utf8')}", file=sys.stderr) + + +def pytest_addoption(parser): + """Add extra cli arguments to pytest.""" + group = parser.getgroup("pygls") + group.addoption( + "--lsp-runtime", + dest="lsp_runtime", + action="store", + default="cpython", + choices=("cpython",), + help="Choose the runtime in which to run servers under test.", + ) + + +@pytest.fixture(scope="session") +def runtime(request): + """This fixture is the source of truth as to which environment we should run the + end-to-end tests in.""" + return request.config.getoption("lsp_runtime") + + +@pytest.fixture(scope="session") +def path_for(): + """Returns the path corresponding to a file in the example workspace""" + + def fn(*args): + fpath = pathlib.Path(WORKSPACE_DIR, *args) + assert fpath.exists() + + return fpath + + return fn + + +@pytest.fixture(scope="session") +def uri_for(runtime, path_for): + """Returns the uri corresponsing to a file in the example workspace. + + Takes into account the runtime. + """ + + def fn(*args): + fpath = path_for(*args) + uri = uris.from_fs_path(str(fpath)) + + assert uri is not None + return uri + + return fn + + +@pytest.fixture(scope="session") +def server_dir(): + """Returns the directory where all the example language servers live""" + path = pathlib.Path(__file__) / ".." / ".." / "examples" / "servers" + return path.resolve() + + +def get_client_for_cpython_server(uri_fixture): + """Return a client configured to communicate with a server running under cpython.""" + + async def fn(server_name: str): + client = LanguageClient("pygls-test-suite", "v1") + await client.start_io(sys.executable, str(SERVER_DIR / server_name)) + + response = await client.initialize_async( + types.InitializeParams( + capabilities=types.ClientCapabilities(), + root_uri=uri_fixture(""), + ) + ) + assert response is not None + yield client, response + + await client.shutdown_async(None) + client.exit(None) + + await client.stop() + + return fn + + +@pytest.fixture(scope="session") +def get_client_for(runtime, uri_for): + """Return a client configured to communicate with the specified server. + + Takes into account the current runtime. + + It's the consuming fixture's responsibility to stop the client. + """ + if runtime not in {"cpython"}: + raise NotImplementedError(f"get_client_for: {runtime=}") + + return get_client_for_cpython_server(uri_for) diff --git a/tests/lsp/test_code_action.py b/tests/e2e/test_code_action.py similarity index 84% rename from tests/lsp/test_code_action.py rename to tests/e2e/test_code_action.py index c10dcd5c..7b74a4d2 100644 --- a/tests/lsp/test_code_action.py +++ b/tests/e2e/test_code_action.py @@ -14,18 +14,30 @@ # See the License for the specific language governing permissions and # # limitations under the License. # ############################################################################ -from typing import Tuple +from __future__ import annotations +import typing + +import pytest_asyncio from lsprotocol import types -from ..client import LanguageClient +if typing.TYPE_CHECKING: + from typing import Tuple + + from pygls.lsp.client import BaseLanguageClient + + +@pytest_asyncio.fixture() +async def code_actions(get_client_for): + async for result in get_client_for("code_actions.py"): + yield result async def test_code_actions( - code_action_client: Tuple[LanguageClient, types.InitializeResult], uri_for + code_actions: Tuple[BaseLanguageClient, types.InitializeResult], uri_for ): """Ensure that the example code action server is working as expected.""" - client, initialize_result = code_action_client + client, initialize_result = code_actions code_action_options = initialize_result.capabilities.code_action_provider assert code_action_options.code_action_kinds == [types.CodeActionKind.QuickFix] diff --git a/tests/lsp/test_completion.py b/tests/e2e/test_completion.py similarity index 80% rename from tests/lsp/test_completion.py rename to tests/e2e/test_completion.py index dcb81245..ffff9797 100644 --- a/tests/lsp/test_completion.py +++ b/tests/e2e/test_completion.py @@ -14,27 +14,38 @@ # See the License for the specific language governing permissions and # # limitations under the License. # ############################################################################ -from typing import Tuple +from __future__ import annotations +import typing + +import pytest_asyncio from lsprotocol import types +if typing.TYPE_CHECKING: + from typing import Tuple + + from pygls.lsp.client import BaseLanguageClient + -from ..client import LanguageClient +@pytest_asyncio.fixture() +async def json_server(get_client_for): + async for result in get_client_for("json_server.py"): + yield result async def test_completion( - json_server_client: Tuple[LanguageClient, types.InitializeResult], + json_server: Tuple[BaseLanguageClient, types.InitializeResult], uri_for, ): """Ensure that the completion methods are working as expected.""" - client, initialize_result = json_server_client + client, initialize_result = json_server completion_provider = initialize_result.capabilities.completion_provider assert completion_provider assert completion_provider.trigger_characters == [","] assert completion_provider.all_commit_characters == [":"] - test_uri = uri_for("example.json") + test_uri = uri_for("test.json") assert test_uri is not None response = await client.text_document_completion_async( diff --git a/tests/lsp/test_inlay_hints.py b/tests/e2e/test_inlay_hints.py similarity index 82% rename from tests/lsp/test_inlay_hints.py rename to tests/e2e/test_inlay_hints.py index 1146e1b9..c764acfc 100644 --- a/tests/lsp/test_inlay_hints.py +++ b/tests/e2e/test_inlay_hints.py @@ -14,18 +14,30 @@ # See the License for the specific language governing permissions and # # limitations under the License. # ############################################################################ -from typing import Tuple +from __future__ import annotations +import typing + +import pytest_asyncio from lsprotocol import types -from ..client import LanguageClient +if typing.TYPE_CHECKING: + from typing import Tuple + + from pygls.lsp.client import BaseLanguageClient + + +@pytest_asyncio.fixture() +async def inlay_hints(get_client_for): + async for result in get_client_for("inlay_hints.py"): + yield result -async def test_code_actions( - inlay_hints_client: Tuple[LanguageClient, types.InitializeResult], uri_for +async def test_inlay_hints( + inlay_hints: Tuple[BaseLanguageClient, types.InitializeResult], uri_for ): """Ensure that the example code action server is working as expected.""" - client, initialize_result = inlay_hints_client + client, initialize_result = inlay_hints inlay_hint_provider = initialize_result.capabilities.inlay_hint_provider assert inlay_hint_provider.resolve_provider is True diff --git a/tests/lsp/test_inline_value.py b/tests/e2e/test_inline_value.py similarity index 83% rename from tests/lsp/test_inline_value.py rename to tests/e2e/test_inline_value.py index 68d682d9..d0636f11 100644 --- a/tests/lsp/test_inline_value.py +++ b/tests/e2e/test_inline_value.py @@ -14,22 +14,33 @@ # See the License for the specific language governing permissions and # # limitations under the License. # ############################################################################ -from typing import Tuple +from __future__ import annotations +import typing + +import pytest_asyncio from lsprotocol import types +if typing.TYPE_CHECKING: + from typing import Tuple + + from pygls.lsp.client import BaseLanguageClient + -from ..client import LanguageClient +@pytest_asyncio.fixture() +async def json_server(get_client_for): + async for result in get_client_for("json_server.py"): + yield result async def test_inline_value( - json_server_client: Tuple[LanguageClient, types.InitializeResult], + json_server: Tuple[BaseLanguageClient, types.InitializeResult], uri_for, ): """Ensure that inline values are working as expected.""" - client, _ = json_server_client + client, _ = json_server - test_uri = uri_for("example.json") + test_uri = uri_for("test.json") assert test_uri is not None document_content = '{\n"foo": "bar"\n}' From cc68caa712678807106d268245a299a42ecca835 Mon Sep 17 00:00:00 2001 From: Alex Carney Date: Sun, 17 Mar 2024 18:31:00 +0000 Subject: [PATCH 3/8] test: add end-to-end test for `textDocument/hover` requests --- examples/servers/hover.py | 78 ++++++++++++++ examples/servers/workspace/dates.txt | 3 + tests/e2e/test_hover.py | 108 +++++++++++++++++++ tests/lsp/test_hover.py | 149 --------------------------- 4 files changed, 189 insertions(+), 149 deletions(-) create mode 100644 examples/servers/hover.py create mode 100644 examples/servers/workspace/dates.txt create mode 100644 tests/e2e/test_hover.py delete mode 100644 tests/lsp/test_hover.py diff --git a/examples/servers/hover.py b/examples/servers/hover.py new file mode 100644 index 00000000..f7c94ee9 --- /dev/null +++ b/examples/servers/hover.py @@ -0,0 +1,78 @@ +############################################################################ +# Copyright(c) Open Law Library. All rights reserved. # +# See ThirdPartyNotices.txt in the project root for additional notices. # +# # +# Licensed under the Apache License, Version 2.0 (the "License") # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http: // www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +############################################################################ +import logging +from datetime import datetime + +from lsprotocol import types + +from pygls.server import LanguageServer + +DATE_FORMATS = [ + "%H:%M:%S", + "%d/%m/%y", + "%Y-%m-%d", + "%Y-%m-%dT%H:%M:%S", +] +server = LanguageServer("hover-server", "v1") + + +@server.feature(types.TEXT_DOCUMENT_HOVER) +def hover(params: types.HoverParams): + pos = params.position + document_uri = params.text_document.uri + document = server.workspace.get_text_document(document_uri) + + try: + line = document.lines[pos.line] + except IndexError: + return None + + for fmt in DATE_FORMATS: + try: + value = datetime.strptime(line.strip(), fmt) + break + except ValueError: + pass + + else: + # No valid datetime found. + return None + + hover_content = [ + f"# {value.strftime('%a %d %b %Y')}", + "", + "| Format | Value |", + "|:-|-:|", + *[f"| `{fmt}` | {value.strftime(fmt)} |" for fmt in DATE_FORMATS], + ] + + return types.Hover( + contents=types.MarkupContent( + kind=types.MarkupKind.Markdown, + value="\n".join(hover_content), + ), + range=types.Range( + start=types.Position(line=pos.line, character=0), + end=types.Position(line=pos.line + 1, character=0), + ), + ) + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO, format="%(message)s") + + server.start_io() diff --git a/examples/servers/workspace/dates.txt b/examples/servers/workspace/dates.txt new file mode 100644 index 00000000..8ce93f3c --- /dev/null +++ b/examples/servers/workspace/dates.txt @@ -0,0 +1,3 @@ +01/02/20 + +1921-01-02T23:59:00 diff --git a/tests/e2e/test_hover.py b/tests/e2e/test_hover.py new file mode 100644 index 00000000..f41a36a3 --- /dev/null +++ b/tests/e2e/test_hover.py @@ -0,0 +1,108 @@ +############################################################################ +# Copyright(c) Open Law Library. All rights reserved. # +# See ThirdPartyNotices.txt in the project root for additional notices. # +# # +# Licensed under the Apache License, Version 2.0 (the "License") # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http: // www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +############################################################################ +from __future__ import annotations + +import typing + +import pytest +import pytest_asyncio +from lsprotocol import types + +if typing.TYPE_CHECKING: + from typing import List + from typing import Tuple + + from pygls.lsp.client import BaseLanguageClient + + +@pytest_asyncio.fixture(scope="module") +async def hover(get_client_for): + async for result in get_client_for("hover.py"): + yield result + + +@pytest.mark.parametrize( + "position, expected", + [ + ( + types.Position(line=0, character=3), + "\n".join( + [ + "# Sat 01 Feb 2020", + "", + "| Format | Value |", + "|:-|-:|", + "| `%H:%M:%S` | 00:00:00 |", + "| `%d/%m/%y` | 01/02/20 |", + "| `%Y-%m-%d` | 2020-02-01 |", + "| `%Y-%m-%dT%H:%M:%S` | 2020-02-01T00:00:00 |", + ] + ), + ), + (types.Position(line=1, character=3), None), + ( + types.Position(line=2, character=3), + "\n".join( + [ + "# Sun 02 Jan 1921", + "", + "| Format | Value |", + "|:-|-:|", + "| `%H:%M:%S` | 23:59:00 |", + "| `%d/%m/%y` | 02/01/21 |", + "| `%Y-%m-%d` | 1921-01-02 |", + "| `%Y-%m-%dT%H:%M:%S` | 1921-01-02T23:59:00 |", + ] + ), + ), + ], +) +@pytest.mark.asyncio(scope="module") +async def test_hover( + hover: Tuple[BaseLanguageClient, types.InitializeResult], + uri_for, + position: types.Position, + expected: List[str], +): + """Ensure that the example hover server is working as expected.""" + client, initialize_result = hover + + hover_options = initialize_result.capabilities.hover_provider + assert hover_options is True + + test_uri = uri_for("dates.txt") + response = await client.text_document_hover_async( + types.HoverParams( + position=position, + text_document=types.TextDocumentIdentifier(uri=test_uri), + ) + ) + + if expected is None: + assert response is None + return + + assert response == types.Hover( + contents=types.MarkupContent( + kind=types.MarkupKind.Markdown, + value=expected, + ), + range=types.Range( + start=types.Position(line=position.line, character=0), + end=types.Position(line=position.line + 1, character=0), + ), + ) diff --git a/tests/lsp/test_hover.py b/tests/lsp/test_hover.py deleted file mode 100644 index 9007c780..00000000 --- a/tests/lsp/test_hover.py +++ /dev/null @@ -1,149 +0,0 @@ -############################################################################ -# Copyright(c) Open Law Library. All rights reserved. # -# See ThirdPartyNotices.txt in the project root for additional notices. # -# # -# Licensed under the Apache License, Version 2.0 (the "License") # -# you may not use this file except in compliance with the License. # -# You may obtain a copy of the License at # -# # -# http: // www.apache.org/licenses/LICENSE-2.0 # -# # -# Unless required by applicable law or agreed to in writing, software # -# distributed under the License is distributed on an "AS IS" BASIS, # -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # -# See the License for the specific language governing permissions and # -# limitations under the License. # -############################################################################ - -from typing import Optional - -from lsprotocol.types import TEXT_DOCUMENT_HOVER -from lsprotocol.types import ( - Hover, - HoverOptions, - HoverParams, - MarkedString_Type1, - MarkupContent, - MarkupKind, - Position, - Range, - TextDocumentIdentifier, -) - -from ..conftest import ClientServer - - -class ConfiguredLS(ClientServer): - def __init__(self): - super().__init__() - - @self.server.feature( - TEXT_DOCUMENT_HOVER, - HoverOptions(), - ) - def f(params: HoverParams) -> Optional[Hover]: - range = Range( - start=Position(line=0, character=0), - end=Position(line=1, character=1), - ) - - return { - "file://return.marked_string": Hover( - range=range, - contents=MarkedString_Type1( - language="language", - value="value", - ), - ), - "file://return.marked_string_list": Hover( - range=range, - contents=[ - MarkedString_Type1( - language="language", - value="value", - ), - "str type", - ], - ), - "file://return.markup_content": Hover( - range=range, - contents=MarkupContent(kind=MarkupKind.Markdown, value="value"), - ), - }.get(params.text_document.uri, None) - - -@ConfiguredLS.decorate() -def test_capabilities(client_server): - _, server = client_server - capabilities = server.server_capabilities - - assert capabilities.hover_provider - - -@ConfiguredLS.decorate() -def test_hover_return_marked_string(client_server): - client, _ = client_server - response = client.lsp.send_request( - TEXT_DOCUMENT_HOVER, - HoverParams( - text_document=TextDocumentIdentifier(uri="file://return.marked_string"), - position=Position(line=0, character=0), - ), - ).result() - - assert response - - assert response.contents.language == "language" - assert response.contents.value == "value" - - assert response.range.start.line == 0 - assert response.range.start.character == 0 - assert response.range.end.line == 1 - assert response.range.end.character == 1 - - -@ConfiguredLS.decorate() -def test_hover_return_marked_string_list(client_server): - client, _ = client_server - response = client.lsp.send_request( - TEXT_DOCUMENT_HOVER, - HoverParams( - text_document=TextDocumentIdentifier( - uri="file://return.marked_string_list" - ), - position=Position(line=0, character=0), - ), - ).result() - - assert response - - assert response.contents[0].language == "language" - assert response.contents[0].value == "value" - assert response.contents[1] == "str type" - - assert response.range.start.line == 0 - assert response.range.start.character == 0 - assert response.range.end.line == 1 - assert response.range.end.character == 1 - - -@ConfiguredLS.decorate() -def test_hover_return_markup_content(client_server): - client, _ = client_server - response = client.lsp.send_request( - TEXT_DOCUMENT_HOVER, - HoverParams( - text_document=TextDocumentIdentifier(uri="file://return.markup_content"), - position=Position(line=0, character=0), - ), - ).result() - - assert response - - assert response.contents.kind == MarkupKind.Markdown - assert response.contents.value == "value" - - assert response.range.start.line == 0 - assert response.range.start.character == 0 - assert response.range.end.line == 1 - assert response.range.end.character == 1 From fed6353e6ee2b5b73b8a7930369b9abf20e5af5e Mon Sep 17 00:00:00 2001 From: Alex Carney Date: Sun, 17 Mar 2024 18:32:09 +0000 Subject: [PATCH 4/8] test: add end-to-end tests for color related methods --- examples/servers/colors.py | 85 ++++++++++++++++++ examples/servers/workspace/colors.txt | 12 +++ tests/e2e/test_colors.py | 123 ++++++++++++++++++++++++++ tests/lsp/test_color_presentation.py | 103 --------------------- tests/lsp/test_document_color.py | 81 ----------------- 5 files changed, 220 insertions(+), 184 deletions(-) create mode 100644 examples/servers/colors.py create mode 100644 examples/servers/workspace/colors.txt create mode 100644 tests/e2e/test_colors.py delete mode 100644 tests/lsp/test_color_presentation.py delete mode 100644 tests/lsp/test_document_color.py diff --git a/examples/servers/colors.py b/examples/servers/colors.py new file mode 100644 index 00000000..2ae6a0bc --- /dev/null +++ b/examples/servers/colors.py @@ -0,0 +1,85 @@ +############################################################################ +# Copyright(c) Open Law Library. All rights reserved. # +# See ThirdPartyNotices.txt in the project root for additional notices. # +# # +# Licensed under the Apache License, Version 2.0 (the "License") # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http: // www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +############################################################################ +import logging +import re + +from lsprotocol import types + +from pygls.server import LanguageServer + +COLOR = re.compile(r"""\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})(?!\w)""") +server = LanguageServer("color-server", "v1") + + +@server.feature( + types.TEXT_DOCUMENT_DOCUMENT_COLOR, +) +def document_color(params: types.CodeActionParams): + """Return a list of colors declared in the document.""" + items = [] + document_uri = params.text_document.uri + document = server.workspace.get_text_document(document_uri) + + for linum, line in enumerate(document.lines): + for match in COLOR.finditer(line.strip()): + start_char, end_char = match.span() + + # Is this a short form color? + if (end_char - start_char) == 4: + color = "".join(c * 2 for c in match.group(1)) + value = int(color, 16) + else: + value = int(match.group(1), 16) + + # Split the single color value into a value for each color channel. + blue = (value & 0xFF) / 0xFF + green = (value & (0xFF << 8)) / (0xFF << 8) + red = (value & (0xFF << 16)) / (0xFF << 16) + + items.append( + types.ColorInformation( + color=types.Color(red=red, green=green, blue=blue, alpha=1.0), + range=types.Range( + start=types.Position(line=linum, character=start_char), + end=types.Position(line=linum, character=end_char), + ), + ) + ) + + return items + + +@server.feature( + types.TEXT_DOCUMENT_COLOR_PRESENTATION, +) +def color_presentation(params: types.ColorPresentationParams): + """Given a color, instruct the client how to insert the representation of that + color into the document""" + color = params.color + + b = int(color.blue * 255) + g = int(color.green * 255) + r = int(color.red * 255) + + # Combine each color channel into a single value + value = (r << 16) | (g << 8) | b + return [types.ColorPresentation(label=f"#{value:0{6}x}")] + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO, format="%(message)s") + server.start_io() diff --git a/examples/servers/workspace/colors.txt b/examples/servers/workspace/colors.txt new file mode 100644 index 00000000..da6aca79 --- /dev/null +++ b/examples/servers/workspace/colors.txt @@ -0,0 +1,12 @@ +red is #ff0000 green is #00ff00 and blue is #0000ff +some more colors are below + +yellow #ffff00 + +pink #ff00ff + +cyan #00ffff + +short form colors are recognised too, e.g. #f00, #0f0, #00f + +hover over a color to reveal a color picker! diff --git a/tests/e2e/test_colors.py b/tests/e2e/test_colors.py new file mode 100644 index 00000000..ac169883 --- /dev/null +++ b/tests/e2e/test_colors.py @@ -0,0 +1,123 @@ +############################################################################ +# Copyright(c) Open Law Library. All rights reserved. # +# See ThirdPartyNotices.txt in the project root for additional notices. # +# # +# Licensed under the Apache License, Version 2.0 (the "License") # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http: // www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +############################################################################ +from __future__ import annotations + +import typing + +import pytest +import pytest_asyncio +from lsprotocol import types + +if typing.TYPE_CHECKING: + from typing import Tuple + + from pygls.lsp.client import BaseLanguageClient + + +@pytest_asyncio.fixture(scope="module") +async def colors(get_client_for): + async for result in get_client_for("colors.py"): + yield result + + +def range_from_str(range_: str) -> types.Range: + start, end = range_.split("-") + start_line, start_char = start.split(":") + end_line, end_char = end.split(":") + + return types.Range( + start=types.Position(line=int(start_line), character=int(start_char)), + end=types.Position(line=int(end_line), character=int(end_char)), + ) + + +@pytest.mark.asyncio(scope="module") +async def test_document_color( + colors: Tuple[BaseLanguageClient, types.InitializeResult], uri_for +): + """Ensure that the example colors server is working as expected.""" + client, initialize_result = colors + + colors_options = initialize_result.capabilities.color_provider + assert colors_options is True + + test_uri = uri_for("colors.txt") + response = await client.text_document_document_color_async( + types.DocumentColorParams( + text_document=types.TextDocumentIdentifier(uri=test_uri) + ) + ) + + assert response == [ + types.ColorInformation( + range=range_from_str("0:7-0:14"), + color=types.Color(red=1.0, green=0.0, blue=0.0, alpha=1.0), + ), + types.ColorInformation( + range=range_from_str("0:24-0:31"), + color=types.Color(red=0.0, green=1.0, blue=0.0, alpha=1.0), + ), + types.ColorInformation( + range=range_from_str("0:44-0:51"), + color=types.Color(red=0.0, green=0.0, blue=1.0, alpha=1.0), + ), + types.ColorInformation( + range=range_from_str("3:7-3:14"), + color=types.Color(red=1.0, green=1.0, blue=0.0, alpha=1.0), + ), + types.ColorInformation( + range=range_from_str("5:5-5:12"), + color=types.Color(red=1.0, green=0.0, blue=1.0, alpha=1.0), + ), + types.ColorInformation( + range=range_from_str("7:5-7:12"), + color=types.Color(red=0.0, green=1.0, blue=1.0, alpha=1.0), + ), + types.ColorInformation( + range=range_from_str("9:43-9:47"), + color=types.Color(red=1.0, green=0.0, blue=0.0, alpha=1.0), + ), + types.ColorInformation( + range=range_from_str("9:49-9:53"), + color=types.Color(red=0.0, green=1.0, blue=0.0, alpha=1.0), + ), + types.ColorInformation( + range=range_from_str("9:55-9:59"), + color=types.Color(red=0.0, green=0.0, blue=1.0, alpha=1.0), + ), + ] + + +@pytest.mark.asyncio(scope="module") +async def test_color_presentation( + colors: Tuple[BaseLanguageClient, types.InitializeResult], uri_for +): + """Ensure that the server can convert colors to their string representation + correctly.""" + + client, _ = colors + + test_uri = uri_for("colors.txt") + response = await client.text_document_color_presentation_async( + types.ColorPresentationParams( + text_document=types.TextDocumentIdentifier(uri=test_uri), + color=types.Color(red=0.25, green=0.5, blue=0.75, alpha=1.0), + range=range_from_str("0:7-0:14"), + ) + ) + + assert response == [types.ColorPresentation(label="#3f7fbf")] diff --git a/tests/lsp/test_color_presentation.py b/tests/lsp/test_color_presentation.py deleted file mode 100644 index 6748e66f..00000000 --- a/tests/lsp/test_color_presentation.py +++ /dev/null @@ -1,103 +0,0 @@ -############################################################################ -# Copyright(c) Open Law Library. All rights reserved. # -# See ThirdPartyNotices.txt in the project root for additional notices. # -# # -# Licensed under the Apache License, Version 2.0 (the "License") # -# you may not use this file except in compliance with the License. # -# You may obtain a copy of the License at # -# # -# http: // www.apache.org/licenses/LICENSE-2.0 # -# # -# Unless required by applicable law or agreed to in writing, software # -# distributed under the License is distributed on an "AS IS" BASIS, # -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # -# See the License for the specific language governing permissions and # -# limitations under the License. # -############################################################################ - -from typing import List - -from lsprotocol.types import TEXT_DOCUMENT_COLOR_PRESENTATION -from lsprotocol.types import ( - Color, - ColorPresentation, - ColorPresentationParams, - Position, - Range, - TextDocumentIdentifier, - TextEdit, -) - -from ..conftest import ClientServer - - -class ConfiguredLS(ClientServer): - def __init__(self): - super().__init__() - - @self.server.feature(TEXT_DOCUMENT_COLOR_PRESENTATION) - def f(params: ColorPresentationParams) -> List[ColorPresentation]: - return [ - ColorPresentation( - label="label1", - text_edit=TextEdit( - range=Range( - start=Position(line=0, character=0), - end=Position(line=1, character=1), - ), - new_text="te", - ), - additional_text_edits=[ - TextEdit( - range=Range( - start=Position(line=1, character=1), - end=Position(line=2, character=2), - ), - new_text="ate1", - ), - TextEdit( - range=Range( - start=Position(line=2, character=2), - end=Position(line=3, character=3), - ), - new_text="ate2", - ), - ], - ) - ] - - -@ConfiguredLS.decorate() -def test_color_presentation(client_server): - client, _ = client_server - response = client.lsp.send_request( - TEXT_DOCUMENT_COLOR_PRESENTATION, - ColorPresentationParams( - text_document=TextDocumentIdentifier(uri="file://return.list"), - color=Color(red=0.6, green=0.2, blue=0.3, alpha=0.5), - range=Range( - start=Position(line=0, character=0), - end=Position(line=3, character=3), - ), - ), - ).result() - - assert response[0].label == "label1" - assert response[0].text_edit.new_text == "te" - - assert response[0].text_edit.range.start.line == 0 - assert response[0].text_edit.range.start.character == 0 - assert response[0].text_edit.range.end.line == 1 - assert response[0].text_edit.range.end.character == 1 - - range = response[0].additional_text_edits[0].range - assert range.start.line == 1 - assert range.start.character == 1 - assert range.end.line == 2 - assert range.end.character == 2 - - range = response[0].additional_text_edits[1].range - assert range.start.line == 2 - assert range.start.character == 2 - assert range.end.line == 3 - assert range.end.character == 3 diff --git a/tests/lsp/test_document_color.py b/tests/lsp/test_document_color.py deleted file mode 100644 index 460a60b4..00000000 --- a/tests/lsp/test_document_color.py +++ /dev/null @@ -1,81 +0,0 @@ -############################################################################ -# Copyright(c) Open Law Library. All rights reserved. # -# See ThirdPartyNotices.txt in the project root for additional notices. # -# # -# Licensed under the Apache License, Version 2.0 (the "License") # -# you may not use this file except in compliance with the License. # -# You may obtain a copy of the License at # -# # -# http: // www.apache.org/licenses/LICENSE-2.0 # -# # -# Unless required by applicable law or agreed to in writing, software # -# distributed under the License is distributed on an "AS IS" BASIS, # -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # -# See the License for the specific language governing permissions and # -# limitations under the License. # -############################################################################ - -from typing import List - -from lsprotocol.types import TEXT_DOCUMENT_DOCUMENT_COLOR -from lsprotocol.types import ( - Color, - ColorInformation, - DocumentColorOptions, - DocumentColorParams, - Position, - Range, - TextDocumentIdentifier, -) - -from ..conftest import ClientServer - - -class ConfiguredLS(ClientServer): - def __init__(self): - super().__init__() - - @self.server.feature( - TEXT_DOCUMENT_DOCUMENT_COLOR, - DocumentColorOptions(), - ) - def f(params: DocumentColorParams) -> List[ColorInformation]: - return [ - ColorInformation( - range=Range( - start=Position(line=0, character=0), - end=Position(line=1, character=1), - ), - color=Color(red=0.5, green=0.5, blue=0.5, alpha=0.5), - ) - ] - - -@ConfiguredLS.decorate() -def test_capabilities(client_server): - _, server = client_server - capabilities = server.server_capabilities - - assert capabilities.color_provider - - -@ConfiguredLS.decorate() -def test_document_color(client_server): - client, _ = client_server - response = client.lsp.send_request( - TEXT_DOCUMENT_DOCUMENT_COLOR, - DocumentColorParams( - text_document=TextDocumentIdentifier(uri="file://return.list") - ), - ).result() - - assert response - assert response[0].color.red == 0.5 - assert response[0].color.green == 0.5 - assert response[0].color.blue == 0.5 - assert response[0].color.alpha == 0.5 - - assert response[0].range.start.line == 0 - assert response[0].range.start.character == 0 - assert response[0].range.end.line == 1 - assert response[0].range.end.character == 1 From cfb2d802df728f7f162b25bbca0bba67a7ccb4fb Mon Sep 17 00:00:00 2001 From: Alex Carney Date: Sun, 17 Mar 2024 18:33:22 +0000 Subject: [PATCH 5/8] test: add end-to-end tests for various "Goto X" methods --- examples/servers/goto.py | 194 ++++++++++++++++++++++++++++ examples/servers/workspace/code.txt | 6 + pygls/capabilities.py | 6 +- tests/e2e/test_declaration.py | 78 +++++++++++ tests/e2e/test_definition.py | 78 +++++++++++ tests/e2e/test_implementation.py | 78 +++++++++++ tests/e2e/test_references.py | 87 +++++++++++++ tests/e2e/test_type_definition.py | 78 +++++++++++ tests/lsp/test_declaration.py | 161 ----------------------- tests/lsp/test_definition.py | 164 ----------------------- tests/lsp/test_implementation.py | 164 ----------------------- tests/lsp/test_references.py | 103 --------------- tests/lsp/test_type_definition.py | 163 ----------------------- 13 files changed, 601 insertions(+), 759 deletions(-) create mode 100644 examples/servers/goto.py create mode 100644 examples/servers/workspace/code.txt create mode 100644 tests/e2e/test_declaration.py create mode 100644 tests/e2e/test_definition.py create mode 100644 tests/e2e/test_implementation.py create mode 100644 tests/e2e/test_references.py create mode 100644 tests/e2e/test_type_definition.py delete mode 100644 tests/lsp/test_declaration.py delete mode 100644 tests/lsp/test_definition.py delete mode 100644 tests/lsp/test_implementation.py delete mode 100644 tests/lsp/test_references.py delete mode 100644 tests/lsp/test_type_definition.py diff --git a/examples/servers/goto.py b/examples/servers/goto.py new file mode 100644 index 00000000..d2548d90 --- /dev/null +++ b/examples/servers/goto.py @@ -0,0 +1,194 @@ +############################################################################ +# Copyright(c) Open Law Library. All rights reserved. # +# See ThirdPartyNotices.txt in the project root for additional notices. # +# # +# Licensed under the Apache License, Version 2.0 (the "License") # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http: // www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +############################################################################ +import logging +import re + +from lsprotocol import types + +from pygls.server import LanguageServer +from pygls.workspace import TextDocument + +ARGUMENT = re.compile(r"(?P\w+): (?P\w+)") +FUNCTION = re.compile(r"^fn ([a-z]\w+)\(") +TYPE = re.compile(r"^type ([A-Z]\w+)\(") + + +class GotoLanguageServer(LanguageServer): + """Language server demonstrating the various "Goto X" methods in the LSP + specification.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.index = {} + + def parse(self, doc: TextDocument): + typedefs = {} + funcs = {} + + for linum, line in enumerate(doc.lines): + if (match := TYPE.match(line)) is not None: + name = match.group(1) + start_char = match.start() + line.find(name) + + typedefs[name] = types.Range( + start=types.Position(line=linum, character=start_char), + end=types.Position(line=linum, character=start_char + len(name)), + ) + + elif (match := FUNCTION.match(line)) is not None: + name = match.group(1) + start_char = match.start() + line.find(name) + + funcs[name] = types.Range( + start=types.Position(line=linum, character=start_char), + end=types.Position(line=linum, character=start_char + len(name)), + ) + + self.index[doc.uri] = { + "types": typedefs, + "functions": funcs, + } + logging.info("Index: %s", self.index) + + +server = GotoLanguageServer("goto-server", "v1") + + +@server.feature(types.TEXT_DOCUMENT_DID_OPEN) +def did_open(ls: GotoLanguageServer, params: types.DidOpenTextDocumentParams): + """Parse each document when it is opened""" + doc = ls.workspace.get_text_document(params.text_document.uri) + ls.parse(doc) + + +@server.feature(types.TEXT_DOCUMENT_DID_CHANGE) +def did_change(ls: GotoLanguageServer, params: types.DidOpenTextDocumentParams): + """Parse each document when it is changed""" + doc = ls.workspace.get_text_document(params.text_document.uri) + ls.parse(doc) + + +@server.feature(types.TEXT_DOCUMENT_TYPE_DEFINITION) +def goto_type_definition(ls: GotoLanguageServer, params: types.TypeDefinitionParams): + """Jump to an object's type definition.""" + doc = ls.workspace.get_text_document(params.text_document.uri) + index = ls.index.get(doc.uri) + if index is None: + return + + try: + line = doc.lines[params.position.line] + except IndexError: + line = "" + + word = doc.word_at_position(params.position) + + for match in ARGUMENT.finditer(line): + if match.group("name") == word: + if (range_ := index["types"].get(match.group("type"), None)) is not None: + return types.Location(uri=doc.uri, range=range_) + + +@server.feature(types.TEXT_DOCUMENT_DEFINITION) +def goto_definition(ls: GotoLanguageServer, params: types.DefinitionParams): + """Jump to an object's definition.""" + doc = ls.workspace.get_text_document(params.text_document.uri) + index = ls.index.get(doc.uri) + if index is None: + return + + word = doc.word_at_position(params.position) + + # Is word a type? + if (range_ := index["types"].get(word, None)) is not None: + return types.Location(uri=doc.uri, range=range_) + + +@server.feature(types.TEXT_DOCUMENT_DECLARATION) +def goto_declaration(ls: GotoLanguageServer, params: types.DeclarationParams): + """Jump to an object's declaration.""" + doc = ls.workspace.get_text_document(params.text_document.uri) + index = ls.index.get(doc.uri) + if index is None: + return + + try: + line = doc.lines[params.position.line] + except IndexError: + line = "" + + word = doc.word_at_position(params.position) + + for match in ARGUMENT.finditer(line): + if match.group("name") == word: + linum = params.position.line + return types.Location( + uri=doc.uri, + range=types.Range( + start=types.Position(line=linum, character=match.start()), + end=types.Position(line=linum, character=match.end()), + ), + ) + + +@server.feature(types.TEXT_DOCUMENT_IMPLEMENTATION) +def goto_implementation(ls: GotoLanguageServer, params: types.ImplementationParams): + """Jump to an object's implementation.""" + doc = ls.workspace.get_text_document(params.text_document.uri) + index = ls.index.get(doc.uri) + if index is None: + return + + word = doc.word_at_position(params.position) + + # Is word a function? + if (range_ := index["functions"].get(word, None)) is not None: + return types.Location(uri=doc.uri, range=range_) + + +@server.feature(types.TEXT_DOCUMENT_REFERENCES) +def find_references(ls: GotoLanguageServer, params: types.ReferenceParams): + """Find references of an object.""" + doc = ls.workspace.get_text_document(params.text_document.uri) + index = ls.index.get(doc.uri) + if index is None: + return + + word = doc.word_at_position(params.position) + is_object = any([word in index[name] for name in index]) + if not is_object: + return + + references = [] + for linum, line in enumerate(doc.lines): + for match in re.finditer(f"\\b{word}\\b", line): + references.append( + types.Location( + uri=doc.uri, + range=types.Range( + start=types.Position(line=linum, character=match.start()), + end=types.Position(line=linum, character=match.end()), + ), + ) + ) + + return references + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO, format="%(message)s") + server.start_io() diff --git a/examples/servers/workspace/code.txt b/examples/servers/workspace/code.txt new file mode 100644 index 00000000..022eefd1 --- /dev/null +++ b/examples/servers/workspace/code.txt @@ -0,0 +1,6 @@ +type Rectangle(x, y, w, h) = {x: x, y: y, width: w, height: h } +type Square(x, y, s) = Rectangle(x, y, s, s) + +fn area(rect: Rectangle) -> rect.width * rect.height + +fn volume(rect: Rectangle, length: float) -> area(rect) * length diff --git a/pygls/capabilities.py b/pygls/capabilities.py index 38bf7ba0..975e5e24 100644 --- a/pygls/capabilities.py +++ b/pygls/capabilities.py @@ -145,7 +145,7 @@ def _with_definition(self): def _with_type_definition(self): value = self._provider_options( - types.TEXT_DOCUMENT_TYPE_DEFINITION, default=types.TypeDefinitionOptions() + types.TEXT_DOCUMENT_TYPE_DEFINITION, default=True ) if value is not None: self.server_cap.type_definition_provider = value @@ -161,9 +161,7 @@ def _with_inlay_hints(self): return self def _with_implementation(self): - value = self._provider_options( - types.TEXT_DOCUMENT_IMPLEMENTATION, default=types.ImplementationOptions() - ) + value = self._provider_options(types.TEXT_DOCUMENT_IMPLEMENTATION, default=True) if value is not None: self.server_cap.implementation_provider = value return self diff --git a/tests/e2e/test_declaration.py b/tests/e2e/test_declaration.py new file mode 100644 index 00000000..efb521dc --- /dev/null +++ b/tests/e2e/test_declaration.py @@ -0,0 +1,78 @@ +############################################################################ +# Copyright(c) Open Law Library. All rights reserved. # +# See ThirdPartyNotices.txt in the project root for additional notices. # +# # +# Licensed under the Apache License, Version 2.0 (the "License") # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http: // www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +############################################################################ +from __future__ import annotations + +import typing + +import pytest_asyncio +from lsprotocol import types + +if typing.TYPE_CHECKING: + from typing import Tuple + + from pygls.lsp.client import BaseLanguageClient + + +@pytest_asyncio.fixture() +async def goto(get_client_for): + async for result in get_client_for("goto.py"): + yield result + + +async def test_declaration( + goto: Tuple[BaseLanguageClient, types.InitializeResult], path_for, uri_for +): + """Ensure that we can implement declaration requests.""" + client, initialize_result = goto + + declaration_options = initialize_result.capabilities.declaration_provider + assert declaration_options is True + + test_uri = uri_for("code.txt") + test_path = path_for("code.txt") + + client.text_document_did_open( + types.DidOpenTextDocumentParams( + types.TextDocumentItem( + uri=test_uri, + language_id="plaintext", + version=0, + text=test_path.read_text(), + ) + ) + ) + + response = await client.text_document_declaration_async( + types.DeclarationParams( + text_document=types.TextDocumentIdentifier(uri=test_uri), + position=types.Position(line=6, character=47), + ) + ) + assert response is None + + response = await client.text_document_declaration_async( + types.DeclarationParams( + text_document=types.TextDocumentIdentifier(uri=test_uri), + position=types.Position(line=5, character=52), + ) + ) + assert isinstance(response, types.Location) + assert response.uri == test_uri + assert response.range == types.Range( + start=types.Position(line=5, character=10), + end=types.Position(line=5, character=25), + ) diff --git a/tests/e2e/test_definition.py b/tests/e2e/test_definition.py new file mode 100644 index 00000000..cf9088ce --- /dev/null +++ b/tests/e2e/test_definition.py @@ -0,0 +1,78 @@ +############################################################################ +# Copyright(c) Open Law Library. All rights reserved. # +# See ThirdPartyNotices.txt in the project root for additional notices. # +# # +# Licensed under the Apache License, Version 2.0 (the "License") # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http: // www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +############################################################################ +from __future__ import annotations + +import typing + +import pytest_asyncio +from lsprotocol import types + +if typing.TYPE_CHECKING: + from typing import Tuple + + from pygls.lsp.client import BaseLanguageClient + + +@pytest_asyncio.fixture() +async def goto(get_client_for): + async for result in get_client_for("goto.py"): + yield result + + +async def test_definition( + goto: Tuple[BaseLanguageClient, types.InitializeResult], path_for, uri_for +): + """Ensure that we can implement type definition requests.""" + client, initialize_result = goto + + definition_options = initialize_result.capabilities.definition_provider + assert definition_options is True + + test_uri = uri_for("code.txt") + test_path = path_for("code.txt") + + client.text_document_did_open( + types.DidOpenTextDocumentParams( + types.TextDocumentItem( + uri=test_uri, + language_id="plaintext", + version=0, + text=test_path.read_text(), + ) + ) + ) + + response = await client.text_document_definition_async( + types.DefinitionParams( + text_document=types.TextDocumentIdentifier(uri=test_uri), + position=types.Position(line=6, character=47), + ) + ) + assert response is None + + response = await client.text_document_definition_async( + types.DefinitionParams( + text_document=types.TextDocumentIdentifier(uri=test_uri), + position=types.Position(line=5, character=20), + ) + ) + assert isinstance(response, types.Location) + assert response.uri == test_uri + assert response.range == types.Range( + start=types.Position(line=0, character=5), + end=types.Position(line=0, character=14), + ) diff --git a/tests/e2e/test_implementation.py b/tests/e2e/test_implementation.py new file mode 100644 index 00000000..6a10689a --- /dev/null +++ b/tests/e2e/test_implementation.py @@ -0,0 +1,78 @@ +############################################################################ +# Copyright(c) Open Law Library. All rights reserved. # +# See ThirdPartyNotices.txt in the project root for additional notices. # +# # +# Licensed under the Apache License, Version 2.0 (the "License") # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http: // www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +############################################################################ +from __future__ import annotations + +import typing + +import pytest_asyncio +from lsprotocol import types + +if typing.TYPE_CHECKING: + from typing import Tuple + + from pygls.lsp.client import BaseLanguageClient + + +@pytest_asyncio.fixture() +async def goto(get_client_for): + async for result in get_client_for("goto.py"): + yield result + + +async def test_implementation( + goto: Tuple[BaseLanguageClient, types.InitializeResult], path_for, uri_for +): + """Ensure that we can implement type implementation requests.""" + client, initialize_result = goto + + implementation_options = initialize_result.capabilities.implementation_provider + assert implementation_options is True + + test_uri = uri_for("code.txt") + test_path = path_for("code.txt") + + client.text_document_did_open( + types.DidOpenTextDocumentParams( + types.TextDocumentItem( + uri=test_uri, + language_id="plaintext", + version=0, + text=test_path.read_text(), + ) + ) + ) + + response = await client.text_document_implementation_async( + types.ImplementationParams( + text_document=types.TextDocumentIdentifier(uri=test_uri), + position=types.Position(line=6, character=47), + ) + ) + assert response is None + + response = await client.text_document_implementation_async( + types.ImplementationParams( + text_document=types.TextDocumentIdentifier(uri=test_uri), + position=types.Position(line=5, character=46), + ) + ) + assert isinstance(response, types.Location) + assert response.uri == test_uri + assert response.range == types.Range( + start=types.Position(line=3, character=3), + end=types.Position(line=3, character=7), + ) diff --git a/tests/e2e/test_references.py b/tests/e2e/test_references.py new file mode 100644 index 00000000..2f9718f4 --- /dev/null +++ b/tests/e2e/test_references.py @@ -0,0 +1,87 @@ +############################################################################ +# Copyright(c) Open Law Library. All rights reserved. # +# See ThirdPartyNotices.txt in the project root for additional notices. # +# # +# Licensed under the Apache License, Version 2.0 (the "License") # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http: // www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +############################################################################ +from __future__ import annotations + +import typing + +import pytest_asyncio +from lsprotocol import types + +if typing.TYPE_CHECKING: + from typing import Tuple + + from pygls.lsp.client import BaseLanguageClient + + +@pytest_asyncio.fixture() +async def goto(get_client_for): + async for result in get_client_for("goto.py"): + yield result + + +async def test_type_definition( + goto: Tuple[BaseLanguageClient, types.InitializeResult], path_for, uri_for +): + """Ensure that we can implement type definition requests.""" + client, initialize_result = goto + + reference_options = initialize_result.capabilities.references_provider + assert reference_options is True + + test_uri = uri_for("code.txt") + test_path = path_for("code.txt") + + client.text_document_did_open( + types.DidOpenTextDocumentParams( + types.TextDocumentItem( + uri=test_uri, + language_id="plaintext", + version=0, + text=test_path.read_text(), + ) + ) + ) + + response = await client.text_document_references_async( + types.ReferenceParams( + context=types.ReferenceContext(include_declaration=True), + text_document=types.TextDocumentIdentifier(uri=test_uri), + position=types.Position(line=0, character=0), + ) + ) + assert response is None + + response = await client.text_document_references_async( + types.ReferenceParams( + context=types.ReferenceContext(include_declaration=True), + text_document=types.TextDocumentIdentifier(uri=test_uri), + position=types.Position(line=3, character=5), + ) + ) + assert len(response) == 2 + + assert response[0].uri == test_uri + assert response[0].range == types.Range( + start=types.Position(line=3, character=3), + end=types.Position(line=3, character=7), + ) + + assert response[1].uri == test_uri + assert response[1].range == types.Range( + start=types.Position(line=5, character=45), + end=types.Position(line=5, character=49), + ) diff --git a/tests/e2e/test_type_definition.py b/tests/e2e/test_type_definition.py new file mode 100644 index 00000000..36a673a6 --- /dev/null +++ b/tests/e2e/test_type_definition.py @@ -0,0 +1,78 @@ +############################################################################ +# Copyright(c) Open Law Library. All rights reserved. # +# See ThirdPartyNotices.txt in the project root for additional notices. # +# # +# Licensed under the Apache License, Version 2.0 (the "License") # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http: // www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +############################################################################ +from __future__ import annotations + +import typing + +import pytest_asyncio +from lsprotocol import types + +if typing.TYPE_CHECKING: + from typing import Tuple + + from pygls.lsp.client import BaseLanguageClient + + +@pytest_asyncio.fixture() +async def goto(get_client_for): + async for result in get_client_for("goto.py"): + yield result + + +async def test_type_definition( + goto: Tuple[BaseLanguageClient, types.InitializeResult], path_for, uri_for +): + """Ensure that we can implement type definition requests.""" + client, initialize_result = goto + + type_definition_options = initialize_result.capabilities.type_definition_provider + assert type_definition_options is True + + test_uri = uri_for("code.txt") + test_path = path_for("code.txt") + + client.text_document_did_open( + types.DidOpenTextDocumentParams( + types.TextDocumentItem( + uri=test_uri, + language_id="plaintext", + version=0, + text=test_path.read_text(), + ) + ) + ) + + response = await client.text_document_type_definition_async( + types.TypeDefinitionParams( + text_document=types.TextDocumentIdentifier(uri=test_uri), + position=types.Position(line=6, character=47), + ) + ) + assert response is None + + response = await client.text_document_type_definition_async( + types.TypeDefinitionParams( + text_document=types.TextDocumentIdentifier(uri=test_uri), + position=types.Position(line=5, character=52), + ) + ) + assert isinstance(response, types.Location) + assert response.uri == test_uri + assert response.range == types.Range( + start=types.Position(line=0, character=5), + end=types.Position(line=0, character=14), + ) diff --git a/tests/lsp/test_declaration.py b/tests/lsp/test_declaration.py deleted file mode 100644 index 221982d6..00000000 --- a/tests/lsp/test_declaration.py +++ /dev/null @@ -1,161 +0,0 @@ -############################################################################ -# Copyright(c) Open Law Library. All rights reserved. # -# See ThirdPartyNotices.txt in the project root for additional notices. # -# # -# Licensed under the Apache License, Version 2.0 (the "License") # -# you may not use this file except in compliance with the License. # -# You may obtain a copy of the License at # -# # -# http: // www.apache.org/licenses/LICENSE-2.0 # -# # -# Unless required by applicable law or agreed to in writing, software # -# distributed under the License is distributed on an "AS IS" BASIS, # -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # -# See the License for the specific language governing permissions and # -# limitations under the License. # -############################################################################ - -from typing import List, Optional, Union - -from lsprotocol.types import TEXT_DOCUMENT_DECLARATION -from lsprotocol.types import ( - DeclarationOptions, - DeclarationParams, - Location, - LocationLink, - Position, - Range, - TextDocumentIdentifier, -) - -from ..conftest import ClientServer - - -class ConfiguredLS(ClientServer): - def __init__(self): - super().__init__() - - @self.server.feature(TEXT_DOCUMENT_DECLARATION, DeclarationOptions()) - def f( - params: DeclarationParams, - ) -> Optional[Union[Location, List[Location], List[LocationLink]]]: - location = Location( - uri="uri", - range=Range( - start=Position(line=0, character=0), - end=Position(line=1, character=1), - ), - ) - - location_link = LocationLink( - target_uri="uri", - target_range=Range( - start=Position(line=0, character=0), - end=Position(line=1, character=1), - ), - target_selection_range=Range( - start=Position(line=0, character=0), - end=Position(line=2, character=2), - ), - origin_selection_range=Range( - start=Position(line=0, character=0), - end=Position(line=3, character=3), - ), - ) - - return { # type: ignore - "file://return.location": location, - "file://return.location_list": [location], - "file://return.location_link_list": [location_link], - }.get(params.text_document.uri, None) - - -@ConfiguredLS.decorate() -def test_capabilities(client_server): - _, server = client_server - capabilities = server.server_capabilities - - assert capabilities.declaration_provider - - -@ConfiguredLS.decorate() -def test_declaration_return_location(client_server): - client, _ = client_server - response = client.lsp.send_request( - TEXT_DOCUMENT_DECLARATION, - DeclarationParams( - text_document=TextDocumentIdentifier(uri="file://return.location"), - position=Position(line=0, character=0), - ), - ).result() - - assert response.uri == "uri" - - assert response.range.start.line == 0 - assert response.range.start.character == 0 - assert response.range.end.line == 1 - assert response.range.end.character == 1 - - -@ConfiguredLS.decorate() -def test_declaration_return_location_list(client_server): - client, _ = client_server - response = client.lsp.send_request( - TEXT_DOCUMENT_DECLARATION, - DeclarationParams( - text_document=TextDocumentIdentifier(uri="file://return.location_list"), - position=Position(line=0, character=0), - ), - ).result() - - assert response[0].uri == "uri" - - assert response[0].range.start.line == 0 - assert response[0].range.start.character == 0 - assert response[0].range.end.line == 1 - assert response[0].range.end.character == 1 - - -@ConfiguredLS.decorate() -def test_declaration_return_location_link_list(client_server): - client, _ = client_server - response = client.lsp.send_request( - TEXT_DOCUMENT_DECLARATION, - DeclarationParams( - text_document=TextDocumentIdentifier( - uri="file://return.location_link_list" - ), - position=Position(line=0, character=0), - ), - ).result() - - assert response[0].target_uri == "uri" - - assert response[0].target_range.start.line == 0 - assert response[0].target_range.start.character == 0 - assert response[0].target_range.end.line == 1 - assert response[0].target_range.end.character == 1 - - assert response[0].target_selection_range.start.line == 0 - assert response[0].target_selection_range.start.character == 0 - assert response[0].target_selection_range.end.line == 2 - assert response[0].target_selection_range.end.character == 2 - - assert response[0].origin_selection_range.start.line == 0 - assert response[0].origin_selection_range.start.character == 0 - assert response[0].origin_selection_range.end.line == 3 - assert response[0].origin_selection_range.end.character == 3 - - -@ConfiguredLS.decorate() -def test_declaration_return_none(client_server): - client, _ = client_server - response = client.lsp.send_request( - TEXT_DOCUMENT_DECLARATION, - DeclarationParams( - text_document=TextDocumentIdentifier(uri="file://return.none"), - position=Position(line=0, character=0), - ), - ).result() - - assert response is None diff --git a/tests/lsp/test_definition.py b/tests/lsp/test_definition.py deleted file mode 100644 index 3ed2f964..00000000 --- a/tests/lsp/test_definition.py +++ /dev/null @@ -1,164 +0,0 @@ -############################################################################ -# Copyright(c) Open Law Library. All rights reserved. # -# See ThirdPartyNotices.txt in the project root for additional notices. # -# # -# Licensed under the Apache License, Version 2.0 (the "License") # -# you may not use this file except in compliance with the License. # -# You may obtain a copy of the License at # -# # -# http: // www.apache.org/licenses/LICENSE-2.0 # -# # -# Unless required by applicable law or agreed to in writing, software # -# distributed under the License is distributed on an "AS IS" BASIS, # -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # -# See the License for the specific language governing permissions and # -# limitations under the License. # -############################################################################ - -from typing import List, Optional, Union - -from lsprotocol.types import TEXT_DOCUMENT_DEFINITION -from lsprotocol.types import ( - DefinitionOptions, - DefinitionParams, - Location, - LocationLink, - Position, - Range, - TextDocumentIdentifier, -) - -from ..conftest import ClientServer - - -class ConfiguredLS(ClientServer): - def __init__(self): - super().__init__() - - @self.server.feature( - TEXT_DOCUMENT_DEFINITION, - DefinitionOptions(), - ) - def f( - params: DefinitionParams, - ) -> Optional[Union[Location, List[Location], List[LocationLink]]]: - location = Location( - uri="uri", - range=Range( - start=Position(line=0, character=0), - end=Position(line=1, character=1), - ), - ) - - location_link = LocationLink( - target_uri="uri", - target_range=Range( - start=Position(line=0, character=0), - end=Position(line=1, character=1), - ), - target_selection_range=Range( - start=Position(line=0, character=0), - end=Position(line=2, character=2), - ), - origin_selection_range=Range( - start=Position(line=0, character=0), - end=Position(line=3, character=3), - ), - ) - - return { # type: ignore - "file://return.location": location, - "file://return.location_list": [location], - "file://return.location_link_list": [location_link], - }.get(params.text_document.uri, None) - - -@ConfiguredLS.decorate() -def test_capabilities(client_server): - _, server = client_server - capabilities = server.server_capabilities - - assert capabilities.definition_provider is not None - - -@ConfiguredLS.decorate() -def test_definition_return_location(client_server): - client, _ = client_server - response = client.lsp.send_request( - TEXT_DOCUMENT_DEFINITION, - DefinitionParams( - text_document=TextDocumentIdentifier(uri="file://return.location"), - position=Position(line=0, character=0), - ), - ).result() - - assert response.uri == "uri" - - assert response.range.start.line == 0 - assert response.range.start.character == 0 - assert response.range.end.line == 1 - assert response.range.end.character == 1 - - -@ConfiguredLS.decorate() -def test_definition_return_location_list(client_server): - client, _ = client_server - response = client.lsp.send_request( - TEXT_DOCUMENT_DEFINITION, - DefinitionParams( - text_document=TextDocumentIdentifier(uri="file://return.location_list"), - position=Position(line=0, character=0), - ), - ).result() - - assert response[0].uri == "uri" - - assert response[0].range.start.line == 0 - assert response[0].range.start.character == 0 - assert response[0].range.end.line == 1 - assert response[0].range.end.character == 1 - - -@ConfiguredLS.decorate() -def test_definition_return_location_link_list(client_server): - client, _ = client_server - response = client.lsp.send_request( - TEXT_DOCUMENT_DEFINITION, - DefinitionParams( - text_document=TextDocumentIdentifier( - uri="file://return.location_link_list" - ), - position=Position(line=0, character=0), - ), - ).result() - - assert response[0].target_uri == "uri" - - assert response[0].target_range.start.line == 0 - assert response[0].target_range.start.character == 0 - assert response[0].target_range.end.line == 1 - assert response[0].target_range.end.character == 1 - - assert response[0].target_selection_range.start.line == 0 - assert response[0].target_selection_range.start.character == 0 - assert response[0].target_selection_range.end.line == 2 - assert response[0].target_selection_range.end.character == 2 - - assert response[0].origin_selection_range.start.line == 0 - assert response[0].origin_selection_range.start.character == 0 - assert response[0].origin_selection_range.end.line == 3 - assert response[0].origin_selection_range.end.character == 3 - - -@ConfiguredLS.decorate() -def test_definition_return_none(client_server): - client, _ = client_server - response = client.lsp.send_request( - TEXT_DOCUMENT_DEFINITION, - DefinitionParams( - text_document=TextDocumentIdentifier(uri="file://return.none"), - position=Position(line=0, character=0), - ), - ).result() - - assert response is None diff --git a/tests/lsp/test_implementation.py b/tests/lsp/test_implementation.py deleted file mode 100644 index 4fea3a91..00000000 --- a/tests/lsp/test_implementation.py +++ /dev/null @@ -1,164 +0,0 @@ -############################################################################ -# Copyright(c) Open Law Library. All rights reserved. # -# See ThirdPartyNotices.txt in the project root for additional notices. # -# # -# Licensed under the Apache License, Version 2.0 (the "License") # -# you may not use this file except in compliance with the License. # -# You may obtain a copy of the License at # -# # -# http: // www.apache.org/licenses/LICENSE-2.0 # -# # -# Unless required by applicable law or agreed to in writing, software # -# distributed under the License is distributed on an "AS IS" BASIS, # -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # -# See the License for the specific language governing permissions and # -# limitations under the License. # -############################################################################ - -from typing import List, Optional, Union - -from lsprotocol.types import TEXT_DOCUMENT_IMPLEMENTATION -from lsprotocol.types import ( - ImplementationOptions, - ImplementationParams, - Location, - LocationLink, - Position, - Range, - TextDocumentIdentifier, -) - -from ..conftest import ClientServer - - -class ConfiguredLS(ClientServer): - def __init__(self): - super().__init__() - - @self.server.feature( - TEXT_DOCUMENT_IMPLEMENTATION, - ImplementationOptions(), - ) - def f( - params: ImplementationParams, - ) -> Optional[Union[Location, List[Location], List[LocationLink]]]: - location = Location( - uri="uri", - range=Range( - start=Position(line=0, character=0), - end=Position(line=1, character=1), - ), - ) - - location_link = LocationLink( - target_uri="uri", - target_range=Range( - start=Position(line=0, character=0), - end=Position(line=1, character=1), - ), - target_selection_range=Range( - start=Position(line=0, character=0), - end=Position(line=2, character=2), - ), - origin_selection_range=Range( - start=Position(line=0, character=0), - end=Position(line=3, character=3), - ), - ) - - return { # type: ignore - "file://return.location": location, - "file://return.location_list": [location], - "file://return.location_link_list": [location_link], - }.get(params.text_document.uri, None) - - -@ConfiguredLS.decorate() -def test_capabilities(client_server): - _, server = client_server - capabilities = server.server_capabilities - - assert capabilities.implementation_provider - - -@ConfiguredLS.decorate() -def test_type_definition_return_location(client_server): - client, _ = client_server - response = client.lsp.send_request( - TEXT_DOCUMENT_IMPLEMENTATION, - ImplementationParams( - text_document=TextDocumentIdentifier(uri="file://return.location"), - position=Position(line=0, character=0), - ), - ).result() - - assert response.uri == "uri" - - assert response.range.start.line == 0 - assert response.range.start.character == 0 - assert response.range.end.line == 1 - assert response.range.end.character == 1 - - -@ConfiguredLS.decorate() -def test_type_definition_return_location_list(client_server): - client, _ = client_server - response = client.lsp.send_request( - TEXT_DOCUMENT_IMPLEMENTATION, - ImplementationParams( - text_document=TextDocumentIdentifier(uri="file://return.location_list"), - position=Position(line=0, character=0), - ), - ).result() - - assert response[0].uri == "uri" - - assert response[0].range.start.line == 0 - assert response[0].range.start.character == 0 - assert response[0].range.end.line == 1 - assert response[0].range.end.character == 1 - - -@ConfiguredLS.decorate() -def test_type_definition_return_location_link_list(client_server): - client, _ = client_server - response = client.lsp.send_request( - TEXT_DOCUMENT_IMPLEMENTATION, - ImplementationParams( - text_document=TextDocumentIdentifier( - uri="file://return.location_link_list" - ), - position=Position(line=0, character=0), - ), - ).result() - - assert response[0].target_uri == "uri" - - assert response[0].target_range.start.line == 0 - assert response[0].target_range.start.character == 0 - assert response[0].target_range.end.line == 1 - assert response[0].target_range.end.character == 1 - - assert response[0].target_selection_range.start.line == 0 - assert response[0].target_selection_range.start.character == 0 - assert response[0].target_selection_range.end.line == 2 - assert response[0].target_selection_range.end.character == 2 - - assert response[0].origin_selection_range.start.line == 0 - assert response[0].origin_selection_range.start.character == 0 - assert response[0].origin_selection_range.end.line == 3 - assert response[0].origin_selection_range.end.character == 3 - - -@ConfiguredLS.decorate() -def test_type_definition_return_none(client_server): - client, _ = client_server - response = client.lsp.send_request( - TEXT_DOCUMENT_IMPLEMENTATION, - ImplementationParams( - text_document=TextDocumentIdentifier(uri="file://return.none"), - position=Position(line=0, character=0), - ), - ).result() - - assert response is None diff --git a/tests/lsp/test_references.py b/tests/lsp/test_references.py deleted file mode 100644 index 5867e35d..00000000 --- a/tests/lsp/test_references.py +++ /dev/null @@ -1,103 +0,0 @@ -############################################################################ -# Copyright(c) Open Law Library. All rights reserved. # -# See ThirdPartyNotices.txt in the project root for additional notices. # -# # -# Licensed under the Apache License, Version 2.0 (the "License") # -# you may not use this file except in compliance with the License. # -# You may obtain a copy of the License at # -# # -# http: // www.apache.org/licenses/LICENSE-2.0 # -# # -# Unless required by applicable law or agreed to in writing, software # -# distributed under the License is distributed on an "AS IS" BASIS, # -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # -# See the License for the specific language governing permissions and # -# limitations under the License. # -############################################################################ - -from typing import List, Optional - -from lsprotocol.types import TEXT_DOCUMENT_REFERENCES -from lsprotocol.types import ( - Location, - Position, - Range, - ReferenceContext, - ReferenceOptions, - ReferenceParams, - TextDocumentIdentifier, -) - -from ..conftest import ClientServer - - -class ConfiguredLS(ClientServer): - def __init__(self): - super().__init__() - - @self.server.feature( - TEXT_DOCUMENT_REFERENCES, - ReferenceOptions(), - ) - def f(params: ReferenceParams) -> Optional[List[Location]]: - if params.text_document.uri == "file://return.list": - return [ - Location( - uri="uri", - range=Range( - start=Position(line=0, character=0), - end=Position(line=1, character=1), - ), - ), - ] - else: - return None - - -@ConfiguredLS.decorate() -def test_capabilities(client_server): - _, server = client_server - capabilities = server.server_capabilities - - assert capabilities.references_provider - - -@ConfiguredLS.decorate() -def test_references_return_list(client_server): - client, _ = client_server - response = client.lsp.send_request( - TEXT_DOCUMENT_REFERENCES, - ReferenceParams( - text_document=TextDocumentIdentifier(uri="file://return.list"), - position=Position(line=0, character=0), - context=ReferenceContext( - include_declaration=True, - ), - ), - ).result() - - assert response - - assert response[0].uri == "uri" - - assert response[0].range.start.line == 0 - assert response[0].range.start.character == 0 - assert response[0].range.end.line == 1 - assert response[0].range.end.character == 1 - - -@ConfiguredLS.decorate() -def test_references_return_none(client_server): - client, _ = client_server - response = client.lsp.send_request( - TEXT_DOCUMENT_REFERENCES, - ReferenceParams( - text_document=TextDocumentIdentifier(uri="file://return.none"), - position=Position(line=0, character=0), - context=ReferenceContext( - include_declaration=True, - ), - ), - ).result() - - assert response is None diff --git a/tests/lsp/test_type_definition.py b/tests/lsp/test_type_definition.py deleted file mode 100644 index b6d3eff3..00000000 --- a/tests/lsp/test_type_definition.py +++ /dev/null @@ -1,163 +0,0 @@ -############################################################################ -# Copyright(c) Open Law Library. All rights reserved. # -# See ThirdPartyNotices.txt in the project root for additional notices. # -# # -# Licensed under the Apache License, Version 2.0 (the "License") # -# you may not use this file except in compliance with the License. # -# You may obtain a copy of the License at # -# # -# http: // www.apache.org/licenses/LICENSE-2.0 # -# # -# Unless required by applicable law or agreed to in writing, software # -# distributed under the License is distributed on an "AS IS" BASIS, # -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # -# See the License for the specific language governing permissions and # -# limitations under the License. # -############################################################################ -from typing import List, Optional, Union - -from lsprotocol.types import TEXT_DOCUMENT_TYPE_DEFINITION -from lsprotocol.types import ( - Location, - LocationLink, - Position, - Range, - TextDocumentIdentifier, - TypeDefinitionOptions, - TypeDefinitionParams, -) - -from ..conftest import ClientServer - - -class ConfiguredLS(ClientServer): - def __init__(self): - super().__init__() - - @self.server.feature( - TEXT_DOCUMENT_TYPE_DEFINITION, - TypeDefinitionOptions(), - ) - def f( - params: TypeDefinitionParams, - ) -> Optional[Union[Location, List[Location], List[LocationLink]]]: - location = Location( - uri="uri", - range=Range( - start=Position(line=0, character=0), - end=Position(line=1, character=1), - ), - ) - - location_link = LocationLink( - target_uri="uri", - target_range=Range( - start=Position(line=0, character=0), - end=Position(line=1, character=1), - ), - target_selection_range=Range( - start=Position(line=0, character=0), - end=Position(line=2, character=2), - ), - origin_selection_range=Range( - start=Position(line=0, character=0), - end=Position(line=3, character=3), - ), - ) - - return { # type: ignore - "file://return.location": location, - "file://return.location_list": [location], - "file://return.location_link_list": [location_link], - }.get(params.text_document.uri, None) - - -@ConfiguredLS.decorate() -def test_capabilities(client_server): - _, server = client_server - capabilities = server.server_capabilities - - assert capabilities.type_definition_provider - - -@ConfiguredLS.decorate() -def test_type_definition_return_location(client_server): - client, _ = client_server - response = client.lsp.send_request( - TEXT_DOCUMENT_TYPE_DEFINITION, - TypeDefinitionParams( - text_document=TextDocumentIdentifier(uri="file://return.location"), - position=Position(line=0, character=0), - ), - ).result() - - assert response.uri == "uri" - - assert response.range.start.line == 0 - assert response.range.start.character == 0 - assert response.range.end.line == 1 - assert response.range.end.character == 1 - - -@ConfiguredLS.decorate() -def test_type_definition_return_location_list(client_server): - client, _ = client_server - response = client.lsp.send_request( - TEXT_DOCUMENT_TYPE_DEFINITION, - TypeDefinitionParams( - text_document=TextDocumentIdentifier(uri="file://return.location_list"), - position=Position(line=0, character=0), - ), - ).result() - - assert response[0].uri == "uri" - - assert response[0].range.start.line == 0 - assert response[0].range.start.character == 0 - assert response[0].range.end.line == 1 - assert response[0].range.end.character == 1 - - -@ConfiguredLS.decorate() -def test_type_definition_return_location_link_list(client_server): - client, _ = client_server - response = client.lsp.send_request( - TEXT_DOCUMENT_TYPE_DEFINITION, - TypeDefinitionParams( - text_document=TextDocumentIdentifier( - uri="file://return.location_link_list" - ), - position=Position(line=0, character=0), - ), - ).result() - - assert response[0].target_uri == "uri" - - assert response[0].target_range.start.line == 0 - assert response[0].target_range.start.character == 0 - assert response[0].target_range.end.line == 1 - assert response[0].target_range.end.character == 1 - - assert response[0].target_selection_range.start.line == 0 - assert response[0].target_selection_range.start.character == 0 - assert response[0].target_selection_range.end.line == 2 - assert response[0].target_selection_range.end.character == 2 - - assert response[0].origin_selection_range.start.line == 0 - assert response[0].origin_selection_range.start.character == 0 - assert response[0].origin_selection_range.end.line == 3 - assert response[0].origin_selection_range.end.character == 3 - - -@ConfiguredLS.decorate() -def test_type_definition_return_none(client_server): - client, _ = client_server - response = client.lsp.send_request( - TEXT_DOCUMENT_TYPE_DEFINITION, - TypeDefinitionParams( - text_document=TextDocumentIdentifier(uri="file://return.none"), - position=Position(line=0, character=0), - ), - ).result() - - assert response is None From 15d523bec9efb3b4de6a3b551a32eb07bedc4198 Mon Sep 17 00:00:00 2001 From: Alex Carney Date: Sun, 17 Mar 2024 18:33:58 +0000 Subject: [PATCH 6/8] test: add end-to-end tests for `textDocument/codeLens` --- examples/servers/code_lens.py | 123 +++++++++++++++++++++++++++ tests/e2e/test_code_lens.py | 155 ++++++++++++++++++++++++++++++++++ tests/lsp/test_code_lens.py | 94 --------------------- 3 files changed, 278 insertions(+), 94 deletions(-) create mode 100644 examples/servers/code_lens.py create mode 100644 tests/e2e/test_code_lens.py delete mode 100644 tests/lsp/test_code_lens.py diff --git a/examples/servers/code_lens.py b/examples/servers/code_lens.py new file mode 100644 index 00000000..7a217028 --- /dev/null +++ b/examples/servers/code_lens.py @@ -0,0 +1,123 @@ +############################################################################ +# Copyright(c) Open Law Library. All rights reserved. # +# See ThirdPartyNotices.txt in the project root for additional notices. # +# # +# Licensed under the Apache License, Version 2.0 (the "License") # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http: // www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +############################################################################ +import logging +import re + +from lsprotocol import types + +from pygls.server import LanguageServer + +ADDITION = re.compile(r"^\s*(\d+)\s*\+\s*(\d+)\s*=(?=\s*$)") +server = LanguageServer("code-lens-server", "v1") + + +@server.feature(types.TEXT_DOCUMENT_CODE_LENS) +def code_lens(params: types.CodeLensParams): + """Return a list of code lens to insert into the given document. + + This method will read the whole document and identify each sum in the document and + tell the language client to insert a code lens at each location. + """ + items = [] + document_uri = params.text_document.uri + document = server.workspace.get_text_document(document_uri) + + lines = document.lines + for idx, line in enumerate(lines): + match = ADDITION.match(line) + if match is not None: + range_ = types.Range( + start=types.Position(line=idx, character=0), + end=types.Position(line=idx, character=len(line) - 1), + ) + + left = int(match.group(1)) + right = int(match.group(2)) + + code_lens = types.CodeLens( + range=range_, + data={ + "left": left, + "right": right, + "uri": document_uri, + }, + ) + items.append(code_lens) + + return items + + +@server.feature(types.CODE_LENS_RESOLVE) +def code_lens_resolve(ls: LanguageServer, item: types.CodeLens): + """Resolve the ``command`` field of the given code lens. + + Using the ``data`` that was attached to the code lens item created in the function + above, this prepares an invocation of the ``evaluateSum`` command below. + """ + logging.info("Resolving code lens: %s", item) + + left = item.data["left"] + right = item.data["right"] + uri = item.data["uri"] + + args = dict( + uri=uri, + left=left, + right=right, + line=item.range.start.line, + ) + + item.command = types.Command( + title=f"Evaluate {left} + {right}", + command="codeLens.evaluateSum", + arguments=[args], + ) + return item + + +@server.command("codeLens.evaluateSum") +def evaluate_sum(ls: LanguageServer, args): + logging.info("arguments: %s", args) + + arguments = args[0] + document = ls.workspace.get_text_document(arguments["uri"]) + line = document.lines[arguments["line"]] + + # Compute the edit that will update the document with the result. + answer = arguments["left"] + arguments["right"] + edit = types.TextDocumentEdit( + text_document=types.OptionalVersionedTextDocumentIdentifier( + uri=arguments["uri"], version=document.version + ), + edits=[ + types.TextEdit( + new_text=f"{line.strip()} {answer}\n", + range=types.Range( + start=types.Position(line=arguments["line"], character=0), + end=types.Position(line=arguments["line"] + 1, character=0), + ), + ) + ], + ) + + # Apply the edit. + ls.apply_edit(types.WorkspaceEdit(document_changes=[edit])) + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO, format="%(message)s") + server.start_io() diff --git a/tests/e2e/test_code_lens.py b/tests/e2e/test_code_lens.py new file mode 100644 index 00000000..c4f40b95 --- /dev/null +++ b/tests/e2e/test_code_lens.py @@ -0,0 +1,155 @@ +############################################################################ +# Copyright(c) Open Law Library. All rights reserved. # +# See ThirdPartyNotices.txt in the project root for additional notices. # +# # +# Licensed under the Apache License, Version 2.0 (the "License") # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http: // www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +############################################################################ +from __future__ import annotations + +import typing + +import pytest +import pytest_asyncio +from lsprotocol import types + +if typing.TYPE_CHECKING: + from typing import Tuple + + from pygls.lsp.client import BaseLanguageClient + + +@pytest_asyncio.fixture(scope="module") +async def code_lens(get_client_for): + async for result in get_client_for("code_lens.py"): + yield result + + +@pytest.mark.asyncio(scope="module") +async def test_code_lens( + code_lens: Tuple[BaseLanguageClient, types.InitializeResult], uri_for +): + """Ensure that the example code lens server is working as expected.""" + client, initialize_result = code_lens + + code_lens_options = initialize_result.capabilities.code_lens_provider + assert code_lens_options.resolve_provider is True + + test_uri = uri_for("sums.txt") + response = await client.text_document_code_lens_async( + types.CodeLensParams( + text_document=types.TextDocumentIdentifier(uri=test_uri), + ) + ) + assert response == [ + types.CodeLens( + range=types.Range( + start=types.Position(line=0, character=0), + end=types.Position(line=0, character=7), + ), + data=dict(left=1, right=1, uri=test_uri), + ), + types.CodeLens( + range=types.Range( + start=types.Position(line=3, character=0), + end=types.Position(line=3, character=7), + ), + data=dict(left=2, right=3, uri=test_uri), + ), + types.CodeLens( + range=types.Range( + start=types.Position(line=6, character=0), + end=types.Position(line=6, character=7), + ), + data=dict(left=6, right=6, uri=test_uri), + ), + ] + + +@pytest.mark.asyncio(scope="module") +async def test_code_lens_resolve( + code_lens: Tuple[BaseLanguageClient, types.InitializeResult], uri_for +): + """Ensure that the example code lens server can resolve a code lens correctly.""" + + client, _ = code_lens + + test_uri = uri_for("sums.txt") + assert test_uri is not None + + lens = types.CodeLens( + range=types.Range( + start=types.Position(line=0, character=0), + end=types.Position(line=1, character=0), + ), + data=dict(left=1, right=1, uri=test_uri), + ) + + result = await client.code_lens_resolve_async(lens) + + # The existing fields should not be modified. + assert result.range == lens.range + assert result.data == lens.data + + # The command field should also be filled in. + assert result.command == types.Command( + title="Evaluate 1 + 1", + command="codeLens.evaluateSum", + arguments=[dict(uri=test_uri, left=1, right=1, line=0)], + ) + + +@pytest.mark.asyncio(scope="module") +async def test_evaluate_sum( + code_lens: Tuple[BaseLanguageClient, types.InitializeResult], uri_for +): + """Ensure that the example code lens server can execute the ``evaluateSum`` command + correctly.""" + + workspace_edit = [] + client, initialize_result = code_lens + + @client.feature(types.WORKSPACE_APPLY_EDIT) + def on_edit(params: types.ApplyWorkspaceEditParams): + workspace_edit.extend(params.edit.document_changes) + return types.ApplyWorkspaceEditResult(applied=True) + + provider = initialize_result.capabilities.execute_command_provider + assert provider.commands == ["codeLens.evaluateSum"] + + test_uri = uri_for("sums.txt") + assert test_uri is not None + + await client.workspace_execute_command_async( + types.ExecuteCommandParams( + command="codeLens.evaluateSum", + arguments=[dict(uri=test_uri, left=1, right=1, line=0)], + ) + ) + + assert workspace_edit == [ + types.TextDocumentEdit( + text_document=types.OptionalVersionedTextDocumentIdentifier( + uri=test_uri, + version=None, + ), + edits=[ + types.TextEdit( + new_text="1 + 1 = 2\n", + range=types.Range( + start=types.Position(line=0, character=0), + end=types.Position(line=1, character=0), + ), + ) + ], + ) + ] diff --git a/tests/lsp/test_code_lens.py b/tests/lsp/test_code_lens.py deleted file mode 100644 index 70241109..00000000 --- a/tests/lsp/test_code_lens.py +++ /dev/null @@ -1,94 +0,0 @@ -############################################################################ -# Copyright(c) Open Law Library. All rights reserved. # -# See ThirdPartyNotices.txt in the project root for additional notices. # -# # -# Licensed under the Apache License, Version 2.0 (the "License") # -# you may not use this file except in compliance with the License. # -# You may obtain a copy of the License at # -# # -# http: // www.apache.org/licenses/LICENSE-2.0 # -# # -# Unless required by applicable law or agreed to in writing, software # -# distributed under the License is distributed on an "AS IS" BASIS, # -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # -# See the License for the specific language governing permissions and # -# limitations under the License. # -############################################################################ -from typing import List, Optional - -from lsprotocol.types import TEXT_DOCUMENT_CODE_LENS -from lsprotocol.types import ( - CodeLens, - CodeLensOptions, - CodeLensParams, - Command, - Position, - Range, - TextDocumentIdentifier, -) - -from ..conftest import ClientServer - - -class ConfiguredLS(ClientServer): - def __init__(self): - super().__init__() - - @self.server.feature( - TEXT_DOCUMENT_CODE_LENS, - CodeLensOptions(resolve_provider=False), - ) - def f(params: CodeLensParams) -> Optional[List[CodeLens]]: - if params.text_document.uri == "file://return.list": - return [ - CodeLens( - range=Range( - start=Position(line=0, character=0), - end=Position(line=1, character=1), - ), - command=Command( - title="cmd1", - command="cmd1", - ), - data="some data", - ), - ] - else: - return None - - -@ConfiguredLS.decorate() -def test_capabilities(client_server): - _, server = client_server - capabilities = server.server_capabilities - - assert capabilities.code_lens_provider - assert not capabilities.code_lens_provider.resolve_provider - - -@ConfiguredLS.decorate() -def test_code_lens_return_list(client_server): - client, _ = client_server - response = client.lsp.send_request( - TEXT_DOCUMENT_CODE_LENS, - CodeLensParams(text_document=TextDocumentIdentifier(uri="file://return.list")), - ).result() - - assert response[0].data == "some data" - assert response[0].range.start.line == 0 - assert response[0].range.start.character == 0 - assert response[0].range.end.line == 1 - assert response[0].range.end.character == 1 - assert response[0].command.title == "cmd1" - assert response[0].command.command == "cmd1" - - -@ConfiguredLS.decorate() -def test_code_lens_return_none(client_server): - client, _ = client_server - response = client.lsp.send_request( - TEXT_DOCUMENT_CODE_LENS, - CodeLensParams(text_document=TextDocumentIdentifier(uri="file://return.none")), - ).result() - - assert response is None From 01c928183352b847661ca4ef0811fba6421a4cb3 Mon Sep 17 00:00:00 2001 From: Alex Carney Date: Sun, 17 Mar 2024 18:34:27 +0000 Subject: [PATCH 7/8] test: add end-to-end tests for the different diagnostic approaches --- examples/servers/publish_diagnostics.py | 97 +++++++++ examples/servers/pull_diagnostics.py | 150 +++++++++++++ tests/e2e/test_publish_diagnostics.py | 146 +++++++++++++ tests/e2e/test_pull_diagnostics.py | 271 ++++++++++++++++++++++++ tests/lsp/test_diagnostics.py | 68 ------ 5 files changed, 664 insertions(+), 68 deletions(-) create mode 100644 examples/servers/publish_diagnostics.py create mode 100644 examples/servers/pull_diagnostics.py create mode 100644 tests/e2e/test_publish_diagnostics.py create mode 100644 tests/e2e/test_pull_diagnostics.py delete mode 100644 tests/lsp/test_diagnostics.py diff --git a/examples/servers/publish_diagnostics.py b/examples/servers/publish_diagnostics.py new file mode 100644 index 00000000..b97454da --- /dev/null +++ b/examples/servers/publish_diagnostics.py @@ -0,0 +1,97 @@ +############################################################################ +# Copyright(c) Open Law Library. All rights reserved. # +# See ThirdPartyNotices.txt in the project root for additional notices. # +# # +# Licensed under the Apache License, Version 2.0 (the "License") # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http: // www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +############################################################################ +import logging +import re + +from lsprotocol import types + +from pygls.server import LanguageServer +from pygls.workspace import TextDocument + +ADDITION = re.compile(r"^\s*(\d+)\s*\+\s*(\d+)\s*=\s*(\d+)?$") + + +class PublishDiagnosticServer(LanguageServer): + """Language server demonstrating "push-model" diagnostics.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.diagnostics = {} + + def parse(self, document: TextDocument): + diagnostics = [] + + for idx, line in enumerate(document.lines): + match = ADDITION.match(line) + if match is not None: + left = int(match.group(1)) + right = int(match.group(2)) + + expected_answer = left + right + actual_answer = match.group(3) + + if actual_answer is not None and expected_answer == int(actual_answer): + continue + + if actual_answer is None: + message = "Missing answer" + severity = types.DiagnosticSeverity.Warning + else: + message = f"Incorrect answer: {actual_answer}" + severity = types.DiagnosticSeverity.Error + + diagnostics.append( + types.Diagnostic( + message=message, + severity=severity, + range=types.Range( + start=types.Position(line=idx, character=0), + end=types.Position(line=idx, character=len(line) - 1), + ), + ) + ) + + self.diagnostics[document.uri] = (document.version, diagnostics) + # logging.info("%s", self.diagnostics) + + +server = PublishDiagnosticServer("diagnostic-server", "v1") + + +@server.feature(types.TEXT_DOCUMENT_DID_OPEN) +def did_open(ls: PublishDiagnosticServer, params: types.DidOpenTextDocumentParams): + """Parse each document when it is opened""" + doc = ls.workspace.get_text_document(params.text_document.uri) + ls.parse(doc) + + for uri, (version, diagnostics) in ls.diagnostics.items(): + ls.publish_diagnostics(uri=uri, version=version, diagnostics=diagnostics) + + +@server.feature(types.TEXT_DOCUMENT_DID_CHANGE) +def did_change(ls: PublishDiagnosticServer, params: types.DidOpenTextDocumentParams): + """Parse each document when it is changed""" + doc = ls.workspace.get_text_document(params.text_document.uri) + ls.parse(doc) + + for uri, (version, diagnostics) in ls.diagnostics.items(): + ls.publish_diagnostics(uri=uri, version=version, diagnostics=diagnostics) + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO, format="%(message)s") + server.start_io() diff --git a/examples/servers/pull_diagnostics.py b/examples/servers/pull_diagnostics.py new file mode 100644 index 00000000..0c6f1470 --- /dev/null +++ b/examples/servers/pull_diagnostics.py @@ -0,0 +1,150 @@ +############################################################################ +# Copyright(c) Open Law Library. All rights reserved. # +# See ThirdPartyNotices.txt in the project root for additional notices. # +# # +# Licensed under the Apache License, Version 2.0 (the "License") # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http: // www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +############################################################################ +import logging +import re + +from lsprotocol import types + +from pygls.server import LanguageServer +from pygls.workspace import TextDocument + +ADDITION = re.compile(r"^\s*(\d+)\s*\+\s*(\d+)\s*=\s*(\d+)?$") + + +class PublishDiagnosticServer(LanguageServer): + """Language server demonstrating "push-model" diagnostics.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.diagnostics = {} + + def parse(self, document: TextDocument): + _, previous = self.diagnostics.get(document.uri, (0, [])) + diagnostics = [] + + for idx, line in enumerate(document.lines): + match = ADDITION.match(line) + if match is not None: + left = int(match.group(1)) + right = int(match.group(2)) + + expected_answer = left + right + actual_answer = match.group(3) + + if actual_answer is not None and expected_answer == int(actual_answer): + continue + + if actual_answer is None: + message = "Missing answer" + severity = types.DiagnosticSeverity.Warning + else: + message = f"Incorrect answer: {actual_answer}" + severity = types.DiagnosticSeverity.Error + + diagnostics.append( + types.Diagnostic( + message=message, + severity=severity, + range=types.Range( + start=types.Position(line=idx, character=0), + end=types.Position(line=idx, character=len(line) - 1), + ), + ) + ) + + # Only update if the list has changed + if previous != diagnostics: + self.diagnostics[document.uri] = (document.version, diagnostics) + + # logging.info("%s", self.diagnostics) + + +server = PublishDiagnosticServer("diagnostic-server", "v1") + + +@server.feature(types.TEXT_DOCUMENT_DID_OPEN) +def did_open(ls: PublishDiagnosticServer, params: types.DidOpenTextDocumentParams): + """Parse each document when it is opened""" + doc = ls.workspace.get_text_document(params.text_document.uri) + ls.parse(doc) + + +@server.feature(types.TEXT_DOCUMENT_DID_CHANGE) +def did_change(ls: PublishDiagnosticServer, params: types.DidOpenTextDocumentParams): + """Parse each document when it is changed""" + doc = ls.workspace.get_text_document(params.text_document.uri) + ls.parse(doc) + + +@server.feature( + types.TEXT_DOCUMENT_DIAGNOSTIC, + types.DiagnosticOptions( + identifier="pull-diagnostics", + inter_file_dependencies=False, + workspace_diagnostics=True, + ), +) +def document_diagnostic( + ls: PublishDiagnosticServer, params: types.DocumentDiagnosticParams +): + """Return diagnostics for the requested document""" + # logging.info("%s", params) + + if (uri := params.text_document.uri) not in ls.diagnostics: + return + + version, diagnostics = ls.diagnostics[uri] + result_id = f"{uri}@{version}" + + if result_id == params.previous_result_id: + return types.UnchangedDocumentDiagnosticReport(result_id) + + return types.FullDocumentDiagnosticReport(items=diagnostics, result_id=result_id) + + +@server.feature(types.WORKSPACE_DIAGNOSTIC) +def workspace_diagnostic( + ls: PublishDiagnosticServer, params: types.WorkspaceDiagnosticParams +): + """Return diagnostics for the workspace.""" + # logging.info("%s", params) + items = [] + previous_ids = {result.value for result in params.previous_result_ids} + + for uri, (version, diagnostics) in ls.diagnostics.items(): + result_id = f"{uri}@{version}" + if result_id in previous_ids: + items.append( + types.WorkspaceUnchangedDocumentDiagnosticReport( + uri=uri, result_id=result_id, version=version + ) + ) + else: + items.append( + types.WorkspaceFullDocumentDiagnosticReport( + uri=uri, + version=version, + items=diagnostics, + ) + ) + + return types.WorkspaceDiagnosticReport(items=items) + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO, format="%(message)s") + server.start_io() diff --git a/tests/e2e/test_publish_diagnostics.py b/tests/e2e/test_publish_diagnostics.py new file mode 100644 index 00000000..043c3935 --- /dev/null +++ b/tests/e2e/test_publish_diagnostics.py @@ -0,0 +1,146 @@ +############################################################################ +# Copyright(c) Open Law Library. All rights reserved. # +# See ThirdPartyNotices.txt in the project root for additional notices. # +# # +# Licensed under the Apache License, Version 2.0 (the "License") # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http: // www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +############################################################################ +from __future__ import annotations + +import asyncio +import typing + +import pytest_asyncio +from lsprotocol import types + +if typing.TYPE_CHECKING: + from typing import Tuple + + from pygls.lsp.client import BaseLanguageClient + + +@pytest_asyncio.fixture() +async def push_diagnostics(get_client_for): + async for client, response in get_client_for("publish_diagnostics.py"): + # Setup a diagnostics handler + client.diagnostics = {} + + @client.feature(types.TEXT_DOCUMENT_PUBLISH_DIAGNOSTICS) + def publish_diagnostics(params: types.PublishDiagnosticsParams): + client.diagnostics[params.uri] = params.diagnostics + + yield client, response + + +async def test_publish_diagnostics( + push_diagnostics: Tuple[BaseLanguageClient, types.InitializeResult], + path_for, + uri_for, +): + """Ensure that the publish diagnostics server is working as expected.""" + client, initialize_result = push_diagnostics + + test_uri = uri_for("sums.txt") + test_path = path_for("sums.txt") + + client.text_document_did_open( + types.DidOpenTextDocumentParams( + types.TextDocumentItem( + uri=test_uri, + language_id="plaintext", + version=0, + text=test_path.read_text(), + ) + ) + ) + + await asyncio.sleep(0.5) + assert test_uri in client.diagnostics + + expected = [ + types.Diagnostic( + message="Missing answer", + severity=types.DiagnosticSeverity.Warning, + range=types.Range( + start=types.Position(line=0, character=0), + end=types.Position(line=0, character=7), + ), + ), + types.Diagnostic( + message="Missing answer", + severity=types.DiagnosticSeverity.Warning, + range=types.Range( + start=types.Position(line=3, character=0), + end=types.Position(line=3, character=7), + ), + ), + types.Diagnostic( + message="Missing answer", + severity=types.DiagnosticSeverity.Warning, + range=types.Range( + start=types.Position(line=6, character=0), + end=types.Position(line=6, character=7), + ), + ), + ] + + assert expected == client.diagnostics[test_uri] + + # Write an incorrect answer... + client.text_document_did_change( + types.DidChangeTextDocumentParams( + text_document=types.VersionedTextDocumentIdentifier( + uri=test_uri, version=1 + ), + content_changes=[ + types.TextDocumentContentChangeEvent_Type1( + text=" 12", + range=types.Range( + start=types.Position(line=0, character=7), + end=types.Position(line=0, character=7), + ), + ) + ], + ) + ) + + await asyncio.sleep(0.5) + assert test_uri in client.diagnostics + + expected = [ + types.Diagnostic( + message="Incorrect answer: 12", + severity=types.DiagnosticSeverity.Error, + range=types.Range( + start=types.Position(line=0, character=0), + end=types.Position(line=0, character=10), + ), + ), + types.Diagnostic( + message="Missing answer", + severity=types.DiagnosticSeverity.Warning, + range=types.Range( + start=types.Position(line=3, character=0), + end=types.Position(line=3, character=7), + ), + ), + types.Diagnostic( + message="Missing answer", + severity=types.DiagnosticSeverity.Warning, + range=types.Range( + start=types.Position(line=6, character=0), + end=types.Position(line=6, character=7), + ), + ), + ] + + assert expected == client.diagnostics[test_uri] diff --git a/tests/e2e/test_pull_diagnostics.py b/tests/e2e/test_pull_diagnostics.py new file mode 100644 index 00000000..4847cf10 --- /dev/null +++ b/tests/e2e/test_pull_diagnostics.py @@ -0,0 +1,271 @@ +############################################################################ +# Copyright(c) Open Law Library. All rights reserved. # +# See ThirdPartyNotices.txt in the project root for additional notices. # +# # +# Licensed under the Apache License, Version 2.0 (the "License") # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http: // www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +############################################################################ +from __future__ import annotations + +import typing + +import pytest_asyncio +from lsprotocol import types + +if typing.TYPE_CHECKING: + from typing import Tuple + + from pygls.lsp.client import BaseLanguageClient + + +@pytest_asyncio.fixture() +async def pull_diagnostics(get_client_for): + async for result in get_client_for("pull_diagnostics.py"): + yield result + + +async def test_document_diagnostics( + pull_diagnostics: Tuple[BaseLanguageClient, types.InitializeResult], + path_for, + uri_for, +): + """Ensure that the pull diagnostics server is working as expected.""" + client, initialize_result = pull_diagnostics + + diagnostic_options = initialize_result.capabilities.diagnostic_provider + assert diagnostic_options.identifier == "pull-diagnostics" + assert diagnostic_options.workspace_diagnostics is True + + test_uri = uri_for("sums.txt") + test_path = path_for("sums.txt") + + client.text_document_did_open( + types.DidOpenTextDocumentParams( + types.TextDocumentItem( + uri=test_uri, + language_id="plaintext", + version=0, + text=test_path.read_text(), + ) + ) + ) + + result = await client.text_document_diagnostic_async( + types.DocumentDiagnosticParams( + text_document=types.TextDocumentIdentifier(test_uri) + ) + ) + + expected = [ + types.Diagnostic( + message="Missing answer", + severity=types.DiagnosticSeverity.Warning, + range=types.Range( + start=types.Position(line=0, character=0), + end=types.Position(line=0, character=7), + ), + ), + types.Diagnostic( + message="Missing answer", + severity=types.DiagnosticSeverity.Warning, + range=types.Range( + start=types.Position(line=3, character=0), + end=types.Position(line=3, character=7), + ), + ), + types.Diagnostic( + message="Missing answer", + severity=types.DiagnosticSeverity.Warning, + range=types.Range( + start=types.Position(line=6, character=0), + end=types.Position(line=6, character=7), + ), + ), + ] + + assert result.result_id == f"{test_uri}@{0}" + assert result.items == expected + assert result.kind == types.DocumentDiagnosticReportKind.Full + + # Write a correct answer... + client.text_document_did_change( + types.DidChangeTextDocumentParams( + text_document=types.VersionedTextDocumentIdentifier( + uri=test_uri, version=1 + ), + content_changes=[ + types.TextDocumentContentChangeEvent_Type1( + text=" 2", + range=types.Range( + start=types.Position(line=0, character=7), + end=types.Position(line=0, character=7), + ), + ) + ], + ) + ) + + result = await client.text_document_diagnostic_async( + types.DocumentDiagnosticParams( + text_document=types.TextDocumentIdentifier(test_uri) + ) + ) + + expected = [ + types.Diagnostic( + message="Missing answer", + severity=types.DiagnosticSeverity.Warning, + range=types.Range( + start=types.Position(line=3, character=0), + end=types.Position(line=3, character=7), + ), + ), + types.Diagnostic( + message="Missing answer", + severity=types.DiagnosticSeverity.Warning, + range=types.Range( + start=types.Position(line=6, character=0), + end=types.Position(line=6, character=7), + ), + ), + ] + + assert result.result_id == f"{test_uri}@{1}" + assert result.items == expected + assert result.kind == types.DocumentDiagnosticReportKind.Full + + +async def test_document_diagnostic_unchanged( + pull_diagnostics: Tuple[BaseLanguageClient, types.InitializeResult], + path_for, + uri_for, +): + """Ensure that the pull diagnostics server is working as expected.""" + client, initialize_result = pull_diagnostics + + diagnostic_options = initialize_result.capabilities.diagnostic_provider + assert diagnostic_options.identifier == "pull-diagnostics" + assert diagnostic_options.workspace_diagnostics is True + + test_uri = uri_for("sums.txt") + test_path = path_for("sums.txt") + + client.text_document_did_open( + types.DidOpenTextDocumentParams( + types.TextDocumentItem( + uri=test_uri, + language_id="plaintext", + version=0, + text=test_path.read_text(), + ) + ) + ) + + result = await client.text_document_diagnostic_async( + types.DocumentDiagnosticParams( + text_document=types.TextDocumentIdentifier(test_uri) + ) + ) + + expected_id = f"{test_uri}@{0}" + + assert result.result_id == expected_id + assert len(result.items) > 0 + assert result.kind == types.DocumentDiagnosticReportKind.Full + + # Making second request should result in an unchanged response + result = await client.text_document_diagnostic_async( + types.DocumentDiagnosticParams( + text_document=types.TextDocumentIdentifier(test_uri), + previous_result_id=expected_id, + ) + ) + + assert result.result_id == expected_id + assert result.kind == types.DocumentDiagnosticReportKind.Unchanged + + +async def test_workspace_diagnostic( + pull_diagnostics: Tuple[BaseLanguageClient, types.InitializeResult], + path_for, + uri_for, +): + """Ensure that the pull diagnostics server is working as expected.""" + client, initialize_result = pull_diagnostics + + diagnostic_options = initialize_result.capabilities.diagnostic_provider + assert diagnostic_options.identifier == "pull-diagnostics" + assert diagnostic_options.workspace_diagnostics is True + + test_uri = uri_for("sums.txt") + test_path = path_for("sums.txt") + + client.text_document_did_open( + types.DidOpenTextDocumentParams( + types.TextDocumentItem( + uri=test_uri, + language_id="plaintext", + version=0, + text=test_path.read_text(), + ) + ) + ) + + result = await client.workspace_diagnostic_async( + types.WorkspaceDiagnosticParams(previous_result_ids=[]) + ) + + expected = [ + types.Diagnostic( + message="Missing answer", + severity=types.DiagnosticSeverity.Warning, + range=types.Range( + start=types.Position(line=0, character=0), + end=types.Position(line=0, character=7), + ), + ), + types.Diagnostic( + message="Missing answer", + severity=types.DiagnosticSeverity.Warning, + range=types.Range( + start=types.Position(line=3, character=0), + end=types.Position(line=3, character=7), + ), + ), + types.Diagnostic( + message="Missing answer", + severity=types.DiagnosticSeverity.Warning, + range=types.Range( + start=types.Position(line=6, character=0), + end=types.Position(line=6, character=7), + ), + ), + ] + + report = result.items[0] + assert report.uri == test_uri + assert report.version == 0 + assert report.items == expected + assert report.kind == types.DocumentDiagnosticReportKind.Full + + result = await client.workspace_diagnostic_async( + types.WorkspaceDiagnosticParams( + previous_result_ids=[ + types.PreviousResultId(uri=test_uri, value=f"{test_uri}@{0}") + ] + ) + ) + + report = result.items[0] + assert report.uri == test_uri + assert report.version == 0 + assert report.kind == types.DocumentDiagnosticReportKind.Unchanged diff --git a/tests/lsp/test_diagnostics.py b/tests/lsp/test_diagnostics.py deleted file mode 100644 index c420942a..00000000 --- a/tests/lsp/test_diagnostics.py +++ /dev/null @@ -1,68 +0,0 @@ -############################################################################ -# Copyright(c) Open Law Library. All rights reserved. # -# See ThirdPartyNotices.txt in the project root for additional notices. # -# # -# Licensed under the Apache License, Version 2.0 (the "License") # -# you may not use this file except in compliance with the License. # -# You may obtain a copy of the License at # -# # -# http: // www.apache.org/licenses/LICENSE-2.0 # -# # -# Unless required by applicable law or agreed to in writing, software # -# distributed under the License is distributed on an "AS IS" BASIS, # -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # -# See the License for the specific language governing permissions and # -# limitations under the License. # -############################################################################ -import json -from typing import Tuple - -from lsprotocol import types - - -from ..client import LanguageClient - - -async def test_diagnostics( - json_server_client: Tuple[LanguageClient, types.InitializeResult], - uri_for, -): - """Ensure that diagnostics are working as expected.""" - client, _ = json_server_client - - test_uri = uri_for("example.json") - assert test_uri is not None - - # Get the expected error message - document_content = "text" - try: - json.loads(document_content) - except json.JSONDecodeError as err: - expected_message = err.msg - - client.text_document_did_open( - types.DidOpenTextDocumentParams( - text_document=types.TextDocumentItem( - uri=test_uri, language_id="json", version=1, text=document_content - ) - ) - ) - - await client.wait_for_notification(types.TEXT_DOCUMENT_PUBLISH_DIAGNOSTICS) - - diagnostics = client.diagnostics[test_uri] - assert diagnostics[0].message == expected_message - - result = await client.text_document_diagnostic_async( - types.DocumentDiagnosticParams( - text_document=types.TextDocumentIdentifier(test_uri) - ) - ) - diagnostics = result.items - assert diagnostics[0].message == expected_message - - workspace_result = await client.workspace_diagnostic_async( - types.WorkspaceDiagnosticParams(previous_result_ids=[]) - ) - diagnostics = workspace_result.items[0].items - assert diagnostics[0].message == expected_message From b46b87eba059f68e43e6ac3ff810f9388be76b11 Mon Sep 17 00:00:00 2001 From: Alex Carney Date: Sun, 17 Mar 2024 18:57:14 +0000 Subject: [PATCH 8/8] chore: add README --- examples/servers/README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 examples/servers/README.md diff --git a/examples/servers/README.md b/examples/servers/README.md new file mode 100644 index 00000000..c6baea41 --- /dev/null +++ b/examples/servers/README.md @@ -0,0 +1,12 @@ +# Example Servers + +| Filename | Works With | Description | +|-|-|-| +| `code_actions.py` | `sums.txt` | Evaluate sums via a code action | +| `code_lens.py` | `sums.txt` | Evaluate sums via a code lens | +| `colors.py` | `colors.txt` | Provides a visual representation of color values and even a color picker in supported clients | +| `goto.py` | `code.txt` | Implements the various "Goto X" requests in the specification | +| `hover.py` | `dates.txt` | Opens a popup showing the date underneath the cursor in multiple formats | +| `inlay_hints.py` | `sums.txt` | Use inlay hints to show the binary representation of numbers in the file | +| `publish_diagnostics.py` | `sums.txt` | Use "push-model" diagnostics to highlight missing or incorrect answers | +| `pull_diagnostics.py` | `sums.txt` | Use "pull-model" diagnostics to highlight missing or incorrect answers |