926 lines
36 KiB
Python
926 lines
36 KiB
Python
# Copyright 2017-2020 Palantir Technologies, Inc.
|
|
# Copyright 2021- Python Language Server Contributors.
|
|
|
|
import logging
|
|
import os
|
|
import socketserver
|
|
import sys
|
|
import threading
|
|
import uuid
|
|
from functools import partial
|
|
from typing import Any
|
|
|
|
try:
|
|
import ujson as json
|
|
except Exception:
|
|
import json
|
|
|
|
from pylsp_jsonrpc.dispatchers import MethodDispatcher
|
|
from pylsp_jsonrpc.endpoint import Endpoint
|
|
from pylsp_jsonrpc.streams import JsonRpcStreamReader, JsonRpcStreamWriter
|
|
|
|
from . import _utils, lsp, uris
|
|
from ._version import __version__
|
|
from .config import config
|
|
from .workspace import Cell, Document, Notebook, Workspace
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
LINT_DEBOUNCE_S = 0.5 # 500 ms
|
|
PARENT_PROCESS_WATCH_INTERVAL = 10 # 10 s
|
|
MAX_WORKERS = 64
|
|
PYTHON_FILE_EXTENSIONS = (".py", ".pyi")
|
|
CONFIG_FILEs = ("pycodestyle.cfg", "setup.cfg", "tox.ini", ".flake8")
|
|
|
|
|
|
class _StreamHandlerWrapper(socketserver.StreamRequestHandler):
|
|
"""A wrapper class that is used to construct a custom handler class."""
|
|
|
|
delegate = None
|
|
|
|
def setup(self) -> None:
|
|
super().setup()
|
|
self.delegate = self.DELEGATE_CLASS(self.rfile, self.wfile)
|
|
|
|
def handle(self) -> None:
|
|
try:
|
|
self.delegate.start()
|
|
except OSError as e:
|
|
if os.name == "nt":
|
|
# Catch and pass on ConnectionResetError when parent process
|
|
# dies
|
|
if isinstance(e, WindowsError) and e.winerror == 10054:
|
|
pass
|
|
|
|
self.SHUTDOWN_CALL()
|
|
|
|
|
|
def start_tcp_lang_server(bind_addr, port, check_parent_process, handler_class) -> None:
|
|
if not issubclass(handler_class, PythonLSPServer):
|
|
raise ValueError("Handler class must be an instance of PythonLSPServer")
|
|
|
|
def shutdown_server(check_parent_process, *args):
|
|
if check_parent_process:
|
|
log.debug("Shutting down server")
|
|
# Shutdown call must be done on a thread, to prevent deadlocks
|
|
stop_thread = threading.Thread(target=server.shutdown)
|
|
stop_thread.start()
|
|
|
|
# Construct a custom wrapper class around the user's handler_class
|
|
wrapper_class = type(
|
|
handler_class.__name__ + "Handler",
|
|
(_StreamHandlerWrapper,),
|
|
{
|
|
# We need to wrap this in staticmethod due to the changes to
|
|
# functools.partial in Python 3.14+
|
|
"DELEGATE_CLASS": staticmethod(
|
|
partial(handler_class, check_parent_process=check_parent_process)
|
|
)
|
|
if sys.version_info >= (3, 14)
|
|
else partial(handler_class, check_parent_process=check_parent_process),
|
|
"SHUTDOWN_CALL": partial(shutdown_server, check_parent_process),
|
|
},
|
|
)
|
|
|
|
server = socketserver.TCPServer(
|
|
(bind_addr, port), wrapper_class, bind_and_activate=False
|
|
)
|
|
server.allow_reuse_address = True
|
|
|
|
try:
|
|
server.server_bind()
|
|
server.server_activate()
|
|
log.info("Serving %s on (%s, %s)", handler_class.__name__, bind_addr, port)
|
|
server.serve_forever()
|
|
finally:
|
|
log.info("Shutting down")
|
|
server.server_close()
|
|
|
|
|
|
def start_io_lang_server(rfile, wfile, check_parent_process, handler_class) -> None:
|
|
if not issubclass(handler_class, PythonLSPServer):
|
|
raise ValueError("Handler class must be an instance of PythonLSPServer")
|
|
log.info("Starting %s IO language server", handler_class.__name__)
|
|
server = handler_class(rfile, wfile, check_parent_process)
|
|
server.start()
|
|
|
|
|
|
def start_ws_lang_server(port, check_parent_process, handler_class) -> None:
|
|
if not issubclass(handler_class, PythonLSPServer):
|
|
raise ValueError("Handler class must be an instance of PythonLSPServer")
|
|
|
|
# imports needed only for websockets based server
|
|
try:
|
|
import asyncio
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
|
|
import websockets
|
|
except ImportError as e:
|
|
raise ImportError(
|
|
"websocket modules missing. Please run: pip install 'python-lsp-server[websockets]'"
|
|
) from e
|
|
|
|
with ThreadPoolExecutor(max_workers=10) as tpool:
|
|
send_queue = None
|
|
loop = None
|
|
|
|
async def pylsp_ws(websocket):
|
|
log.debug("Creating LSP object")
|
|
|
|
# creating a partial function and suppling the websocket connection
|
|
response_handler = partial(send_message, websocket=websocket)
|
|
|
|
# Not using default stream reader and writer.
|
|
# Instead using a consumer based approach to handle processed requests
|
|
pylsp_handler = handler_class(
|
|
rx=None,
|
|
tx=None,
|
|
consumer=response_handler,
|
|
check_parent_process=check_parent_process,
|
|
)
|
|
|
|
async for message in websocket:
|
|
try:
|
|
log.debug("consuming payload and feeding it to LSP handler")
|
|
request = json.loads(message)
|
|
loop = asyncio.get_running_loop()
|
|
await loop.run_in_executor(tpool, pylsp_handler.consume, request)
|
|
except Exception as e:
|
|
log.exception("Failed to process request %s, %s", message, str(e))
|
|
|
|
def send_message(message, websocket):
|
|
"""Handler to send responses of processed requests to respective web socket clients"""
|
|
try:
|
|
payload = json.dumps(message, ensure_ascii=False)
|
|
loop.call_soon_threadsafe(send_queue.put_nowait, (payload, websocket))
|
|
except Exception as e:
|
|
log.exception("Failed to write message %s, %s", message, str(e))
|
|
|
|
async def run_server():
|
|
nonlocal send_queue, loop
|
|
send_queue = asyncio.Queue()
|
|
loop = asyncio.get_running_loop()
|
|
|
|
async with websockets.serve(pylsp_ws, port=port):
|
|
while 1:
|
|
# Wait until payload is available for sending
|
|
payload, websocket = await send_queue.get()
|
|
await websocket.send(payload)
|
|
|
|
asyncio.run(run_server())
|
|
|
|
|
|
class PythonLSPServer(MethodDispatcher):
|
|
"""Implementation of the Microsoft VSCode Language Server Protocol
|
|
https://github.com/Microsoft/language-server-protocol/blob/master/versions/protocol-1-x.md
|
|
"""
|
|
|
|
def __init__(
|
|
self, rx, tx, check_parent_process=False, consumer=None, *, endpoint_cls=None
|
|
) -> None:
|
|
self.workspace = None
|
|
self.config = None
|
|
self.root_uri = None
|
|
self.watching_thread = None
|
|
self.workspaces = {}
|
|
self.uri_workspace_mapper = {}
|
|
|
|
self._check_parent_process = check_parent_process
|
|
|
|
if rx is not None:
|
|
self._jsonrpc_stream_reader = JsonRpcStreamReader(rx)
|
|
else:
|
|
self._jsonrpc_stream_reader = None
|
|
|
|
if tx is not None:
|
|
self._jsonrpc_stream_writer = JsonRpcStreamWriter(tx)
|
|
else:
|
|
self._jsonrpc_stream_writer = None
|
|
|
|
endpoint_cls = endpoint_cls or Endpoint
|
|
|
|
# if consumer is None, it is assumed that the default streams-based approach is being used
|
|
if consumer is None:
|
|
self._endpoint = endpoint_cls(
|
|
self, self._jsonrpc_stream_writer.write, max_workers=MAX_WORKERS
|
|
)
|
|
else:
|
|
self._endpoint = endpoint_cls(self, consumer, max_workers=MAX_WORKERS)
|
|
|
|
self._dispatchers = []
|
|
self._shutdown = False
|
|
|
|
def start(self) -> None:
|
|
"""Entry point for the server."""
|
|
self._jsonrpc_stream_reader.listen(self._endpoint.consume)
|
|
|
|
def consume(self, message) -> None:
|
|
"""Entry point for consumer based server. Alternative to stream listeners."""
|
|
# assuming message will be JSON
|
|
self._endpoint.consume(message)
|
|
|
|
def __getitem__(self, item):
|
|
"""Override getitem to fallback through multiple dispatchers."""
|
|
if self._shutdown and item != "exit":
|
|
# exit is the only allowed method during shutdown
|
|
log.debug("Ignoring non-exit method during shutdown: %s", item)
|
|
item = "invalid_request_after_shutdown"
|
|
|
|
try:
|
|
return super().__getitem__(item)
|
|
except KeyError:
|
|
# Fallback through extra dispatchers
|
|
for dispatcher in self._dispatchers:
|
|
try:
|
|
return dispatcher[item]
|
|
except KeyError:
|
|
continue
|
|
|
|
raise KeyError()
|
|
|
|
def m_shutdown(self, **_kwargs) -> None:
|
|
for workspace in self.workspaces.values():
|
|
workspace.close()
|
|
self._hook("pylsp_shutdown")
|
|
self._shutdown = True
|
|
|
|
def m_invalid_request_after_shutdown(self, **_kwargs):
|
|
return {
|
|
"error": {
|
|
"code": lsp.ErrorCodes.InvalidRequest,
|
|
"message": "Requests after shutdown are not valid",
|
|
}
|
|
}
|
|
|
|
def m_exit(self, **_kwargs) -> None:
|
|
self._endpoint.shutdown()
|
|
if self._jsonrpc_stream_reader is not None:
|
|
self._jsonrpc_stream_reader.close()
|
|
if self._jsonrpc_stream_writer is not None:
|
|
self._jsonrpc_stream_writer.close()
|
|
|
|
def _match_uri_to_workspace(self, uri):
|
|
workspace_uri = _utils.match_uri_to_workspace(uri, self.workspaces)
|
|
return self.workspaces.get(workspace_uri, self.workspace)
|
|
|
|
def _hook(self, hook_name, doc_uri=None, **kwargs):
|
|
"""Calls hook_name and returns a list of results from all registered handlers"""
|
|
workspace = self._match_uri_to_workspace(doc_uri)
|
|
doc = workspace.get_document(doc_uri) if doc_uri else None
|
|
hook_handlers = self.config.plugin_manager.subset_hook_caller(
|
|
hook_name, self.config.disabled_plugins
|
|
)
|
|
return hook_handlers(
|
|
config=self.config, workspace=workspace, document=doc, **kwargs
|
|
)
|
|
|
|
def capabilities(self):
|
|
server_capabilities = {
|
|
"codeActionProvider": True,
|
|
"codeLensProvider": {
|
|
"resolveProvider": False, # We may need to make this configurable
|
|
},
|
|
"completionProvider": {
|
|
"resolveProvider": True, # We could know everything ahead of time, but this takes time to transfer
|
|
"triggerCharacters": ["."],
|
|
},
|
|
"documentFormattingProvider": True,
|
|
"documentHighlightProvider": True,
|
|
"documentRangeFormattingProvider": True,
|
|
"documentSymbolProvider": True,
|
|
"definitionProvider": True,
|
|
"typeDefinitionProvider": True,
|
|
"executeCommandProvider": {
|
|
"commands": flatten(self._hook("pylsp_commands"))
|
|
},
|
|
"hoverProvider": True,
|
|
"referencesProvider": True,
|
|
"renameProvider": True,
|
|
"foldingRangeProvider": True,
|
|
"signatureHelpProvider": {"triggerCharacters": ["(", ",", "="]},
|
|
"textDocumentSync": {
|
|
"change": lsp.TextDocumentSyncKind.INCREMENTAL,
|
|
"save": {
|
|
"includeText": True,
|
|
},
|
|
"openClose": True,
|
|
},
|
|
"notebookDocumentSync": {
|
|
"notebookSelector": [{"cells": [{"language": "python"}]}]
|
|
},
|
|
"workspace": {
|
|
"workspaceFolders": {"supported": True, "changeNotifications": True}
|
|
},
|
|
"experimental": merge(self._hook("pylsp_experimental_capabilities")),
|
|
}
|
|
log.info("Server capabilities: %s", server_capabilities)
|
|
return server_capabilities
|
|
|
|
def m_initialize(
|
|
self,
|
|
processId=None,
|
|
rootUri=None,
|
|
rootPath=None,
|
|
initializationOptions=None,
|
|
workspaceFolders=None,
|
|
**_kwargs,
|
|
):
|
|
log.debug(
|
|
"Language server initialized with %s %s %s %s",
|
|
processId,
|
|
rootUri,
|
|
rootPath,
|
|
initializationOptions,
|
|
)
|
|
if rootUri is None:
|
|
rootUri = uris.from_fs_path(rootPath) if rootPath is not None else ""
|
|
|
|
self.workspaces.pop(self.root_uri, None)
|
|
self.root_uri = rootUri
|
|
self.config = config.Config(
|
|
rootUri,
|
|
initializationOptions or {},
|
|
processId,
|
|
_kwargs.get("capabilities", {}),
|
|
)
|
|
self.workspace = Workspace(rootUri, self._endpoint, self.config)
|
|
self.workspaces[rootUri] = self.workspace
|
|
if workspaceFolders:
|
|
for folder in workspaceFolders:
|
|
uri = folder["uri"]
|
|
if uri == rootUri:
|
|
# Already created
|
|
continue
|
|
workspace_config = config.Config(
|
|
uri,
|
|
self.config._init_opts,
|
|
self.config._process_id,
|
|
self.config._capabilities,
|
|
)
|
|
workspace_config.update(self.config._settings)
|
|
self.workspaces[uri] = Workspace(uri, self._endpoint, workspace_config)
|
|
|
|
self._dispatchers = self._hook("pylsp_dispatchers")
|
|
self._hook("pylsp_initialize")
|
|
|
|
if (
|
|
self._check_parent_process
|
|
and processId is not None
|
|
and self.watching_thread is None
|
|
):
|
|
|
|
def watch_parent_process(pid):
|
|
# exit when the given pid is not alive
|
|
if not _utils.is_process_alive(pid):
|
|
log.info("parent process %s is not alive, exiting!", pid)
|
|
self.m_exit()
|
|
else:
|
|
threading.Timer(
|
|
PARENT_PROCESS_WATCH_INTERVAL, watch_parent_process, args=[pid]
|
|
).start()
|
|
|
|
self.watching_thread = threading.Thread(
|
|
target=watch_parent_process, args=(processId,)
|
|
)
|
|
self.watching_thread.daemon = True
|
|
self.watching_thread.start()
|
|
# Get our capabilities
|
|
return {
|
|
"capabilities": self.capabilities(),
|
|
"serverInfo": {
|
|
"name": "pylsp",
|
|
"version": __version__,
|
|
},
|
|
}
|
|
|
|
def m_initialized(self, **_kwargs) -> None:
|
|
self._hook("pylsp_initialized")
|
|
|
|
def code_actions(self, doc_uri: str, range: dict, context: dict):
|
|
return flatten(
|
|
self._hook("pylsp_code_actions", doc_uri, range=range, context=context)
|
|
)
|
|
|
|
def code_lens(self, doc_uri):
|
|
return flatten(self._hook("pylsp_code_lens", doc_uri))
|
|
|
|
def completions(self, doc_uri, position):
|
|
workspace = self._match_uri_to_workspace(doc_uri)
|
|
document = workspace.get_document(doc_uri)
|
|
ignored_names = None
|
|
if isinstance(document, Cell):
|
|
# We need to get the ignored names from the whole notebook document
|
|
notebook_document = workspace.get_maybe_document(document.notebook_uri)
|
|
ignored_names = notebook_document.jedi_names(doc_uri)
|
|
completions = self._hook(
|
|
"pylsp_completions", doc_uri, position=position, ignored_names=ignored_names
|
|
)
|
|
return {"isIncomplete": False, "items": flatten(completions)}
|
|
|
|
def completion_item_resolve(self, completion_item):
|
|
doc_uri = completion_item.get("data", {}).get("doc_uri", None)
|
|
return self._hook(
|
|
"pylsp_completion_item_resolve", doc_uri, completion_item=completion_item
|
|
)
|
|
|
|
def definitions(self, doc_uri, position):
|
|
return flatten(self._hook("pylsp_definitions", doc_uri, position=position))
|
|
|
|
def type_definition(self, doc_uri, position):
|
|
return self._hook("pylsp_type_definition", doc_uri, position=position)
|
|
|
|
def document_symbols(self, doc_uri):
|
|
return flatten(self._hook("pylsp_document_symbols", doc_uri))
|
|
|
|
def document_did_save(self, doc_uri):
|
|
return self._hook("pylsp_document_did_save", doc_uri)
|
|
|
|
def execute_command(self, command, arguments):
|
|
return self._hook("pylsp_execute_command", command=command, arguments=arguments)
|
|
|
|
def format_document(self, doc_uri, options):
|
|
return lambda: self._hook("pylsp_format_document", doc_uri, options=options)
|
|
|
|
def format_range(self, doc_uri, range, options):
|
|
return self._hook("pylsp_format_range", doc_uri, range=range, options=options)
|
|
|
|
def highlight(self, doc_uri, position):
|
|
return (
|
|
flatten(self._hook("pylsp_document_highlight", doc_uri, position=position))
|
|
or None
|
|
)
|
|
|
|
def hover(self, doc_uri, position):
|
|
return self._hook("pylsp_hover", doc_uri, position=position) or {"contents": ""}
|
|
|
|
@_utils.debounce(LINT_DEBOUNCE_S, keyed_by="doc_uri")
|
|
def lint(self, doc_uri, is_saved) -> None:
|
|
# Since we're debounced, the document may no longer be open
|
|
workspace = self._match_uri_to_workspace(doc_uri)
|
|
document_object = workspace.documents.get(doc_uri, None)
|
|
if isinstance(document_object, Document):
|
|
self._lint_text_document(
|
|
doc_uri, workspace, is_saved, document_object.version
|
|
)
|
|
elif isinstance(document_object, Notebook):
|
|
self._lint_notebook_document(document_object, workspace)
|
|
|
|
def _lint_text_document(
|
|
self, doc_uri, workspace, is_saved, doc_version=None
|
|
) -> None:
|
|
workspace.publish_diagnostics(
|
|
doc_uri,
|
|
flatten(self._hook("pylsp_lint", doc_uri, is_saved=is_saved)),
|
|
doc_version,
|
|
)
|
|
|
|
def _lint_notebook_document(self, notebook_document, workspace) -> None:
|
|
"""
|
|
Lint a notebook document.
|
|
|
|
This is a bit more complicated than linting a text document, because we need to
|
|
send the entire notebook document to the pylsp_lint hook, but we need to send
|
|
the diagnostics back to the client on a per-cell basis.
|
|
"""
|
|
|
|
# First, we create a temp TextDocument that represents the whole notebook
|
|
# contents. We'll use this to send to the pylsp_lint hook.
|
|
random_uri = str(uuid.uuid4())
|
|
|
|
# cell_list helps us map the diagnostics back to the correct cell later.
|
|
cell_list: list[dict[str, Any]] = []
|
|
|
|
offset = 0
|
|
total_source = ""
|
|
for cell in notebook_document.cells:
|
|
cell_uri = cell["document"]
|
|
cell_document = workspace.get_cell_document(cell_uri)
|
|
|
|
num_lines = cell_document.line_count
|
|
|
|
data = {
|
|
"uri": cell_uri,
|
|
"line_start": offset,
|
|
"line_end": offset + num_lines - 1,
|
|
"source": cell_document.source,
|
|
}
|
|
|
|
cell_list.append(data)
|
|
if offset == 0:
|
|
total_source = cell_document.source
|
|
else:
|
|
total_source += "\n" + cell_document.source
|
|
|
|
offset += num_lines
|
|
|
|
workspace.put_document(random_uri, total_source)
|
|
|
|
try:
|
|
document_diagnostics = flatten(
|
|
self._hook("pylsp_lint", random_uri, is_saved=True)
|
|
)
|
|
|
|
# Now we need to map the diagnostics back to the correct cell and publish them.
|
|
# Note: this is O(n*m) in the number of cells and diagnostics, respectively.
|
|
for cell in cell_list:
|
|
cell_diagnostics = []
|
|
for diagnostic in document_diagnostics:
|
|
start_line = diagnostic["range"]["start"]["line"]
|
|
end_line = diagnostic["range"]["end"]["line"]
|
|
|
|
if start_line > cell["line_end"] or end_line < cell["line_start"]:
|
|
continue
|
|
diagnostic["range"]["start"]["line"] = (
|
|
start_line - cell["line_start"]
|
|
)
|
|
diagnostic["range"]["end"]["line"] = end_line - cell["line_start"]
|
|
cell_diagnostics.append(diagnostic)
|
|
|
|
workspace.publish_diagnostics(cell["uri"], cell_diagnostics)
|
|
finally:
|
|
workspace.rm_document(random_uri)
|
|
|
|
def references(self, doc_uri, position, exclude_declaration):
|
|
return flatten(
|
|
self._hook(
|
|
"pylsp_references",
|
|
doc_uri,
|
|
position=position,
|
|
exclude_declaration=exclude_declaration,
|
|
)
|
|
)
|
|
|
|
def rename(self, doc_uri, position, new_name):
|
|
return self._hook("pylsp_rename", doc_uri, position=position, new_name=new_name)
|
|
|
|
def signature_help(self, doc_uri, position):
|
|
return self._hook("pylsp_signature_help", doc_uri, position=position)
|
|
|
|
def folding(self, doc_uri):
|
|
return flatten(self._hook("pylsp_folding_range", doc_uri))
|
|
|
|
def m_completion_item__resolve(self, **completionItem):
|
|
return self.completion_item_resolve(completionItem)
|
|
|
|
def m_notebook_document__did_open(
|
|
self, notebookDocument=None, cellTextDocuments=None, **_kwargs
|
|
) -> None:
|
|
workspace = self._match_uri_to_workspace(notebookDocument["uri"])
|
|
workspace.put_notebook_document(
|
|
notebookDocument["uri"],
|
|
notebookDocument["notebookType"],
|
|
cells=notebookDocument["cells"],
|
|
version=notebookDocument.get("version"),
|
|
metadata=notebookDocument.get("metadata"),
|
|
)
|
|
for cell in cellTextDocuments or []:
|
|
workspace.put_cell_document(
|
|
cell["uri"],
|
|
notebookDocument["uri"],
|
|
cell["languageId"],
|
|
cell["text"],
|
|
version=cell.get("version"),
|
|
)
|
|
self.lint(notebookDocument["uri"], is_saved=True)
|
|
|
|
def m_notebook_document__did_close(
|
|
self, notebookDocument=None, cellTextDocuments=None, **_kwargs
|
|
) -> None:
|
|
workspace = self._match_uri_to_workspace(notebookDocument["uri"])
|
|
for cell in cellTextDocuments or []:
|
|
workspace.publish_diagnostics(cell["uri"], [])
|
|
workspace.rm_document(cell["uri"])
|
|
workspace.rm_document(notebookDocument["uri"])
|
|
|
|
def m_notebook_document__did_change(
|
|
self, notebookDocument=None, change=None, **_kwargs
|
|
) -> None:
|
|
"""
|
|
Changes to the notebook document.
|
|
|
|
This could be one of the following:
|
|
1. Notebook metadata changed
|
|
2. Cell(s) added
|
|
3. Cell(s) deleted
|
|
4. Cell(s) data changed
|
|
4.1 Cell metadata changed
|
|
4.2 Cell source changed
|
|
"""
|
|
workspace = self._match_uri_to_workspace(notebookDocument["uri"])
|
|
|
|
if change.get("metadata"):
|
|
# Case 1
|
|
workspace.update_notebook_metadata(
|
|
notebookDocument["uri"], change.get("metadata")
|
|
)
|
|
|
|
cells = change.get("cells")
|
|
if cells:
|
|
# Change to cells
|
|
structure = cells.get("structure")
|
|
if structure:
|
|
# Case 2 or 3
|
|
notebook_cell_array_change = structure["array"]
|
|
start = notebook_cell_array_change["start"]
|
|
cell_delete_count = notebook_cell_array_change["deleteCount"]
|
|
if cell_delete_count == 0:
|
|
# Case 2
|
|
# Cell documents
|
|
for cell_document in structure["didOpen"]:
|
|
workspace.put_cell_document(
|
|
cell_document["uri"],
|
|
notebookDocument["uri"],
|
|
cell_document["languageId"],
|
|
cell_document["text"],
|
|
cell_document.get("version"),
|
|
)
|
|
# Cell metadata which is added to Notebook
|
|
workspace.add_notebook_cells(
|
|
notebookDocument["uri"],
|
|
notebook_cell_array_change["cells"],
|
|
start,
|
|
)
|
|
else:
|
|
# Case 3
|
|
# Cell documents
|
|
for cell_document in structure["didClose"]:
|
|
workspace.rm_document(cell_document["uri"])
|
|
workspace.publish_diagnostics(cell_document["uri"], [])
|
|
# Cell metadata which is removed from Notebook
|
|
workspace.remove_notebook_cells(
|
|
notebookDocument["uri"], start, cell_delete_count
|
|
)
|
|
|
|
data = cells.get("data")
|
|
if data:
|
|
# Case 4.1
|
|
for cell in data:
|
|
# update NotebookDocument.cells properties
|
|
pass
|
|
|
|
text_content = cells.get("textContent")
|
|
if text_content:
|
|
# Case 4.2
|
|
for cell in text_content:
|
|
cell_uri = cell["document"]["uri"]
|
|
# Even though the protocol says that `changes` is an array, we assume that it's always a single
|
|
# element array that contains the last change to the cell source.
|
|
workspace.update_document(cell_uri, cell["changes"][0])
|
|
self.lint(notebookDocument["uri"], is_saved=True)
|
|
|
|
def m_text_document__did_close(self, textDocument=None, **_kwargs) -> None:
|
|
workspace = self._match_uri_to_workspace(textDocument["uri"])
|
|
workspace.publish_diagnostics(textDocument["uri"], [])
|
|
workspace.rm_document(textDocument["uri"])
|
|
|
|
def m_text_document__did_open(self, textDocument=None, **_kwargs) -> None:
|
|
workspace = self._match_uri_to_workspace(textDocument["uri"])
|
|
workspace.put_document(
|
|
textDocument["uri"],
|
|
textDocument["text"],
|
|
version=textDocument.get("version"),
|
|
)
|
|
self._hook("pylsp_document_did_open", textDocument["uri"])
|
|
self.lint(textDocument["uri"], is_saved=True)
|
|
|
|
def m_text_document__did_change(
|
|
self, contentChanges=None, textDocument=None, **_kwargs
|
|
) -> None:
|
|
workspace = self._match_uri_to_workspace(textDocument["uri"])
|
|
for change in contentChanges:
|
|
workspace.update_document(
|
|
textDocument["uri"], change, version=textDocument.get("version")
|
|
)
|
|
self.lint(textDocument["uri"], is_saved=False)
|
|
|
|
def m_text_document__did_save(self, textDocument=None, **_kwargs) -> None:
|
|
self.lint(textDocument["uri"], is_saved=True)
|
|
self.document_did_save(textDocument["uri"])
|
|
|
|
def m_text_document__code_action(
|
|
self, textDocument=None, range=None, context=None, **_kwargs
|
|
):
|
|
return self.code_actions(textDocument["uri"], range, context)
|
|
|
|
def m_text_document__code_lens(self, textDocument=None, **_kwargs):
|
|
return self.code_lens(textDocument["uri"])
|
|
|
|
def _cell_document__completion(self, cellDocument, position=None, **_kwargs):
|
|
workspace = self._match_uri_to_workspace(cellDocument.notebook_uri)
|
|
notebookDocument = workspace.get_maybe_document(cellDocument.notebook_uri)
|
|
if notebookDocument is None:
|
|
raise ValueError("Invalid notebook document")
|
|
|
|
cell_data = notebookDocument.cell_data()
|
|
|
|
# Concatenate all cells to be a single temporary document
|
|
total_source = "\n".join(data["source"] for data in cell_data.values())
|
|
with workspace.temp_document(total_source) as temp_uri:
|
|
# update position to be the position in the temp document
|
|
if position is not None:
|
|
position["line"] += cell_data[cellDocument.uri]["line_start"]
|
|
|
|
completions = self.completions(temp_uri, position)
|
|
|
|
# Translate temp_uri locations to cell document locations
|
|
for item in completions.get("items", []):
|
|
if item.get("data", {}).get("doc_uri") == temp_uri:
|
|
item["data"]["doc_uri"] = cellDocument.uri
|
|
|
|
# Copy LAST_JEDI_COMPLETIONS to cell document so that completionItem/resolve will work
|
|
tempDocument = workspace.get_document(temp_uri)
|
|
cellDocument.shared_data["LAST_JEDI_COMPLETIONS"] = (
|
|
tempDocument.shared_data.get("LAST_JEDI_COMPLETIONS", None)
|
|
)
|
|
|
|
return completions
|
|
|
|
def m_text_document__completion(self, textDocument=None, position=None, **_kwargs):
|
|
# textDocument here is just a dict with a uri
|
|
workspace = self._match_uri_to_workspace(textDocument["uri"])
|
|
document = workspace.get_document(textDocument["uri"])
|
|
if isinstance(document, Cell):
|
|
return self._cell_document__completion(document, position, **_kwargs)
|
|
return self.completions(textDocument["uri"], position)
|
|
|
|
def _cell_document__definition(self, cellDocument, position=None, **_kwargs):
|
|
workspace = self._match_uri_to_workspace(cellDocument.notebook_uri)
|
|
notebookDocument = workspace.get_maybe_document(cellDocument.notebook_uri)
|
|
if notebookDocument is None:
|
|
raise ValueError("Invalid notebook document")
|
|
|
|
cell_data = notebookDocument.cell_data()
|
|
|
|
# Concatenate all cells to be a single temporary document
|
|
total_source = "\n".join(data["source"] for data in cell_data.values())
|
|
with workspace.temp_document(total_source) as temp_uri:
|
|
# update position to be the position in the temp document
|
|
if position is not None:
|
|
position["line"] += cell_data[cellDocument.uri]["line_start"]
|
|
|
|
definitions = self.definitions(temp_uri, position)
|
|
|
|
# Translate temp_uri locations to cell document locations
|
|
for definition in definitions:
|
|
if definition["uri"] == temp_uri:
|
|
# Find the cell the start line is in and adjust the uri and line numbers
|
|
for cell_uri, data in cell_data.items():
|
|
if (
|
|
data["line_start"]
|
|
<= definition["range"]["start"]["line"]
|
|
<= data["line_end"]
|
|
):
|
|
definition["uri"] = cell_uri
|
|
definition["range"]["start"]["line"] -= data["line_start"]
|
|
definition["range"]["end"]["line"] -= data["line_start"]
|
|
break
|
|
|
|
return definitions
|
|
|
|
def m_text_document__definition(self, textDocument=None, position=None, **_kwargs):
|
|
# textDocument here is just a dict with a uri
|
|
workspace = self._match_uri_to_workspace(textDocument["uri"])
|
|
document = workspace.get_document(textDocument["uri"])
|
|
if isinstance(document, Cell):
|
|
return self._cell_document__definition(document, position, **_kwargs)
|
|
return self.definitions(textDocument["uri"], position)
|
|
|
|
def m_text_document__type_definition(
|
|
self, textDocument=None, position=None, **_kwargs
|
|
):
|
|
return self.type_definition(textDocument["uri"], position)
|
|
|
|
def m_text_document__document_highlight(
|
|
self, textDocument=None, position=None, **_kwargs
|
|
):
|
|
return self.highlight(textDocument["uri"], position)
|
|
|
|
def m_text_document__hover(self, textDocument=None, position=None, **_kwargs):
|
|
return self.hover(textDocument["uri"], position)
|
|
|
|
def m_text_document__document_symbol(self, textDocument=None, **_kwargs):
|
|
return self.document_symbols(textDocument["uri"])
|
|
|
|
def m_text_document__formatting(self, textDocument=None, options=None, **_kwargs):
|
|
return self.format_document(textDocument["uri"], options)
|
|
|
|
def m_text_document__rename(
|
|
self, textDocument=None, position=None, newName=None, **_kwargs
|
|
):
|
|
return self.rename(textDocument["uri"], position, newName)
|
|
|
|
def m_text_document__folding_range(self, textDocument=None, **_kwargs):
|
|
return self.folding(textDocument["uri"])
|
|
|
|
def m_text_document__range_formatting(
|
|
self, textDocument=None, range=None, options=None, **_kwargs
|
|
):
|
|
return self.format_range(textDocument["uri"], range, options)
|
|
|
|
def m_text_document__references(
|
|
self, textDocument=None, position=None, context=None, **_kwargs
|
|
):
|
|
exclude_declaration = not context["includeDeclaration"]
|
|
return self.references(textDocument["uri"], position, exclude_declaration)
|
|
|
|
def m_text_document__signature_help(
|
|
self, textDocument=None, position=None, **_kwargs
|
|
):
|
|
return self.signature_help(textDocument["uri"], position)
|
|
|
|
def m_workspace__did_change_configuration(self, settings=None) -> None:
|
|
if self.config is not None:
|
|
self.config.update((settings or {}).get("pylsp", {}))
|
|
for workspace in self.workspaces.values():
|
|
workspace.update_config(settings)
|
|
self._hook("pylsp_workspace_configuration_changed")
|
|
for doc_uri in workspace.documents:
|
|
self.lint(doc_uri, is_saved=False)
|
|
|
|
def m_workspace__did_change_workspace_folders(self, event=None, **_kwargs):
|
|
if event is None:
|
|
return
|
|
added = event.get("added", [])
|
|
removed = event.get("removed", [])
|
|
|
|
for removed_info in removed:
|
|
if "uri" in removed_info:
|
|
removed_uri = removed_info["uri"]
|
|
self.workspaces.pop(removed_uri, None)
|
|
|
|
for added_info in added:
|
|
if "uri" in added_info:
|
|
added_uri = added_info["uri"]
|
|
workspace_config = config.Config(
|
|
added_uri,
|
|
self.config._init_opts,
|
|
self.config._process_id,
|
|
self.config._capabilities,
|
|
)
|
|
workspace_config.update(self.config._settings)
|
|
self.workspaces[added_uri] = Workspace(
|
|
added_uri, self._endpoint, workspace_config
|
|
)
|
|
|
|
root_workspace_removed = any(
|
|
removed_info["uri"] == self.root_uri for removed_info in removed
|
|
)
|
|
workspace_added = len(added) > 0 and "uri" in added[0]
|
|
if root_workspace_removed and workspace_added:
|
|
added_uri = added[0]["uri"]
|
|
self.root_uri = added_uri
|
|
new_root_workspace = self.workspaces[added_uri]
|
|
self.config = new_root_workspace._config
|
|
self.workspace = new_root_workspace
|
|
elif root_workspace_removed:
|
|
# NOTE: Removing the root workspace can only happen when the server
|
|
# is closed, thus the else condition of this if can never happen.
|
|
if self.workspaces:
|
|
log.debug("Root workspace deleted!")
|
|
available_workspaces = sorted(self.workspaces)
|
|
first_workspace = available_workspaces[0]
|
|
new_root_workspace = self.workspaces[first_workspace]
|
|
self.root_uri = first_workspace
|
|
self.config = new_root_workspace._config
|
|
self.workspace = new_root_workspace
|
|
|
|
# Migrate documents that are on the root workspace and have a better
|
|
# match now
|
|
doc_uris = list(self.workspace._docs.keys())
|
|
for uri in doc_uris:
|
|
doc = self.workspace._docs.pop(uri)
|
|
new_workspace = self._match_uri_to_workspace(uri)
|
|
new_workspace._docs[uri] = doc
|
|
|
|
def m_workspace__did_change_watched_files(self, changes=None, **_kwargs):
|
|
changed_py_files = set()
|
|
config_changed = False
|
|
for d in changes or []:
|
|
if d["uri"].endswith(PYTHON_FILE_EXTENSIONS):
|
|
changed_py_files.add(d["uri"])
|
|
elif d["uri"].endswith(CONFIG_FILEs):
|
|
config_changed = True
|
|
|
|
if config_changed:
|
|
self.config.settings.cache_clear()
|
|
elif not changed_py_files:
|
|
# Only externally changed python files and lint configs may result in changed diagnostics.
|
|
return
|
|
|
|
for workspace in self.workspaces.values():
|
|
for doc_uri in workspace.documents:
|
|
# Changes in doc_uri are already handled by m_text_document__did_save
|
|
if doc_uri not in changed_py_files:
|
|
self.lint(doc_uri, is_saved=False)
|
|
|
|
def m_workspace__execute_command(self, command=None, arguments=None):
|
|
return self.execute_command(command, arguments)
|
|
|
|
|
|
def flatten(list_of_lists):
|
|
return [item for lst in list_of_lists for item in lst]
|
|
|
|
|
|
def merge(list_of_dicts):
|
|
return {k: v for dictionary in list_of_dicts for k, v in dictionary.items()}
|