chore(lsprotocol): migrate to 2025.0.0 and cleanup artifacts

The changes align the project with the 2025.0.0 lsprotocol
release, removing the old backport and updating type hints
in the protocol hooks to use Sequence where appropriate. The
dist-info and packaging metadata for older lsprotocol
versions are replaced with the new 2025.0.0 artifacts.

- Remove exceptiongroup backport used on Python <3.11
- Use Sequence instead of List in LS protocol hooks
- Replace old dist-info with 2025.0.0 metadata
This commit is contained in:
Christoph Brandau
2026-09-03 08:39:12 +02:00
parent a0a0d38fe5
commit 53ebc5d055
101 changed files with 13838 additions and 7624 deletions
+3 -89
View File
@@ -1,97 +1,11 @@
from typing import List
import warnings
from lsprotocol import types
from .workspace import Workspace
from .text_document import TextDocument
from .position_codec import PositionCodec
# For backwards compatibility
Document = TextDocument
def utf16_unit_offset(chars: str):
warnings.warn(
"'utf16_unit_offset' has been deprecated, instead use "
"'PositionCodec.utf16_unit_offset' via 'workspace.position_codec' "
"or 'text_document.position_codec'",
DeprecationWarning,
stacklevel=2,
)
_codec = PositionCodec()
return _codec.utf16_unit_offset(chars)
def utf16_num_units(chars: str):
warnings.warn(
"'utf16_num_units' has been deprecated, instead use "
"'PositionCodec.client_num_units' via 'workspace.position_codec' "
"or 'text_document.position_codec'",
DeprecationWarning,
stacklevel=2,
)
_codec = PositionCodec()
return _codec.client_num_units(chars)
def position_from_utf16(lines: List[str], position: types.Position):
warnings.warn(
"'position_from_utf16' has been deprecated, instead use "
"'PositionCodec.position_from_client_units' via "
"'workspace.position_codec' or 'text_document.position_codec'",
DeprecationWarning,
stacklevel=2,
)
_codec = PositionCodec()
return _codec.position_from_client_units(lines, position)
def position_to_utf16(lines: List[str], position: types.Position):
warnings.warn(
"'position_to_utf16' has been deprecated, instead use "
"'PositionCodec.position_to_client_units' via "
"'workspace.position_codec' or 'text_document.position_codec'",
DeprecationWarning,
stacklevel=2,
)
_codec = PositionCodec()
return _codec.position_to_client_units(lines, position)
def range_from_utf16(lines: List[str], range: types.Range):
warnings.warn(
"'range_from_utf16' has been deprecated, instead use "
"'PositionCodec.range_from_client_units' via "
"'workspace.position_codec' or 'text_document.position_codec'",
DeprecationWarning,
stacklevel=2,
)
_codec = PositionCodec()
return _codec.range_from_client_units(lines, range)
def range_to_utf16(lines: List[str], range: types.Range):
warnings.warn(
"'range_to_utf16' has been deprecated, instead use "
"'PositionCodec.range_to_client_units' via 'workspace.position_codec' "
"or 'text_document.position_codec'",
DeprecationWarning,
stacklevel=2,
)
_codec = PositionCodec()
return _codec.range_to_client_units(lines, range)
from .position_codec import PositionCodec, ServerTextPosition, ServerTextRange
__all__ = (
"Workspace",
"TextDocument",
"PositionCodec",
"Document",
"utf16_unit_offset",
"utf16_num_units",
"position_from_utf16",
"position_to_utf16",
"range_from_utf16",
"range_to_utf16",
"ServerTextPosition",
"ServerTextRange",
)
+134 -77
View File
@@ -17,14 +17,113 @@
# limitations under the License. #
############################################################################
import logging
from typing import List, Optional, Union
from dataclasses import dataclass
from typing import Optional, Union, Sequence, Any
from lsprotocol import types
log = logging.getLogger(__name__)
@dataclass(order=True)
class ServerTextPosition:
line: int
character: int
def __repr__(self) -> str:
return f"{self.line}:{self.character}"
@dataclass
class ServerTextRange:
start: ServerTextPosition
end: ServerTextPosition
def __repr__(self):
return f"{self.start}-{self.end}"
def __contains__(self, position: Any) -> bool:
if not isinstance(position, ServerTextPosition):
raise TypeError("ServerTextRanges can only contain ServerTextPositions.")
return self.start <= position <= self.end
def includes(self, inner: "ServerTextRange") -> bool:
"""
Returns whether `inner` is entirely contained within self, i.e. all
positions in `inner` are also in `self`.
"""
return self.start <= inner.start and inner.end <= self.end
def overlaps(self, other: "ServerTextRange") -> bool:
"""
Returns whether `self` and `other` overlap, i.e. any positions exist that
are included in both self and other.
"""
return self.start <= other.end and other.start <= self.end
class UnitCounter:
def code_units_for_char(self, char: str) -> int:
"""
Get the number of code units used to encode the given single character.
"""
raise NotImplementedError
def num_units(self, chars: str) -> int:
"""
Get the number of code units used to encode the given string.
"""
return sum(self.code_units_for_char(c) for c in chars)
def column_from_utf32(self, line: str, column: int) -> int:
"""
Convert the codepoint index `column` into code units.
"""
return sum(self.code_units_for_char(c) for c in line[:column])
class Utf32(UnitCounter):
def code_units_for_char(self, char: str) -> int:
return 1
def num_units(self, chars: str) -> int:
# We can avoid the loop needed for other encodings here
return len(chars)
def column_from_utf32(self, line: str, column: int) -> int:
return column
def is_beyond_basic_multilingual_plane(char: str) -> bool:
return ord(char) > 0xFFFF
class Utf16(UnitCounter):
def code_units_for_char(self, char: str) -> int:
if is_beyond_basic_multilingual_plane(char):
return 2
return 1
class Utf8(UnitCounter):
def code_units_for_char(self, char: str) -> int:
codepoint = ord(char)
if codepoint < 0x80:
return 1
if codepoint < 0x800:
return 2
if codepoint < 0x10000:
return 3
return 4
impls: dict["str | types.PositionEncodingKind | None", UnitCounter] = {
types.PositionEncodingKind.Utf8: Utf8(),
types.PositionEncodingKind.Utf16: Utf16(),
types.PositionEncodingKind.Utf32: Utf32(),
}
class PositionCodec:
def __init__(
self,
@@ -33,39 +132,17 @@ class PositionCodec:
] = types.PositionEncodingKind.Utf16,
):
self.encoding = encoding
self.impl = impls.get(encoding, Utf16())
@classmethod
def is_char_beyond_multilingual_plane(cls, char: str) -> bool:
return ord(char) > 0xFFFF
def __repr__(self):
return f"<{self.__class__.__name__}, encoding {self.encoding}>"
def utf16_unit_offset(self, chars: str):
"""
Calculate the number of characters which need two utf-16 code units.
Arguments:
chars (str): The string to count occurrences of utf-16 code units for.
"""
return sum(self.is_char_beyond_multilingual_plane(ch) for ch in chars)
def client_num_units(self, chars: str):
"""
Calculate the length of `str` in client-supported UTF-[32|16|8] code units.
Arguments:
chars (str): The string to return the length in UTF-[32|16|8] code units for.
"""
utf32_units = len(chars)
if self.encoding == types.PositionEncodingKind.Utf32:
return utf32_units
if self.encoding == types.PositionEncodingKind.Utf8:
return utf32_units + (self.utf16_unit_offset(chars) * 2)
return utf32_units + self.utf16_unit_offset(chars)
def client_num_units(self, string: str):
return self.impl.num_units(string)
def position_from_client_units(
self, lines: List[str], position: types.Position
) -> types.Position:
self, lines: Sequence[str], position: types.Position
) -> ServerTextPosition:
"""
Convert the position.character from UTF-[32|16|8] code units to UTF-32.
@@ -84,7 +161,7 @@ class PositionCodec:
see: https://github.com/microsoft/language-server-protocol/issues/376
Arguments:
lines (list):
lines (sequence):
The content of the document which the position refers to.
position (Position):
The line and character offset in UTF-[32|16|8] code units.
@@ -93,59 +170,42 @@ class PositionCodec:
The position with `character` being converted to UTF-32 code units.
"""
if len(lines) == 0:
return types.Position(0, 0)
return ServerTextPosition(0, 0)
if position.line >= len(lines):
return types.Position(len(lines) - 1, self.client_num_units(lines[-1]))
return ServerTextPosition(len(lines) - 1, self.impl.num_units(lines[-1]))
_line = lines[position.line]
_line = _line.replace("\r\n", "\n") # TODO: it's a bit of a hack
_client_len = self.client_num_units(_line)
_utf32_len = len(_line)
_client_len = self.impl.num_units(_line)
if _client_len == 0:
return types.Position(position.line, 0)
return ServerTextPosition(position.line, 0)
_client_end_of_line = self.client_num_units(_line)
if position.character > _client_end_of_line:
position.character = _client_end_of_line - 1
if position.character > _client_len:
position.character = _client_len - 1
_client_index = 0
client_position = 0
utf32_index = 0
while True:
_is_searching_queried_position = _client_index < position.character
_is_before_end_of_line = utf32_index < _utf32_len
_is_searching_for_position = (
_is_searching_queried_position and _is_before_end_of_line
)
if not _is_searching_for_position:
for c in _line:
if client_position >= position.character:
break
_current_char = _line[utf32_index]
_is_double_width = PositionCodec.is_char_beyond_multilingual_plane(
_current_char
)
if _is_double_width:
if self.encoding == types.PositionEncodingKind.Utf32:
_client_index += 1
if self.encoding == types.PositionEncodingKind.Utf8:
_client_index += 4
_client_index += 2
else:
_client_index += 1
client_position += self.impl.code_units_for_char(c)
utf32_index += 1
position = types.Position(line=position.line, character=utf32_index)
return position
if client_position < position.character:
utf32_index = len(_line)
return ServerTextPosition(line=position.line, character=utf32_index)
def position_to_client_units(
self, lines: List[str], position: types.Position
self, lines: Sequence[str], position: "ServerTextPosition | types.Position"
) -> types.Position:
"""
Convert the position.character from its internal UTF-32 representation
to client-supported UTF-[32|16|8] code units.
Arguments:
lines (list):
lines (sequence):
The content of the document which the position refers to.
position (Position):
The line and character offset in UTF-32 code units.
@@ -154,9 +214,7 @@ class PositionCodec:
The position with `character` being converted to UTF-[32|16|8] code units.
"""
try:
character = self.client_num_units(
lines[position.line][: position.character]
)
character = self.impl.num_units(lines[position.line][: position.character])
return types.Position(
line=position.line,
character=character,
@@ -165,13 +223,13 @@ class PositionCodec:
return types.Position(line=len(lines), character=0)
def range_from_client_units(
self, lines: List[str], range: types.Range
) -> types.Range:
self, lines: Sequence[str], range: types.Range
) -> ServerTextRange:
"""
Convert range.[start|end].character from UTF-[32|16|8] code units to UTF-32.
Arguments:
lines (list):
lines (sequence):
The content of the document which the range refers to.
range (Range):
The line and character offset in UTF-[32|16|8] code units.
@@ -179,26 +237,25 @@ class PositionCodec:
Returns:
The range with `character` offsets being converted to UTF-32 code units.
"""
range_new = types.Range(
return ServerTextRange(
start=self.position_from_client_units(lines, range.start),
end=self.position_from_client_units(lines, range.end),
)
return range_new
def range_to_client_units(
self, lines: List[str], range: types.Range
self, lines: Sequence[str], range: "ServerTextRange | types.Range"
) -> types.Range:
"""
Convert range.[start|end].character from UTF-32 to UTF-[32|16|8] code units.
Arguments:
lines (list):
lines (sequence):
The content of the document which the range refers to.
range (Range):
The line and character offset in code units.
The line and character offset in code points.
Returns:
The range with `character` offsets being converted to UTF-[32|16|8] code units.
The range with `character` offsets converted to UTF-[32|16|8] code units.
"""
return types.Range(
start=self.position_to_client_units(lines, range.start),
+96 -19
View File
@@ -19,13 +19,14 @@
import io
import logging
import os
import pathlib
import re
from typing import List, Optional, Pattern
from typing import Optional, Pattern, Sequence
from lsprotocol import types
from pygls.uris import to_fs_path
from .position_codec import PositionCodec
from pygls.uris import urlparse, to_fs_path
from .position_codec import PositionCodec, ServerTextPosition, ServerTextRange
# TODO: this is not the best e.g. we capture numbers
RE_END_WORD = re.compile("^[A-Za-z_0-9]*")
@@ -47,9 +48,10 @@ class TextDocument(object):
):
self.uri = uri
self.version = version
path = to_fs_path(uri)
if path is None:
raise Exception("`path` cannot be None")
if (path := to_fs_path(uri)) is None:
_, _, path, *_ = urlparse(uri)
self.path = path
self.language_id = language_id
self.filename: Optional[str] = os.path.basename(self.path)
@@ -73,7 +75,7 @@ class TextDocument(object):
return self._position_codec
def _apply_incremental_change(
self, change: types.TextDocumentContentChangeEvent_Type1
self, change: types.TextDocumentContentChangePartial
) -> None:
"""Apply an ``Incremental`` text change to the document"""
lines = self.lines
@@ -142,7 +144,7 @@ class TextDocument(object):
content update client requests in the pygls Python library.
"""
if isinstance(change, types.TextDocumentContentChangeEvent_Type1):
if isinstance(change, types.TextDocumentContentChangePartial):
if self._is_sync_kind_incremental:
self._apply_incremental_change(change)
return
@@ -161,26 +163,101 @@ class TextDocument(object):
self._apply_full_change(change)
@property
def lines(self) -> List[str]:
return self.source.splitlines(True)
def lines(self) -> Sequence[str]:
return tuple(self.source.splitlines(True))
def offset_at_server_position(self, server_position: ServerTextPosition) -> int:
"""
Convert server_position to an index into self.source.
The index is the number of code points preceding the client_position in self.source.
"""
row, col = server_position.line, server_position.character
return col + sum(len(line) for line in self.lines[:row])
def offset_at_position(self, client_position: types.Position) -> int:
"""Return the character offset pointed at by the given client_position."""
"""
Convert client_position to an index into self.source.
The index is the number of code points preceding the client_position in self.source.
Example in a code action request handler:
selected_string = document.source[
document.offset_at_position(params.range.start) : document.offset_at_position(params.range.end)
]
"""
lines = self.lines
server_position = self._position_codec.position_from_client_units(
lines, client_position
)
row, col = server_position.line, server_position.character
return col + sum(
self._position_codec.client_num_units(line) for line in lines[:row]
)
return self.offset_at_server_position(server_position)
def server_position_at_offset(self, offset: int) -> ServerTextPosition:
"""
Convert a numeric character offset (index into self.source) into a line-column position.
"""
remaining_offset = offset
for lineno, line in enumerate(self.lines):
if remaining_offset < len(line):
return ServerTextPosition(lineno, remaining_offset)
remaining_offset -= len(line)
# The desired position is beyond the end of the last line.
return ServerTextPosition(lineno + 1, 0)
def client_position_at_offset(self, offset: int) -> types.Position:
"""
Convert a numeric character offset (index into self.source) into a line-column position in client units.
"""
return self.position_to_client_units(self.server_position_at_offset(offset))
def range_from_client_units(self, range: types.Range) -> ServerTextRange:
"""
Convert a range from client units into code points, suitable for indexing into `self.lines`.
"""
return self.position_codec.range_from_client_units(self.lines, range)
def position_from_client_units(
self, position: types.Position
) -> ServerTextPosition:
"""
Convert a position from client units into code points, suitable for indexing into `self.lines`.
"""
return self.position_codec.position_from_client_units(self.lines, position)
def range_to_client_units(self, range: ServerTextRange) -> types.Range:
"""
Convert a range from code points into client units, suitable for sending to the client.
"""
return self.position_codec.range_to_client_units(self.lines, range)
def position_to_client_units(self, position: ServerTextPosition) -> types.Position:
"""
Convert a position from code points into client units, suitable for sending to the client.
"""
return self.position_codec.position_to_client_units(self.lines, position)
def text_in_client_range(self, range: types.Range) -> str:
"""
Given a range in client units, return the text in this range in this document.
"""
return self.text_in_server_range(self.range_from_client_units(range))
def text_in_server_range(self, range: ServerTextRange) -> str:
"""
Given a range in server units, return the text in this range in this document.
"""
return self.source[
self.offset_at_server_position(
range.start
) : self.offset_at_server_position(range.end)
]
@property
def source(self) -> str:
if self._source is None:
with io.open(self.path, "r", encoding="utf-8") as f:
return f.read()
return self._source
if self._source is None and self.path is not None:
return pathlib.Path(self.path).read_text(encoding="utf-8")
return self._source or ""
def word_at_position(
self,
+29 -71
View File
@@ -19,8 +19,8 @@
import copy
import logging
import os
import warnings
from typing import Dict, List, Optional, Union
from typing import Dict, Optional, Sequence, Union
from urllib.parse import unquote
from lsprotocol import types
from lsprotocol.types import (
@@ -40,7 +40,7 @@ class Workspace(object):
self,
root_uri: Optional[str],
sync_kind: TextDocumentSyncKind = TextDocumentSyncKind.Incremental,
workspace_folders: Optional[List[WorkspaceFolder]] = None,
workspace_folders: Optional[Sequence[WorkspaceFolder]] = None,
position_encoding: Optional[
Union[PositionEncodingKind, str]
] = PositionEncodingKind.Utf16,
@@ -48,10 +48,7 @@ class Workspace(object):
self._root_uri = root_uri
if self._root_uri is not None:
self._root_uri_scheme = uri_scheme(self._root_uri)
root_path = to_fs_path(self._root_uri)
if root_path is None:
raise Exception("Couldn't get `root_path` from `root_uri`")
self._root_path = root_path
self._root_path = to_fs_path(self._root_uri)
else:
self._root_path = None
self._sync_kind = sync_kind
@@ -94,17 +91,7 @@ class Workspace(object):
)
def add_folder(self, folder: WorkspaceFolder):
self._folders[folder.uri] = folder
@property
def documents(self):
warnings.warn(
"'workspace.documents' has been deprecated, use "
"'workspace.text_documents' instead",
DeprecationWarning,
stacklevel=2,
)
return self.text_documents
self._folders[unquote(folder.uri)] = folder
@property
def notebook_documents(self):
@@ -141,10 +128,10 @@ class Workspace(object):
The requested notebook document if found, ``None`` otherwise.
"""
if notebook_uri is not None:
return self._notebook_documents.get(notebook_uri)
return self._notebook_documents.get(unquote(notebook_uri))
if cell_uri is not None:
notebook_uri = self._cell_in_notebook.get(cell_uri)
notebook_uri = self._cell_in_notebook.get(unquote(cell_uri))
if notebook_uri is None:
return None
@@ -159,18 +146,25 @@ class Workspace(object):
See https://github.com/Microsoft/language-server-protocol/issues/177
"""
return self._text_documents.get(doc_uri) or self._create_text_document(doc_uri)
return self._text_documents.get(unquote(doc_uri)) or self._create_text_document(
doc_uri
)
def is_local(self):
return (
self._root_uri_scheme == "" or self._root_uri_scheme == "file"
) and os.path.exists(self._root_path)
if self._root_uri_scheme not in {"", "file"}:
return False
if (path := self._root_path) is None:
return False
return os.path.exists(path)
def put_notebook_document(self, params: types.DidOpenNotebookDocumentParams):
notebook = params.notebook_document
# Create a fresh instance to ensure our copy cannot be accidentally modified.
self._notebook_documents[notebook.uri] = copy.deepcopy(notebook)
self._notebook_documents[unquote(notebook.uri)] = copy.deepcopy(notebook)
for cell_document in params.cell_text_documents:
self.put_text_document(cell_document, notebook_uri=notebook.uri)
@@ -193,7 +187,7 @@ class Workspace(object):
"""
doc_uri = text_document.uri
self._text_documents[doc_uri] = self._create_text_document(
self._text_documents[unquote(doc_uri)] = self._create_text_document(
doc_uri,
source=text_document.text,
version=text_document.version,
@@ -201,23 +195,23 @@ class Workspace(object):
)
if notebook_uri:
self._cell_in_notebook[doc_uri] = notebook_uri
self._cell_in_notebook[unquote(doc_uri)] = unquote(notebook_uri)
def remove_notebook_document(self, params: types.DidCloseNotebookDocumentParams):
notebook_uri = params.notebook_document.uri
self._notebook_documents.pop(notebook_uri, None)
self._notebook_documents.pop(unquote(notebook_uri), None)
for cell_document in params.cell_text_documents:
self.remove_text_document(cell_document.uri)
def remove_text_document(self, doc_uri: str):
self._text_documents.pop(doc_uri, None)
self._cell_in_notebook.pop(doc_uri, None)
self._text_documents.pop(unquote(doc_uri), None)
self._cell_in_notebook.pop(unquote(doc_uri), None)
def remove_folder(self, folder_uri: str):
self._folders.pop(folder_uri, None)
self._folders.pop(unquote(folder_uri), None)
try:
del self._folders[folder_uri]
del self._folders[unquote(folder_uri)]
except KeyError:
pass
@@ -231,7 +225,7 @@ class Workspace(object):
def update_notebook_document(self, params: types.DidChangeNotebookDocumentParams):
uri = params.notebook_document.uri
notebook = self._notebook_documents[uri]
notebook = self._notebook_documents[unquote(uri)]
notebook.version = params.notebook_document.version
if params.change.metadata:
@@ -283,41 +277,5 @@ class Workspace(object):
change: types.TextDocumentContentChangeEvent,
):
doc_uri = text_doc.uri
self._text_documents[doc_uri].apply_change(change)
self._text_documents[doc_uri].version = text_doc.version
def get_document(self, *args, **kwargs):
warnings.warn(
"'workspace.get_document' has been deprecated, use "
"'workspace.get_text_document' instead",
DeprecationWarning,
stacklevel=2,
)
return self.get_text_document(*args, **kwargs)
def remove_document(self, *args, **kwargs):
warnings.warn(
"'workspace.remove_document' has been deprecated, use "
"'workspace.remove_text_document' instead",
DeprecationWarning,
stacklevel=2,
)
return self.remove_text_document(*args, **kwargs)
def put_document(self, *args, **kwargs):
warnings.warn(
"'workspace.put_document' has been deprecated, use "
"'workspace.put_text_document' instead",
DeprecationWarning,
stacklevel=2,
)
return self.put_text_document(*args, **kwargs)
def update_document(self, *args, **kwargs):
warnings.warn(
"'workspace.update_document' has been deprecated, use "
"'workspace.update_text_document' instead",
DeprecationWarning,
stacklevel=2,
)
return self.update_text_document(*args, **kwargs)
self._text_documents[unquote(doc_uri)].apply_change(change)
self._text_documents[unquote(doc_uri)].version = text_doc.version