Skip to content

Commit

Permalink
docs: add Document Links example server
Browse files Browse the repository at this point in the history
  • Loading branch information
alcarney committed Jun 3, 2024
1 parent 59a6dc2 commit bb5dc40
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 99 deletions.
5 changes: 5 additions & 0 deletions docs/source/examples/links.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Document Links
==============

.. example-server:: links.py
:start-at: import logging
8 changes: 7 additions & 1 deletion docs/source/getting-started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ Each of the following example servers are focused on implementing a particular s

:octicon:`info`

.. grid-item-card:: Links
:link: /examples/links
:link-type: doc
:text-align: center

:octicon:`link`

.. grid-item-card:: Publish Diagnostics
:link: /examples/publish-diagnostics
:link-type: doc
Expand Down Expand Up @@ -110,4 +117,3 @@ Tutorial
.. note::

Coming soon\ :sup:`TM`

1 change: 1 addition & 0 deletions examples/servers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
| `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 |
| `links.py` | `links.txt` | Implements `textDocument/documentLink` |
| `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 |
| `rename.py` | `code.txt` | Implements symbol renaming |
Expand Down
94 changes: 94 additions & 0 deletions examples/servers/links.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
############################################################################
# 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. #
############################################################################
"""This implements the :lsp:`textDocument/documentLink` and :lsp:`documentLink/resolve`
requests.
These allow you to add support for custom link syntax to your language.
In editors like VSCode, links will often be underlined and can be opened with a
:kbd:`Ctrl+Click`.
This server scans the document given to ``textDocument/documentLink`` for the
syntax ``<LINK_TYPE:PATH>`` and returns a document link desribing its location.
While we could easily compute the ``target`` and ``tooltip`` fields in the same
method, this example demonstrates how the ``documentLink/resolve`` method can be used
to defer this until it is actually necessary
"""

import logging
import re

from lsprotocol import types

from pygls.server import LanguageServer

LINK = re.compile(r"<(\w+):([^>]+)>")
server = LanguageServer("links-server", "v1")


@server.feature(
types.TEXT_DOCUMENT_DOCUMENT_LINK,
)
def document_links(params: types.DocumentLinkParams):
"""Return a list of links contained 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 LINK.finditer(line):
start_char, end_char = match.span()
items.append(
types.DocumentLink(
range=types.Range(
start=types.Position(line=linum, character=start_char),
end=types.Position(line=linum, character=end_char),
),
data={"type": match.group(1), "target": match.group(2)},
),
)

return items


LINK_TYPES = {
"github": ("https://github.com/{}", "Github - {}"),
"pypi": ("https://pypi.org/project/{}", "PyPi - {}"),
}


@server.feature(types.DOCUMENT_LINK_RESOLVE)
def document_link_resolve(link: types.DocumentLink):
"""Given a link, fill in additional information about it"""
logging.info("resolving link: %s", link)

link_type = link.data.get("type", "<unknown>")
link_target = link.data.get("target", "<unknown>")

if (link_info := LINK_TYPES.get(link_type, None)) is None:
logging.error("Unknown link type: '%s'", link_type)
return link

url, tooltip = link_info
link.target = url.format(link_target)
link.tooltip = tooltip.format(link_target)

return link


if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, format="%(message)s")
server.start_io()
2 changes: 2 additions & 0 deletions examples/servers/workspace/links.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pygls <github:openlawlibrary/pygls> is a framework for writing language servers in Python!
It can be installed from PyPi <pypi:pygls>, it depends on the lsprotocol <pypi:lsprotocol> package
100 changes: 100 additions & 0 deletions tests/e2e/test_links.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
############################################################################
# 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 links(get_client_for):
async for result in get_client_for("links.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_link(
links: Tuple[BaseLanguageClient, types.InitializeResult], uri_for
):
"""Ensure that the example links server is working as expected."""
client, initialize_result = links

document_link_options = initialize_result.capabilities.document_link_provider
assert document_link_options.resolve_provider is True

test_uri = uri_for("links.txt")
response = await client.text_document_document_link_async(
types.DocumentLinkParams(
text_document=types.TextDocumentIdentifier(uri=test_uri)
)
)

assert response == [
types.DocumentLink(
range=range_from_str("0:6-0:35"),
data=dict(type="github", target="openlawlibrary/pygls"),
),
types.DocumentLink(
range=range_from_str("1:30-1:42"),
data=dict(type="pypi", target="pygls"),
),
types.DocumentLink(
range=range_from_str("1:73-1:90"),
data=dict(type="pypi", target="lsprotocol"),
),
]


@pytest.mark.asyncio(scope="module")
async def test_document_link_resolve(
links: Tuple[BaseLanguageClient, types.InitializeResult], uri_for
):
"""Ensure that the server can resolve document links correctly."""

client, _ = links
link = types.DocumentLink(
range=range_from_str("0:6-0:35"),
data=dict(type="github", target="openlawlibrary/pygls"),
)

response = await client.document_link_resolve_async(link)

assert response == types.DocumentLink(
range=range_from_str("0:6-0:35"),
target="https://github.com/openlawlibrary/pygls",
tooltip="Github - openlawlibrary/pygls",
data=dict(type="github", target="openlawlibrary/pygls"),
)
98 changes: 0 additions & 98 deletions tests/lsp/test_document_link.py

This file was deleted.

0 comments on commit bb5dc40

Please sign in to comment.