python lsp
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
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)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"Workspace",
|
||||
"TextDocument",
|
||||
"PositionCodec",
|
||||
"Document",
|
||||
"utf16_unit_offset",
|
||||
"utf16_num_units",
|
||||
"position_from_utf16",
|
||||
"position_to_utf16",
|
||||
"range_from_utf16",
|
||||
"range_to_utf16",
|
||||
)
|
||||
@@ -0,0 +1,206 @@
|
||||
############################################################################
|
||||
# Original work Copyright 2017 Palantir Technologies, Inc. #
|
||||
# Original work licensed under the MIT License. #
|
||||
# See ThirdPartyNotices.txt in the project root for license information. #
|
||||
# All modifications Copyright (c) Open Law Library. All rights reserved. #
|
||||
# #
|
||||
# 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 typing import List, Optional, Union
|
||||
|
||||
from lsprotocol import types
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PositionCodec:
|
||||
def __init__(
|
||||
self,
|
||||
encoding: Optional[
|
||||
Union[types.PositionEncodingKind, str]
|
||||
] = types.PositionEncodingKind.Utf16,
|
||||
):
|
||||
self.encoding = encoding
|
||||
|
||||
@classmethod
|
||||
def is_char_beyond_multilingual_plane(cls, char: str) -> bool:
|
||||
return ord(char) > 0xFFFF
|
||||
|
||||
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 position_from_client_units(
|
||||
self, lines: List[str], position: types.Position
|
||||
) -> types.Position:
|
||||
"""
|
||||
Convert the position.character from UTF-[32|16|8] code units to UTF-32.
|
||||
|
||||
A python application can't use the character member of `Position`
|
||||
directly. As per specification it is represented as a zero-based line and
|
||||
character offset based on posible a UTF-[32|16|8] string representation.
|
||||
|
||||
All characters whose code point exceeds the Basic Multilingual Plane are
|
||||
represented by 2 UTF-16 or 4 UTF-8 code units.
|
||||
|
||||
The offset of the closing quotation mark in x="😋" is
|
||||
- 7 in UTF-8 representation
|
||||
- 5 in UTF-16 representation
|
||||
- 4 in UTF-32 representation
|
||||
|
||||
see: https://github.com/microsoft/language-server-protocol/issues/376
|
||||
|
||||
Arguments:
|
||||
lines (list):
|
||||
The content of the document which the position refers to.
|
||||
position (Position):
|
||||
The line and character offset in UTF-[32|16|8] code units.
|
||||
|
||||
Returns:
|
||||
The position with `character` being converted to UTF-32 code units.
|
||||
"""
|
||||
if len(lines) == 0:
|
||||
return types.Position(0, 0)
|
||||
if position.line >= len(lines):
|
||||
return types.Position(len(lines) - 1, self.client_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)
|
||||
|
||||
if _client_len == 0:
|
||||
return types.Position(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
|
||||
|
||||
_client_index = 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:
|
||||
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
|
||||
utf32_index += 1
|
||||
|
||||
position = types.Position(line=position.line, character=utf32_index)
|
||||
return position
|
||||
|
||||
def position_to_client_units(
|
||||
self, lines: List[str], position: 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):
|
||||
The content of the document which the position refers to.
|
||||
position (Position):
|
||||
The line and character offset in UTF-32 code units.
|
||||
|
||||
Returns:
|
||||
The position with `character` being converted to UTF-[32|16|8] code units.
|
||||
"""
|
||||
try:
|
||||
character = self.client_num_units(
|
||||
lines[position.line][: position.character]
|
||||
)
|
||||
return types.Position(
|
||||
line=position.line,
|
||||
character=character,
|
||||
)
|
||||
except IndexError:
|
||||
return types.Position(line=len(lines), character=0)
|
||||
|
||||
def range_from_client_units(
|
||||
self, lines: List[str], range: types.Range
|
||||
) -> types.Range:
|
||||
"""
|
||||
Convert range.[start|end].character from UTF-[32|16|8] code units to UTF-32.
|
||||
|
||||
Arguments:
|
||||
lines (list):
|
||||
The content of the document which the range refers to.
|
||||
range (Range):
|
||||
The line and character offset in UTF-[32|16|8] code units.
|
||||
|
||||
Returns:
|
||||
The range with `character` offsets being converted to UTF-32 code units.
|
||||
"""
|
||||
range_new = types.Range(
|
||||
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
|
||||
) -> types.Range:
|
||||
"""
|
||||
Convert range.[start|end].character from UTF-32 to UTF-[32|16|8] code units.
|
||||
|
||||
Arguments:
|
||||
lines (list):
|
||||
The content of the document which the range refers to.
|
||||
range (Range):
|
||||
The line and character offset in code units.
|
||||
|
||||
Returns:
|
||||
The range with `character` offsets being converted to UTF-[32|16|8] code units.
|
||||
"""
|
||||
return types.Range(
|
||||
start=self.position_to_client_units(lines, range.start),
|
||||
end=self.position_to_client_units(lines, range.end),
|
||||
)
|
||||
@@ -0,0 +1,238 @@
|
||||
############################################################################
|
||||
# Original work Copyright 2017 Palantir Technologies, Inc. #
|
||||
# Original work licensed under the MIT License. #
|
||||
# See ThirdPartyNotices.txt in the project root for license information. #
|
||||
# All modifications Copyright (c) Open Law Library. All rights reserved. #
|
||||
# #
|
||||
# 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 io
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from typing import List, Optional, Pattern
|
||||
|
||||
from lsprotocol import types
|
||||
|
||||
from pygls.uris import to_fs_path
|
||||
from .position_codec import PositionCodec
|
||||
|
||||
# TODO: this is not the best e.g. we capture numbers
|
||||
RE_END_WORD = re.compile("^[A-Za-z_0-9]*")
|
||||
RE_START_WORD = re.compile("[A-Za-z_0-9]*$")
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TextDocument(object):
|
||||
def __init__(
|
||||
self,
|
||||
uri: str,
|
||||
source: Optional[str] = None,
|
||||
version: Optional[int] = None,
|
||||
language_id: Optional[str] = None,
|
||||
local: bool = True,
|
||||
sync_kind: types.TextDocumentSyncKind = types.TextDocumentSyncKind.Incremental,
|
||||
position_codec: Optional[PositionCodec] = None,
|
||||
):
|
||||
self.uri = uri
|
||||
self.version = version
|
||||
path = to_fs_path(uri)
|
||||
if path is None:
|
||||
raise Exception("`path` cannot be None")
|
||||
self.path = path
|
||||
self.language_id = language_id
|
||||
self.filename: Optional[str] = os.path.basename(self.path)
|
||||
|
||||
self._local = local
|
||||
self._source = source
|
||||
|
||||
self._is_sync_kind_full = sync_kind == types.TextDocumentSyncKind.Full
|
||||
self._is_sync_kind_incremental = (
|
||||
sync_kind == types.TextDocumentSyncKind.Incremental
|
||||
)
|
||||
self._is_sync_kind_none = sync_kind == types.TextDocumentSyncKind.None_
|
||||
|
||||
self._position_codec = position_codec if position_codec else PositionCodec()
|
||||
|
||||
def __str__(self):
|
||||
return str(self.uri)
|
||||
|
||||
@property
|
||||
def position_codec(self) -> PositionCodec:
|
||||
return self._position_codec
|
||||
|
||||
def _apply_incremental_change(
|
||||
self, change: types.TextDocumentContentChangeEvent_Type1
|
||||
) -> None:
|
||||
"""Apply an ``Incremental`` text change to the document"""
|
||||
lines = self.lines
|
||||
text = change.text
|
||||
change_range = change.range
|
||||
|
||||
range = self._position_codec.range_from_client_units(lines, change_range)
|
||||
start_line = range.start.line
|
||||
start_col = range.start.character
|
||||
end_line = range.end.line
|
||||
end_col = range.end.character
|
||||
|
||||
# Check for an edit occurring at the very end of the file
|
||||
if start_line == len(lines):
|
||||
self._source = self.source + text
|
||||
return
|
||||
|
||||
new = io.StringIO()
|
||||
|
||||
# Iterate over the existing document until we hit the edit range,
|
||||
# at which point we write the new text, then loop until we hit
|
||||
# the end of the range and continue writing.
|
||||
for i, line in enumerate(lines):
|
||||
if i < start_line:
|
||||
new.write(line)
|
||||
continue
|
||||
|
||||
if i > end_line:
|
||||
new.write(line)
|
||||
continue
|
||||
|
||||
if i == start_line:
|
||||
new.write(line[:start_col])
|
||||
new.write(text)
|
||||
|
||||
if i == end_line:
|
||||
new.write(line[end_col:])
|
||||
|
||||
self._source = new.getvalue()
|
||||
|
||||
def _apply_full_change(self, change: types.TextDocumentContentChangeEvent) -> None:
|
||||
"""Apply a ``Full`` text change to the document."""
|
||||
self._source = change.text
|
||||
|
||||
def _apply_none_change(self, _: types.TextDocumentContentChangeEvent) -> None:
|
||||
"""Apply a ``None`` text change to the document
|
||||
|
||||
Currently does nothing, provided for consistency.
|
||||
"""
|
||||
pass
|
||||
|
||||
def apply_change(self, change: types.TextDocumentContentChangeEvent) -> None:
|
||||
"""Apply a text change to a document, considering TextDocumentSyncKind
|
||||
|
||||
Performs either
|
||||
:attr:`~lsprotocol.types.TextDocumentSyncKind.Incremental`,
|
||||
:attr:`~lsprotocol.types.TextDocumentSyncKind.Full`, or no synchronization
|
||||
based on both the client request and server capabilities.
|
||||
|
||||
.. admonition:: ``Incremental`` versus ``Full`` synchronization
|
||||
|
||||
Even if a server accepts ``Incremantal`` SyncKinds, clients may request
|
||||
a ``Full`` SyncKind. In LSP 3.x, clients make this request by omitting
|
||||
both Range and RangeLength from their request. Consequently, the
|
||||
attributes "range" and "rangeLength" will be missing from ``Full``
|
||||
content update client requests in the pygls Python library.
|
||||
|
||||
"""
|
||||
if isinstance(change, types.TextDocumentContentChangeEvent_Type1):
|
||||
if self._is_sync_kind_incremental:
|
||||
self._apply_incremental_change(change)
|
||||
return
|
||||
# Log an error, but still perform full update to preserve existing
|
||||
# assumptions in test_document/test_document_full_edit. Test breaks
|
||||
# otherwise, and fixing the tests would require a broader fix to
|
||||
# protocol.py.
|
||||
logger.error(
|
||||
"Unsupported client-provided TextDocumentContentChangeEvent. "
|
||||
"Please update / submit a Pull Request to your LSP client."
|
||||
)
|
||||
|
||||
if self._is_sync_kind_none:
|
||||
self._apply_none_change(change)
|
||||
else:
|
||||
self._apply_full_change(change)
|
||||
|
||||
@property
|
||||
def lines(self) -> List[str]:
|
||||
return self.source.splitlines(True)
|
||||
|
||||
def offset_at_position(self, client_position: types.Position) -> int:
|
||||
"""Return the character offset pointed at by the given client_position."""
|
||||
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]
|
||||
)
|
||||
|
||||
@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
|
||||
|
||||
def word_at_position(
|
||||
self,
|
||||
client_position: types.Position,
|
||||
re_start_word: Pattern[str] = RE_START_WORD,
|
||||
re_end_word: Pattern[str] = RE_END_WORD,
|
||||
) -> str:
|
||||
"""Return the word at position.
|
||||
|
||||
The word is constructed in two halves, the first half is found by taking
|
||||
the first match of ``re_start_word`` on the line up until
|
||||
``position.character``.
|
||||
|
||||
The second half is found by taking ``position.character`` up until the
|
||||
last match of ``re_end_word`` on the line.
|
||||
|
||||
:func:`python:re.findall` is used to find the matches.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
position
|
||||
The line and character offset.
|
||||
|
||||
re_start_word
|
||||
The regular expression for extracting the word backward from
|
||||
position. The default pattern is ``[A-Za-z_0-9]*$``.
|
||||
|
||||
re_end_word
|
||||
The regular expression for extracting the word forward from
|
||||
position. The default pattern is ``^[A-Za-z_0-9]*``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The word (obtained by concatenating the two matches) at position.
|
||||
"""
|
||||
lines = self.lines
|
||||
if client_position.line >= len(lines):
|
||||
return ""
|
||||
|
||||
server_position = self._position_codec.position_from_client_units(
|
||||
lines, client_position
|
||||
)
|
||||
row, col = server_position.line, server_position.character
|
||||
line = lines[row]
|
||||
# Split word in two
|
||||
start = line[:col]
|
||||
end = line[col:]
|
||||
|
||||
# Take end of start and start of end to find word
|
||||
# These are guaranteed to match, even if they match the empty string
|
||||
m_start = re_start_word.findall(start)
|
||||
m_end = re_end_word.findall(end)
|
||||
|
||||
return m_start[0] + m_end[-1]
|
||||
@@ -0,0 +1,323 @@
|
||||
############################################################################
|
||||
# Original work Copyright 2017 Palantir Technologies, Inc. #
|
||||
# Original work licensed under the MIT License. #
|
||||
# See ThirdPartyNotices.txt in the project root for license information. #
|
||||
# All modifications Copyright (c) Open Law Library. All rights reserved. #
|
||||
# #
|
||||
# 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 copy
|
||||
import logging
|
||||
import os
|
||||
import warnings
|
||||
from typing import Dict, List, Optional, Union
|
||||
|
||||
from lsprotocol import types
|
||||
from lsprotocol.types import (
|
||||
PositionEncodingKind,
|
||||
TextDocumentSyncKind,
|
||||
WorkspaceFolder,
|
||||
)
|
||||
from pygls.uris import to_fs_path, uri_scheme
|
||||
from pygls.workspace.text_document import TextDocument
|
||||
from pygls.workspace.position_codec import PositionCodec
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Workspace(object):
|
||||
def __init__(
|
||||
self,
|
||||
root_uri: Optional[str],
|
||||
sync_kind: TextDocumentSyncKind = TextDocumentSyncKind.Incremental,
|
||||
workspace_folders: Optional[List[WorkspaceFolder]] = None,
|
||||
position_encoding: Optional[
|
||||
Union[PositionEncodingKind, str]
|
||||
] = PositionEncodingKind.Utf16,
|
||||
):
|
||||
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
|
||||
else:
|
||||
self._root_path = None
|
||||
self._sync_kind = sync_kind
|
||||
self._text_documents: Dict[str, TextDocument] = {}
|
||||
self._notebook_documents: Dict[str, types.NotebookDocument] = {}
|
||||
|
||||
# Used to lookup notebooks which contain a given cell.
|
||||
self._cell_in_notebook: Dict[str, str] = {}
|
||||
self._folders: Dict[str, WorkspaceFolder] = {}
|
||||
self._docs: Dict[str, TextDocument] = {}
|
||||
self._position_encoding = position_encoding
|
||||
self._position_codec = PositionCodec(encoding=position_encoding)
|
||||
|
||||
if workspace_folders is not None:
|
||||
for folder in workspace_folders:
|
||||
self.add_folder(folder)
|
||||
|
||||
@property
|
||||
def position_encoding(self) -> Optional[Union[PositionEncodingKind, str]]:
|
||||
return self._position_encoding
|
||||
|
||||
@property
|
||||
def position_codec(self) -> PositionCodec:
|
||||
return self._position_codec
|
||||
|
||||
def _create_text_document(
|
||||
self,
|
||||
doc_uri: str,
|
||||
source: Optional[str] = None,
|
||||
version: Optional[int] = None,
|
||||
language_id: Optional[str] = None,
|
||||
) -> TextDocument:
|
||||
return TextDocument(
|
||||
doc_uri,
|
||||
source=source,
|
||||
version=version,
|
||||
language_id=language_id,
|
||||
sync_kind=self._sync_kind,
|
||||
position_codec=self._position_codec,
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
@property
|
||||
def notebook_documents(self):
|
||||
return self._notebook_documents
|
||||
|
||||
@property
|
||||
def text_documents(self):
|
||||
return self._text_documents
|
||||
|
||||
@property
|
||||
def folders(self):
|
||||
return self._folders
|
||||
|
||||
def get_notebook_document(
|
||||
self, *, notebook_uri: Optional[str] = None, cell_uri: Optional[str] = None
|
||||
) -> Optional[types.NotebookDocument]:
|
||||
"""Return the notebook corresponding with the given uri.
|
||||
|
||||
If both ``notebook_uri`` and ``cell_uri`` are given, ``notebook_uri`` takes
|
||||
precedence.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
notebook_uri
|
||||
If given, return the notebook document with the given uri.
|
||||
|
||||
cell_uri
|
||||
If given, return the notebook document which contains a cell with the
|
||||
given uri
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[NotebookDocument]
|
||||
The requested notebook document if found, ``None`` otherwise.
|
||||
"""
|
||||
if notebook_uri is not None:
|
||||
return self._notebook_documents.get(notebook_uri)
|
||||
|
||||
if cell_uri is not None:
|
||||
notebook_uri = self._cell_in_notebook.get(cell_uri)
|
||||
if notebook_uri is None:
|
||||
return None
|
||||
|
||||
return self._notebook_documents.get(notebook_uri)
|
||||
|
||||
return None
|
||||
|
||||
def get_text_document(self, doc_uri: str) -> TextDocument:
|
||||
"""
|
||||
Return a managed document if-present,
|
||||
else create one pointing at disk.
|
||||
|
||||
See https://github.com/Microsoft/language-server-protocol/issues/177
|
||||
"""
|
||||
return self._text_documents.get(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)
|
||||
|
||||
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)
|
||||
|
||||
for cell_document in params.cell_text_documents:
|
||||
self.put_text_document(cell_document, notebook_uri=notebook.uri)
|
||||
|
||||
def put_text_document(
|
||||
self,
|
||||
text_document: types.TextDocumentItem,
|
||||
notebook_uri: Optional[str] = None,
|
||||
):
|
||||
"""Add a text document to the workspace.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
text_document
|
||||
The text document to add
|
||||
|
||||
notebook_uri
|
||||
If set, indicates that this text document represents a cell in a notebook
|
||||
document
|
||||
"""
|
||||
doc_uri = text_document.uri
|
||||
|
||||
self._text_documents[doc_uri] = self._create_text_document(
|
||||
doc_uri,
|
||||
source=text_document.text,
|
||||
version=text_document.version,
|
||||
language_id=text_document.language_id,
|
||||
)
|
||||
|
||||
if notebook_uri:
|
||||
self._cell_in_notebook[doc_uri] = notebook_uri
|
||||
|
||||
def remove_notebook_document(self, params: types.DidCloseNotebookDocumentParams):
|
||||
notebook_uri = params.notebook_document.uri
|
||||
self._notebook_documents.pop(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)
|
||||
|
||||
def remove_folder(self, folder_uri: str):
|
||||
self._folders.pop(folder_uri, None)
|
||||
try:
|
||||
del self._folders[folder_uri]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@property
|
||||
def root_path(self):
|
||||
return self._root_path
|
||||
|
||||
@property
|
||||
def root_uri(self):
|
||||
return self._root_uri
|
||||
|
||||
def update_notebook_document(self, params: types.DidChangeNotebookDocumentParams):
|
||||
uri = params.notebook_document.uri
|
||||
notebook = self._notebook_documents[uri]
|
||||
notebook.version = params.notebook_document.version
|
||||
|
||||
if params.change.metadata:
|
||||
notebook.metadata = params.change.metadata
|
||||
|
||||
cell_changes = params.change.cells
|
||||
if cell_changes is None:
|
||||
return
|
||||
|
||||
# Process changes to any cell metadata.
|
||||
nb_cells = {cell.document: cell for cell in notebook.cells}
|
||||
for new_data in cell_changes.data or []:
|
||||
nb_cell = nb_cells.get(new_data.document)
|
||||
if nb_cell is None:
|
||||
logger.warning(
|
||||
"Ignoring metadata for '%s': not in notebook.", new_data.document
|
||||
)
|
||||
continue
|
||||
|
||||
nb_cell.kind = new_data.kind
|
||||
nb_cell.metadata = new_data.metadata
|
||||
nb_cell.execution_summary = new_data.execution_summary
|
||||
|
||||
# Process changes to the notebook's structure
|
||||
structure = cell_changes.structure
|
||||
if structure:
|
||||
cells = notebook.cells
|
||||
new_cells = structure.array.cells or []
|
||||
|
||||
# Re-order the cells
|
||||
before = cells[: structure.array.start]
|
||||
after = cells[(structure.array.start + structure.array.delete_count) :]
|
||||
notebook.cells = [*before, *new_cells, *after]
|
||||
|
||||
for new_cell in structure.did_open or []:
|
||||
self.put_text_document(new_cell, notebook_uri=uri)
|
||||
|
||||
for removed_cell in structure.did_close or []:
|
||||
self.remove_text_document(removed_cell.uri)
|
||||
|
||||
# Process changes to the text content of existing cells.
|
||||
for text in cell_changes.text_content or []:
|
||||
for change in text.changes:
|
||||
self.update_text_document(text.document, change)
|
||||
|
||||
def update_text_document(
|
||||
self,
|
||||
text_doc: types.VersionedTextDocumentIdentifier,
|
||||
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)
|
||||
Reference in New Issue
Block a user