deepnote-python-lsp-server 1.13.1rc2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- deepnote_python_lsp_server-1.13.1rc2.dist-info/METADATA +264 -0
- deepnote_python_lsp_server-1.13.1rc2.dist-info/RECORD +46 -0
- deepnote_python_lsp_server-1.13.1rc2.dist-info/WHEEL +5 -0
- deepnote_python_lsp_server-1.13.1rc2.dist-info/entry_points.txt +25 -0
- deepnote_python_lsp_server-1.13.1rc2.dist-info/licenses/LICENSE +22 -0
- deepnote_python_lsp_server-1.13.1rc2.dist-info/top_level.txt +1 -0
- pylsp/__init__.py +33 -0
- pylsp/__main__.py +132 -0
- pylsp/_utils.py +418 -0
- pylsp/_version.py +1 -0
- pylsp/config/__init__.py +2 -0
- pylsp/config/config.py +203 -0
- pylsp/config/flake8_conf.py +64 -0
- pylsp/config/pycodestyle_conf.py +34 -0
- pylsp/config/source.py +88 -0
- pylsp/hookspecs.py +150 -0
- pylsp/lsp.py +115 -0
- pylsp/plugins/__init__.py +2 -0
- pylsp/plugins/_resolvers.py +132 -0
- pylsp/plugins/_rope_task_handle.py +107 -0
- pylsp/plugins/autopep8_format.py +95 -0
- pylsp/plugins/definition.py +84 -0
- pylsp/plugins/flake8_lint.py +243 -0
- pylsp/plugins/folding.py +210 -0
- pylsp/plugins/highlight.py +36 -0
- pylsp/plugins/hover.py +59 -0
- pylsp/plugins/jedi_completion.py +337 -0
- pylsp/plugins/jedi_rename.py +56 -0
- pylsp/plugins/mccabe_lint.py +56 -0
- pylsp/plugins/preload_imports.py +72 -0
- pylsp/plugins/pycodestyle_lint.py +113 -0
- pylsp/plugins/pydocstyle_lint.py +127 -0
- pylsp/plugins/pyflakes_lint.py +97 -0
- pylsp/plugins/pylint_lint.py +367 -0
- pylsp/plugins/references.py +33 -0
- pylsp/plugins/rope_autoimport.py +408 -0
- pylsp/plugins/rope_completion.py +161 -0
- pylsp/plugins/signature.py +81 -0
- pylsp/plugins/symbols.py +219 -0
- pylsp/plugins/type_definition.py +38 -0
- pylsp/plugins/yapf_format.py +199 -0
- pylsp/py.typed +0 -0
- pylsp/python_lsp.py +945 -0
- pylsp/text_edit.py +97 -0
- pylsp/uris.py +139 -0
- pylsp/workspace.py +791 -0
pylsp/python_lsp.py
ADDED
|
@@ -0,0 +1,945 @@
|
|
|
1
|
+
# Copyright 2017-2020 Palantir Technologies, Inc.
|
|
2
|
+
# Copyright 2021- Python Language Server Contributors.
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import socketserver
|
|
7
|
+
import threading
|
|
8
|
+
import uuid
|
|
9
|
+
from functools import partial
|
|
10
|
+
from hashlib import sha256
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
import ujson as json
|
|
15
|
+
except Exception:
|
|
16
|
+
import json
|
|
17
|
+
|
|
18
|
+
from pylsp_jsonrpc.dispatchers import MethodDispatcher
|
|
19
|
+
from pylsp_jsonrpc.endpoint import Endpoint
|
|
20
|
+
from pylsp_jsonrpc.streams import JsonRpcStreamReader, JsonRpcStreamWriter
|
|
21
|
+
|
|
22
|
+
from . import _utils, lsp, uris
|
|
23
|
+
from ._version import __version__
|
|
24
|
+
from .config import config
|
|
25
|
+
from .workspace import Cell, Document, Notebook, Workspace
|
|
26
|
+
|
|
27
|
+
log = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
LINT_DEBOUNCE_S = 0.5 # 500 ms
|
|
31
|
+
PARENT_PROCESS_WATCH_INTERVAL = 10 # 10 s
|
|
32
|
+
MAX_WORKERS = 64
|
|
33
|
+
PYTHON_FILE_EXTENSIONS = (".py", ".pyi")
|
|
34
|
+
CONFIG_FILEs = ("pycodestyle.cfg", "setup.cfg", "tox.ini", ".flake8")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class _StreamHandlerWrapper(socketserver.StreamRequestHandler):
|
|
38
|
+
"""A wrapper class that is used to construct a custom handler class."""
|
|
39
|
+
|
|
40
|
+
delegate = None
|
|
41
|
+
|
|
42
|
+
def setup(self) -> None:
|
|
43
|
+
super().setup()
|
|
44
|
+
self.delegate = self.DELEGATE_CLASS(self.rfile, self.wfile)
|
|
45
|
+
|
|
46
|
+
def handle(self) -> None:
|
|
47
|
+
self.auth(self.delegate.start)
|
|
48
|
+
|
|
49
|
+
self.SHUTDOWN_CALL()
|
|
50
|
+
|
|
51
|
+
def auth(self, cb):
|
|
52
|
+
token = ""
|
|
53
|
+
if "JUPYTER_TOKEN" in os.environ:
|
|
54
|
+
token = os.environ["JUPYTER_TOKEN"]
|
|
55
|
+
else:
|
|
56
|
+
log.warn("! Missing jupyter token !")
|
|
57
|
+
|
|
58
|
+
data = self.rfile.readline()
|
|
59
|
+
try:
|
|
60
|
+
auth_req = json.loads(data.decode().split("\n")[0])
|
|
61
|
+
except:
|
|
62
|
+
log.error("Error parsing authentication message")
|
|
63
|
+
auth_error_msg = {"msg": "AUTH_ERROR"}
|
|
64
|
+
self.wfile.write(json.dumps(auth_error_msg).encode())
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
hashed_token = sha256(token.encode()).hexdigest()
|
|
68
|
+
if auth_req.get("token") == hashed_token:
|
|
69
|
+
auth_success_msg = {"msg": "AUTH_SUCCESS"}
|
|
70
|
+
self.wfile.write(json.dumps(auth_success_msg).encode())
|
|
71
|
+
cb()
|
|
72
|
+
else:
|
|
73
|
+
log.info("Failed to authenticate: invalid credentials")
|
|
74
|
+
auth_invalid_msg = {"msg": "AUTH_INVALID_CRED"}
|
|
75
|
+
self.wfile.write(json.dumps(auth_invalid_msg).encode())
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def start_tcp_lang_server(bind_addr, port, check_parent_process, handler_class) -> None:
|
|
79
|
+
if not issubclass(handler_class, PythonLSPServer):
|
|
80
|
+
raise ValueError("Handler class must be an instance of PythonLSPServer")
|
|
81
|
+
|
|
82
|
+
def shutdown_server(check_parent_process, *args):
|
|
83
|
+
if check_parent_process:
|
|
84
|
+
log.debug("Shutting down server")
|
|
85
|
+
# Shutdown call must be done on a thread, to prevent deadlocks
|
|
86
|
+
stop_thread = threading.Thread(target=server.shutdown)
|
|
87
|
+
stop_thread.start()
|
|
88
|
+
|
|
89
|
+
# Construct a custom wrapper class around the user's handler_class
|
|
90
|
+
wrapper_class = type(
|
|
91
|
+
handler_class.__name__ + "Handler",
|
|
92
|
+
(_StreamHandlerWrapper,),
|
|
93
|
+
{
|
|
94
|
+
"DELEGATE_CLASS": partial(
|
|
95
|
+
handler_class, check_parent_process=check_parent_process
|
|
96
|
+
),
|
|
97
|
+
"SHUTDOWN_CALL": partial(shutdown_server, check_parent_process),
|
|
98
|
+
},
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
server = socketserver.TCPServer(
|
|
102
|
+
(bind_addr, port), wrapper_class, bind_and_activate=False
|
|
103
|
+
)
|
|
104
|
+
server.allow_reuse_address = True
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
server.server_bind()
|
|
108
|
+
server.server_activate()
|
|
109
|
+
log.info("Serving %s on (%s, %s)", handler_class.__name__, bind_addr, port)
|
|
110
|
+
server.serve_forever()
|
|
111
|
+
finally:
|
|
112
|
+
log.info("Shutting down")
|
|
113
|
+
server.server_close()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def start_io_lang_server(rfile, wfile, check_parent_process, handler_class) -> None:
|
|
117
|
+
if not issubclass(handler_class, PythonLSPServer):
|
|
118
|
+
raise ValueError("Handler class must be an instance of PythonLSPServer")
|
|
119
|
+
log.info("Starting %s IO language server", handler_class.__name__)
|
|
120
|
+
server = handler_class(rfile, wfile, check_parent_process)
|
|
121
|
+
server.start()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def start_ws_lang_server(port, check_parent_process, handler_class) -> None:
|
|
125
|
+
if not issubclass(handler_class, PythonLSPServer):
|
|
126
|
+
raise ValueError("Handler class must be an instance of PythonLSPServer")
|
|
127
|
+
|
|
128
|
+
# imports needed only for websockets based server
|
|
129
|
+
try:
|
|
130
|
+
import asyncio
|
|
131
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
132
|
+
|
|
133
|
+
import websockets
|
|
134
|
+
except ImportError as e:
|
|
135
|
+
raise ImportError(
|
|
136
|
+
"websocket modules missing. Please run: pip install 'python-lsp-server[websockets]'"
|
|
137
|
+
) from e
|
|
138
|
+
|
|
139
|
+
with ThreadPoolExecutor(max_workers=10) as tpool:
|
|
140
|
+
send_queue = None
|
|
141
|
+
loop = None
|
|
142
|
+
|
|
143
|
+
async def pylsp_ws(websocket):
|
|
144
|
+
log.debug("Creating LSP object")
|
|
145
|
+
|
|
146
|
+
# creating a partial function and suppling the websocket connection
|
|
147
|
+
response_handler = partial(send_message, websocket=websocket)
|
|
148
|
+
|
|
149
|
+
# Not using default stream reader and writer.
|
|
150
|
+
# Instead using a consumer based approach to handle processed requests
|
|
151
|
+
pylsp_handler = handler_class(
|
|
152
|
+
rx=None,
|
|
153
|
+
tx=None,
|
|
154
|
+
consumer=response_handler,
|
|
155
|
+
check_parent_process=check_parent_process,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
async for message in websocket:
|
|
159
|
+
try:
|
|
160
|
+
log.debug("consuming payload and feeding it to LSP handler")
|
|
161
|
+
request = json.loads(message)
|
|
162
|
+
loop = asyncio.get_running_loop()
|
|
163
|
+
await loop.run_in_executor(tpool, pylsp_handler.consume, request)
|
|
164
|
+
except Exception as e:
|
|
165
|
+
log.exception("Failed to process request %s, %s", message, str(e))
|
|
166
|
+
|
|
167
|
+
def send_message(message, websocket):
|
|
168
|
+
"""Handler to send responses of processed requests to respective web socket clients"""
|
|
169
|
+
try:
|
|
170
|
+
payload = json.dumps(message, ensure_ascii=False)
|
|
171
|
+
loop.call_soon_threadsafe(send_queue.put_nowait, (payload, websocket))
|
|
172
|
+
except Exception as e:
|
|
173
|
+
log.exception("Failed to write message %s, %s", message, str(e))
|
|
174
|
+
|
|
175
|
+
async def run_server():
|
|
176
|
+
nonlocal send_queue, loop
|
|
177
|
+
send_queue = asyncio.Queue()
|
|
178
|
+
loop = asyncio.get_running_loop()
|
|
179
|
+
|
|
180
|
+
async with websockets.serve(pylsp_ws, port=port):
|
|
181
|
+
while 1:
|
|
182
|
+
# Wait until payload is available for sending
|
|
183
|
+
payload, websocket = await send_queue.get()
|
|
184
|
+
await websocket.send(payload)
|
|
185
|
+
|
|
186
|
+
asyncio.run(run_server())
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class PythonLSPServer(MethodDispatcher):
|
|
190
|
+
"""Implementation of the Microsoft VSCode Language Server Protocol
|
|
191
|
+
https://github.com/Microsoft/language-server-protocol/blob/master/versions/protocol-1-x.md
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
def __init__(
|
|
195
|
+
self, rx, tx, check_parent_process=False, consumer=None, *, endpoint_cls=None
|
|
196
|
+
) -> None:
|
|
197
|
+
self.workspace = None
|
|
198
|
+
self.config = None
|
|
199
|
+
self.root_uri = None
|
|
200
|
+
self.watching_thread = None
|
|
201
|
+
self.workspaces = {}
|
|
202
|
+
self.uri_workspace_mapper = {}
|
|
203
|
+
|
|
204
|
+
self._check_parent_process = check_parent_process
|
|
205
|
+
|
|
206
|
+
if rx is not None:
|
|
207
|
+
self._jsonrpc_stream_reader = JsonRpcStreamReader(rx)
|
|
208
|
+
else:
|
|
209
|
+
self._jsonrpc_stream_reader = None
|
|
210
|
+
|
|
211
|
+
if tx is not None:
|
|
212
|
+
self._jsonrpc_stream_writer = JsonRpcStreamWriter(tx)
|
|
213
|
+
else:
|
|
214
|
+
self._jsonrpc_stream_writer = None
|
|
215
|
+
|
|
216
|
+
endpoint_cls = endpoint_cls or Endpoint
|
|
217
|
+
|
|
218
|
+
# if consumer is None, it is assumed that the default streams-based approach is being used
|
|
219
|
+
if consumer is None:
|
|
220
|
+
self._endpoint = endpoint_cls(
|
|
221
|
+
self, self._jsonrpc_stream_writer.write, max_workers=MAX_WORKERS
|
|
222
|
+
)
|
|
223
|
+
else:
|
|
224
|
+
self._endpoint = endpoint_cls(self, consumer, max_workers=MAX_WORKERS)
|
|
225
|
+
|
|
226
|
+
self._dispatchers = []
|
|
227
|
+
self._shutdown = False
|
|
228
|
+
|
|
229
|
+
def start(self) -> None:
|
|
230
|
+
"""Entry point for the server."""
|
|
231
|
+
self._jsonrpc_stream_reader.listen(self._endpoint.consume)
|
|
232
|
+
|
|
233
|
+
def consume(self, message) -> None:
|
|
234
|
+
"""Entry point for consumer based server. Alternative to stream listeners."""
|
|
235
|
+
# assuming message will be JSON
|
|
236
|
+
self._endpoint.consume(message)
|
|
237
|
+
|
|
238
|
+
def __getitem__(self, item):
|
|
239
|
+
"""Override getitem to fallback through multiple dispatchers."""
|
|
240
|
+
if self._shutdown and item != "exit":
|
|
241
|
+
# exit is the only allowed method during shutdown
|
|
242
|
+
log.debug("Ignoring non-exit method during shutdown: %s", item)
|
|
243
|
+
item = "invalid_request_after_shutdown"
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
return super().__getitem__(item)
|
|
247
|
+
except KeyError:
|
|
248
|
+
# Fallback through extra dispatchers
|
|
249
|
+
for dispatcher in self._dispatchers:
|
|
250
|
+
try:
|
|
251
|
+
return dispatcher[item]
|
|
252
|
+
except KeyError:
|
|
253
|
+
continue
|
|
254
|
+
|
|
255
|
+
raise KeyError()
|
|
256
|
+
|
|
257
|
+
def m_shutdown(self, **_kwargs) -> None:
|
|
258
|
+
for workspace in self.workspaces.values():
|
|
259
|
+
workspace.close()
|
|
260
|
+
self._hook("pylsp_shutdown")
|
|
261
|
+
self._shutdown = True
|
|
262
|
+
|
|
263
|
+
def m_invalid_request_after_shutdown(self, **_kwargs):
|
|
264
|
+
return {
|
|
265
|
+
"error": {
|
|
266
|
+
"code": lsp.ErrorCodes.InvalidRequest,
|
|
267
|
+
"message": "Requests after shutdown are not valid",
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
def m_exit(self, **_kwargs) -> None:
|
|
272
|
+
self._endpoint.shutdown()
|
|
273
|
+
if self._jsonrpc_stream_reader is not None:
|
|
274
|
+
self._jsonrpc_stream_reader.close()
|
|
275
|
+
if self._jsonrpc_stream_writer is not None:
|
|
276
|
+
self._jsonrpc_stream_writer.close()
|
|
277
|
+
|
|
278
|
+
def _match_uri_to_workspace(self, uri):
|
|
279
|
+
workspace_uri = _utils.match_uri_to_workspace(uri, self.workspaces)
|
|
280
|
+
return self.workspaces.get(workspace_uri, self.workspace)
|
|
281
|
+
|
|
282
|
+
def _hook(self, hook_name, doc_uri=None, **kwargs):
|
|
283
|
+
"""Calls hook_name and returns a list of results from all registered handlers"""
|
|
284
|
+
workspace = self._match_uri_to_workspace(doc_uri)
|
|
285
|
+
doc = workspace.get_document(doc_uri) if doc_uri else None
|
|
286
|
+
hook_handlers = self.config.plugin_manager.subset_hook_caller(
|
|
287
|
+
hook_name, self.config.disabled_plugins
|
|
288
|
+
)
|
|
289
|
+
return hook_handlers(
|
|
290
|
+
config=self.config, workspace=workspace, document=doc, **kwargs
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
def capabilities(self):
|
|
294
|
+
server_capabilities = {
|
|
295
|
+
"codeActionProvider": True,
|
|
296
|
+
"codeLensProvider": {
|
|
297
|
+
"resolveProvider": False, # We may need to make this configurable
|
|
298
|
+
},
|
|
299
|
+
"completionProvider": {
|
|
300
|
+
"resolveProvider": True, # We could know everything ahead of time, but this takes time to transfer
|
|
301
|
+
"triggerCharacters": ["."],
|
|
302
|
+
},
|
|
303
|
+
"documentFormattingProvider": True,
|
|
304
|
+
"documentHighlightProvider": True,
|
|
305
|
+
"documentRangeFormattingProvider": True,
|
|
306
|
+
"documentSymbolProvider": True,
|
|
307
|
+
"definitionProvider": True,
|
|
308
|
+
"typeDefinitionProvider": True,
|
|
309
|
+
"executeCommandProvider": {
|
|
310
|
+
"commands": flatten(self._hook("pylsp_commands"))
|
|
311
|
+
},
|
|
312
|
+
"hoverProvider": True,
|
|
313
|
+
"referencesProvider": True,
|
|
314
|
+
"renameProvider": True,
|
|
315
|
+
"foldingRangeProvider": True,
|
|
316
|
+
"signatureHelpProvider": {"triggerCharacters": ["(", ",", "="]},
|
|
317
|
+
"textDocumentSync": {
|
|
318
|
+
"change": lsp.TextDocumentSyncKind.INCREMENTAL,
|
|
319
|
+
"save": {
|
|
320
|
+
"includeText": True,
|
|
321
|
+
},
|
|
322
|
+
"openClose": True,
|
|
323
|
+
},
|
|
324
|
+
"notebookDocumentSync": {
|
|
325
|
+
"notebookSelector": [{"cells": [{"language": "python"}]}]
|
|
326
|
+
},
|
|
327
|
+
"workspace": {
|
|
328
|
+
"workspaceFolders": {"supported": True, "changeNotifications": True}
|
|
329
|
+
},
|
|
330
|
+
"experimental": merge(self._hook("pylsp_experimental_capabilities")),
|
|
331
|
+
}
|
|
332
|
+
log.info("Server capabilities: %s", server_capabilities)
|
|
333
|
+
return server_capabilities
|
|
334
|
+
|
|
335
|
+
def m_initialize(
|
|
336
|
+
self,
|
|
337
|
+
processId=None,
|
|
338
|
+
rootUri=None,
|
|
339
|
+
rootPath=None,
|
|
340
|
+
initializationOptions=None,
|
|
341
|
+
workspaceFolders=None,
|
|
342
|
+
**_kwargs,
|
|
343
|
+
):
|
|
344
|
+
log.debug(
|
|
345
|
+
"Language server initialized with %s %s %s %s",
|
|
346
|
+
processId,
|
|
347
|
+
rootUri,
|
|
348
|
+
rootPath,
|
|
349
|
+
initializationOptions,
|
|
350
|
+
)
|
|
351
|
+
if rootUri is None:
|
|
352
|
+
rootUri = uris.from_fs_path(rootPath) if rootPath is not None else ""
|
|
353
|
+
|
|
354
|
+
self.workspaces.pop(self.root_uri, None)
|
|
355
|
+
self.root_uri = rootUri
|
|
356
|
+
self.config = config.Config(
|
|
357
|
+
rootUri,
|
|
358
|
+
initializationOptions or {},
|
|
359
|
+
processId,
|
|
360
|
+
_kwargs.get("capabilities", {}),
|
|
361
|
+
)
|
|
362
|
+
self.workspace = Workspace(rootUri, self._endpoint, self.config)
|
|
363
|
+
self.workspaces[rootUri] = self.workspace
|
|
364
|
+
if workspaceFolders:
|
|
365
|
+
for folder in workspaceFolders:
|
|
366
|
+
uri = folder["uri"]
|
|
367
|
+
if uri == rootUri:
|
|
368
|
+
# Already created
|
|
369
|
+
continue
|
|
370
|
+
workspace_config = config.Config(
|
|
371
|
+
uri,
|
|
372
|
+
self.config._init_opts,
|
|
373
|
+
self.config._process_id,
|
|
374
|
+
self.config._capabilities,
|
|
375
|
+
)
|
|
376
|
+
workspace_config.update(self.config._settings)
|
|
377
|
+
self.workspaces[uri] = Workspace(uri, self._endpoint, workspace_config)
|
|
378
|
+
|
|
379
|
+
self._dispatchers = self._hook("pylsp_dispatchers")
|
|
380
|
+
self._hook("pylsp_initialize")
|
|
381
|
+
|
|
382
|
+
if (
|
|
383
|
+
self._check_parent_process
|
|
384
|
+
and processId is not None
|
|
385
|
+
and self.watching_thread is None
|
|
386
|
+
):
|
|
387
|
+
|
|
388
|
+
def watch_parent_process(pid):
|
|
389
|
+
# exit when the given pid is not alive
|
|
390
|
+
if not _utils.is_process_alive(pid):
|
|
391
|
+
log.info("parent process %s is not alive, exiting!", pid)
|
|
392
|
+
self.m_exit()
|
|
393
|
+
else:
|
|
394
|
+
threading.Timer(
|
|
395
|
+
PARENT_PROCESS_WATCH_INTERVAL, watch_parent_process, args=[pid]
|
|
396
|
+
).start()
|
|
397
|
+
|
|
398
|
+
self.watching_thread = threading.Thread(
|
|
399
|
+
target=watch_parent_process, args=(processId,)
|
|
400
|
+
)
|
|
401
|
+
self.watching_thread.daemon = True
|
|
402
|
+
self.watching_thread.start()
|
|
403
|
+
# Get our capabilities
|
|
404
|
+
return {
|
|
405
|
+
"capabilities": self.capabilities(),
|
|
406
|
+
"serverInfo": {
|
|
407
|
+
"name": "pylsp",
|
|
408
|
+
"version": __version__,
|
|
409
|
+
},
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
def m_initialized(self, **_kwargs) -> None:
|
|
413
|
+
self._hook("pylsp_initialized")
|
|
414
|
+
|
|
415
|
+
def code_actions(self, doc_uri: str, range: dict, context: dict):
|
|
416
|
+
return flatten(
|
|
417
|
+
self._hook("pylsp_code_actions", doc_uri, range=range, context=context)
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
def code_lens(self, doc_uri):
|
|
421
|
+
return flatten(self._hook("pylsp_code_lens", doc_uri))
|
|
422
|
+
|
|
423
|
+
def completions(self, doc_uri, position):
|
|
424
|
+
workspace = self._match_uri_to_workspace(doc_uri)
|
|
425
|
+
document = workspace.get_document(doc_uri)
|
|
426
|
+
ignored_names = None
|
|
427
|
+
if isinstance(document, Cell):
|
|
428
|
+
# We need to get the ignored names from the whole notebook document
|
|
429
|
+
notebook_document = workspace.get_maybe_document(document.notebook_uri)
|
|
430
|
+
ignored_names = notebook_document.jedi_names(doc_uri)
|
|
431
|
+
completions = self._hook(
|
|
432
|
+
"pylsp_completions", doc_uri, position=position, ignored_names=ignored_names
|
|
433
|
+
)
|
|
434
|
+
return {"isIncomplete": True, "items": flatten(completions)}
|
|
435
|
+
|
|
436
|
+
def completion_detail(self, item):
|
|
437
|
+
detail = self._hook("pylsp_completion_detail", item=item)
|
|
438
|
+
return detail
|
|
439
|
+
|
|
440
|
+
def completion_item_resolve(self, completion_item):
|
|
441
|
+
doc_uri = completion_item.get("data", {}).get("doc_uri", None)
|
|
442
|
+
return self._hook(
|
|
443
|
+
"pylsp_completion_item_resolve", doc_uri, completion_item=completion_item
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
def definitions(self, doc_uri, position):
|
|
447
|
+
return flatten(self._hook("pylsp_definitions", doc_uri, position=position))
|
|
448
|
+
|
|
449
|
+
def type_definition(self, doc_uri, position):
|
|
450
|
+
return self._hook("pylsp_type_definition", doc_uri, position=position)
|
|
451
|
+
|
|
452
|
+
def document_symbols(self, doc_uri):
|
|
453
|
+
return flatten(self._hook("pylsp_document_symbols", doc_uri))
|
|
454
|
+
|
|
455
|
+
def document_did_save(self, doc_uri):
|
|
456
|
+
return self._hook("pylsp_document_did_save", doc_uri)
|
|
457
|
+
|
|
458
|
+
def execute_command(self, command, arguments):
|
|
459
|
+
return self._hook("pylsp_execute_command", command=command, arguments=arguments)
|
|
460
|
+
|
|
461
|
+
def format_document(self, doc_uri, options):
|
|
462
|
+
return lambda: self._hook("pylsp_format_document", doc_uri, options=options)
|
|
463
|
+
|
|
464
|
+
def format_range(self, doc_uri, range, options):
|
|
465
|
+
return self._hook("pylsp_format_range", doc_uri, range=range, options=options)
|
|
466
|
+
|
|
467
|
+
def highlight(self, doc_uri, position):
|
|
468
|
+
return (
|
|
469
|
+
flatten(self._hook("pylsp_document_highlight", doc_uri, position=position))
|
|
470
|
+
or None
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
def hover(self, doc_uri, position):
|
|
474
|
+
return self._hook("pylsp_hover", doc_uri, position=position) or {"contents": ""}
|
|
475
|
+
|
|
476
|
+
@_utils.debounce(LINT_DEBOUNCE_S, keyed_by="doc_uri")
|
|
477
|
+
def lint(self, doc_uri, is_saved) -> None:
|
|
478
|
+
# Since we're debounced, the document may no longer be open
|
|
479
|
+
workspace = self._match_uri_to_workspace(doc_uri)
|
|
480
|
+
document_object = workspace.documents.get(doc_uri, None)
|
|
481
|
+
if isinstance(document_object, Document):
|
|
482
|
+
self._lint_text_document(
|
|
483
|
+
doc_uri, workspace, is_saved, document_object.version
|
|
484
|
+
)
|
|
485
|
+
elif isinstance(document_object, Notebook):
|
|
486
|
+
self._lint_notebook_document(document_object, workspace)
|
|
487
|
+
|
|
488
|
+
def _lint_text_document(
|
|
489
|
+
self, doc_uri, workspace, is_saved, doc_version=None
|
|
490
|
+
) -> None:
|
|
491
|
+
workspace.publish_diagnostics(
|
|
492
|
+
doc_uri,
|
|
493
|
+
flatten(self._hook("pylsp_lint", doc_uri, is_saved=is_saved)),
|
|
494
|
+
doc_version,
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
def _lint_notebook_document(self, notebook_document, workspace) -> None:
|
|
498
|
+
"""
|
|
499
|
+
Lint a notebook document.
|
|
500
|
+
|
|
501
|
+
This is a bit more complicated than linting a text document, because we need to
|
|
502
|
+
send the entire notebook document to the pylsp_lint hook, but we need to send
|
|
503
|
+
the diagnostics back to the client on a per-cell basis.
|
|
504
|
+
"""
|
|
505
|
+
|
|
506
|
+
# First, we create a temp TextDocument that represents the whole notebook
|
|
507
|
+
# contents. We'll use this to send to the pylsp_lint hook.
|
|
508
|
+
random_uri = str(uuid.uuid4())
|
|
509
|
+
|
|
510
|
+
# cell_list helps us map the diagnostics back to the correct cell later.
|
|
511
|
+
cell_list: list[dict[str, Any]] = []
|
|
512
|
+
|
|
513
|
+
offset = 0
|
|
514
|
+
total_source = ""
|
|
515
|
+
for cell in notebook_document.cells:
|
|
516
|
+
cell_uri = cell["document"]
|
|
517
|
+
cell_document = workspace.get_cell_document(cell_uri)
|
|
518
|
+
|
|
519
|
+
num_lines = cell_document.line_count
|
|
520
|
+
|
|
521
|
+
data = {
|
|
522
|
+
"uri": cell_uri,
|
|
523
|
+
"line_start": offset,
|
|
524
|
+
"line_end": offset + num_lines - 1,
|
|
525
|
+
"source": cell_document.source,
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
cell_list.append(data)
|
|
529
|
+
if offset == 0:
|
|
530
|
+
total_source = cell_document.source
|
|
531
|
+
else:
|
|
532
|
+
total_source += "\n" + cell_document.source
|
|
533
|
+
|
|
534
|
+
offset += num_lines
|
|
535
|
+
|
|
536
|
+
workspace.put_document(random_uri, total_source)
|
|
537
|
+
|
|
538
|
+
try:
|
|
539
|
+
document_diagnostics = flatten(
|
|
540
|
+
self._hook("pylsp_lint", random_uri, is_saved=True)
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
# Now we need to map the diagnostics back to the correct cell and publish them.
|
|
544
|
+
# Note: this is O(n*m) in the number of cells and diagnostics, respectively.
|
|
545
|
+
for cell in cell_list:
|
|
546
|
+
cell_diagnostics = []
|
|
547
|
+
for diagnostic in document_diagnostics:
|
|
548
|
+
start_line = diagnostic["range"]["start"]["line"]
|
|
549
|
+
end_line = diagnostic["range"]["end"]["line"]
|
|
550
|
+
|
|
551
|
+
if start_line > cell["line_end"] or end_line < cell["line_start"]:
|
|
552
|
+
continue
|
|
553
|
+
diagnostic["range"]["start"]["line"] = (
|
|
554
|
+
start_line - cell["line_start"]
|
|
555
|
+
)
|
|
556
|
+
diagnostic["range"]["end"]["line"] = end_line - cell["line_start"]
|
|
557
|
+
cell_diagnostics.append(diagnostic)
|
|
558
|
+
|
|
559
|
+
workspace.publish_diagnostics(cell["uri"], cell_diagnostics)
|
|
560
|
+
finally:
|
|
561
|
+
workspace.rm_document(random_uri)
|
|
562
|
+
|
|
563
|
+
def references(self, doc_uri, position, exclude_declaration):
|
|
564
|
+
return flatten(
|
|
565
|
+
self._hook(
|
|
566
|
+
"pylsp_references",
|
|
567
|
+
doc_uri,
|
|
568
|
+
position=position,
|
|
569
|
+
exclude_declaration=exclude_declaration,
|
|
570
|
+
)
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
def rename(self, doc_uri, position, new_name):
|
|
574
|
+
return self._hook("pylsp_rename", doc_uri, position=position, new_name=new_name)
|
|
575
|
+
|
|
576
|
+
def signature_help(self, doc_uri, position):
|
|
577
|
+
return self._hook("pylsp_signature_help", doc_uri, position=position)
|
|
578
|
+
|
|
579
|
+
def folding(self, doc_uri):
|
|
580
|
+
return flatten(self._hook("pylsp_folding_range", doc_uri))
|
|
581
|
+
|
|
582
|
+
def m_completion_item__resolve(self, **completionItem):
|
|
583
|
+
return self.completion_detail(completionItem.get("label"))
|
|
584
|
+
|
|
585
|
+
def m_notebook_document__did_open(
|
|
586
|
+
self, notebookDocument=None, cellTextDocuments=None, **_kwargs
|
|
587
|
+
) -> None:
|
|
588
|
+
workspace = self._match_uri_to_workspace(notebookDocument["uri"])
|
|
589
|
+
workspace.put_notebook_document(
|
|
590
|
+
notebookDocument["uri"],
|
|
591
|
+
notebookDocument["notebookType"],
|
|
592
|
+
cells=notebookDocument["cells"],
|
|
593
|
+
version=notebookDocument.get("version"),
|
|
594
|
+
metadata=notebookDocument.get("metadata"),
|
|
595
|
+
)
|
|
596
|
+
for cell in cellTextDocuments or []:
|
|
597
|
+
workspace.put_cell_document(
|
|
598
|
+
cell["uri"],
|
|
599
|
+
notebookDocument["uri"],
|
|
600
|
+
cell["languageId"],
|
|
601
|
+
cell["text"],
|
|
602
|
+
version=cell.get("version"),
|
|
603
|
+
)
|
|
604
|
+
self.lint(notebookDocument["uri"], is_saved=True)
|
|
605
|
+
|
|
606
|
+
def m_notebook_document__did_close(
|
|
607
|
+
self, notebookDocument=None, cellTextDocuments=None, **_kwargs
|
|
608
|
+
) -> None:
|
|
609
|
+
workspace = self._match_uri_to_workspace(notebookDocument["uri"])
|
|
610
|
+
for cell in cellTextDocuments or []:
|
|
611
|
+
workspace.publish_diagnostics(cell["uri"], [])
|
|
612
|
+
workspace.rm_document(cell["uri"])
|
|
613
|
+
workspace.rm_document(notebookDocument["uri"])
|
|
614
|
+
|
|
615
|
+
def m_notebook_document__did_change(
|
|
616
|
+
self, notebookDocument=None, change=None, **_kwargs
|
|
617
|
+
) -> None:
|
|
618
|
+
"""
|
|
619
|
+
Changes to the notebook document.
|
|
620
|
+
|
|
621
|
+
This could be one of the following:
|
|
622
|
+
1. Notebook metadata changed
|
|
623
|
+
2. Cell(s) added
|
|
624
|
+
3. Cell(s) deleted
|
|
625
|
+
4. Cell(s) data changed
|
|
626
|
+
4.1 Cell metadata changed
|
|
627
|
+
4.2 Cell source changed
|
|
628
|
+
"""
|
|
629
|
+
workspace = self._match_uri_to_workspace(notebookDocument["uri"])
|
|
630
|
+
|
|
631
|
+
if change.get("metadata"):
|
|
632
|
+
# Case 1
|
|
633
|
+
workspace.update_notebook_metadata(
|
|
634
|
+
notebookDocument["uri"], change.get("metadata")
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
cells = change.get("cells")
|
|
638
|
+
if cells:
|
|
639
|
+
# Change to cells
|
|
640
|
+
structure = cells.get("structure")
|
|
641
|
+
if structure:
|
|
642
|
+
# Case 2 or 3
|
|
643
|
+
notebook_cell_array_change = structure["array"]
|
|
644
|
+
start = notebook_cell_array_change["start"]
|
|
645
|
+
cell_delete_count = notebook_cell_array_change["deleteCount"]
|
|
646
|
+
if cell_delete_count == 0:
|
|
647
|
+
# Case 2
|
|
648
|
+
# Cell documents
|
|
649
|
+
for cell_document in structure["didOpen"]:
|
|
650
|
+
workspace.put_cell_document(
|
|
651
|
+
cell_document["uri"],
|
|
652
|
+
notebookDocument["uri"],
|
|
653
|
+
cell_document["languageId"],
|
|
654
|
+
cell_document["text"],
|
|
655
|
+
cell_document.get("version"),
|
|
656
|
+
)
|
|
657
|
+
# Cell metadata which is added to Notebook
|
|
658
|
+
workspace.add_notebook_cells(
|
|
659
|
+
notebookDocument["uri"],
|
|
660
|
+
notebook_cell_array_change["cells"],
|
|
661
|
+
start,
|
|
662
|
+
)
|
|
663
|
+
else:
|
|
664
|
+
# Case 3
|
|
665
|
+
# Cell documents
|
|
666
|
+
for cell_document in structure["didClose"]:
|
|
667
|
+
workspace.rm_document(cell_document["uri"])
|
|
668
|
+
workspace.publish_diagnostics(cell_document["uri"], [])
|
|
669
|
+
# Cell metadata which is removed from Notebook
|
|
670
|
+
workspace.remove_notebook_cells(
|
|
671
|
+
notebookDocument["uri"], start, cell_delete_count
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
data = cells.get("data")
|
|
675
|
+
if data:
|
|
676
|
+
# Case 4.1
|
|
677
|
+
for cell in data:
|
|
678
|
+
# update NotebookDocument.cells properties
|
|
679
|
+
pass
|
|
680
|
+
|
|
681
|
+
text_content = cells.get("textContent")
|
|
682
|
+
if text_content:
|
|
683
|
+
# Case 4.2
|
|
684
|
+
for cell in text_content:
|
|
685
|
+
cell_uri = cell["document"]["uri"]
|
|
686
|
+
# Even though the protocol says that `changes` is an array, we assume that it's always a single
|
|
687
|
+
# element array that contains the last change to the cell source.
|
|
688
|
+
workspace.update_document(cell_uri, cell["changes"][0])
|
|
689
|
+
self.lint(notebookDocument["uri"], is_saved=True)
|
|
690
|
+
|
|
691
|
+
def m_text_document__did_close(self, textDocument=None, **_kwargs) -> None:
|
|
692
|
+
workspace = self._match_uri_to_workspace(textDocument["uri"])
|
|
693
|
+
workspace.publish_diagnostics(textDocument["uri"], [])
|
|
694
|
+
workspace.rm_document(textDocument["uri"])
|
|
695
|
+
|
|
696
|
+
def m_text_document__did_open(self, textDocument=None, **_kwargs) -> None:
|
|
697
|
+
workspace = self._match_uri_to_workspace(textDocument["uri"])
|
|
698
|
+
workspace.put_document(
|
|
699
|
+
textDocument["uri"],
|
|
700
|
+
textDocument["text"],
|
|
701
|
+
version=textDocument.get("version"),
|
|
702
|
+
)
|
|
703
|
+
self._hook("pylsp_document_did_open", textDocument["uri"])
|
|
704
|
+
self.lint(textDocument["uri"], is_saved=True)
|
|
705
|
+
|
|
706
|
+
def m_text_document__did_change(
|
|
707
|
+
self, contentChanges=None, textDocument=None, **_kwargs
|
|
708
|
+
) -> None:
|
|
709
|
+
workspace = self._match_uri_to_workspace(textDocument["uri"])
|
|
710
|
+
for change in contentChanges:
|
|
711
|
+
workspace.update_document(
|
|
712
|
+
textDocument["uri"], change, version=textDocument.get("version")
|
|
713
|
+
)
|
|
714
|
+
self.lint(textDocument["uri"], is_saved=False)
|
|
715
|
+
|
|
716
|
+
def m_text_document__did_save(self, textDocument=None, **_kwargs) -> None:
|
|
717
|
+
self.lint(textDocument["uri"], is_saved=True)
|
|
718
|
+
self.document_did_save(textDocument["uri"])
|
|
719
|
+
|
|
720
|
+
def m_text_document__code_action(
|
|
721
|
+
self, textDocument=None, range=None, context=None, **_kwargs
|
|
722
|
+
):
|
|
723
|
+
return self.code_actions(textDocument["uri"], range, context)
|
|
724
|
+
|
|
725
|
+
def m_text_document__code_lens(self, textDocument=None, **_kwargs):
|
|
726
|
+
return self.code_lens(textDocument["uri"])
|
|
727
|
+
|
|
728
|
+
def _cell_document__completion(self, cellDocument, position=None, **_kwargs):
|
|
729
|
+
workspace = self._match_uri_to_workspace(cellDocument.notebook_uri)
|
|
730
|
+
notebookDocument = workspace.get_maybe_document(cellDocument.notebook_uri)
|
|
731
|
+
if notebookDocument is None:
|
|
732
|
+
raise ValueError("Invalid notebook document")
|
|
733
|
+
|
|
734
|
+
cell_data = notebookDocument.cell_data()
|
|
735
|
+
|
|
736
|
+
# Concatenate all cells to be a single temporary document
|
|
737
|
+
total_source = "\n".join(data["source"] for data in cell_data.values())
|
|
738
|
+
with workspace.temp_document(total_source) as temp_uri:
|
|
739
|
+
# update position to be the position in the temp document
|
|
740
|
+
if position is not None:
|
|
741
|
+
position["line"] += cell_data[cellDocument.uri]["line_start"]
|
|
742
|
+
|
|
743
|
+
completions = self.completions(temp_uri, position)
|
|
744
|
+
|
|
745
|
+
# Translate temp_uri locations to cell document locations
|
|
746
|
+
for item in completions.get("items", []):
|
|
747
|
+
if item.get("data", {}).get("doc_uri") == temp_uri:
|
|
748
|
+
item["data"]["doc_uri"] = cellDocument.uri
|
|
749
|
+
|
|
750
|
+
# Copy LAST_JEDI_COMPLETIONS to cell document so that completionItem/resolve will work
|
|
751
|
+
tempDocument = workspace.get_document(temp_uri)
|
|
752
|
+
cellDocument.shared_data["LAST_JEDI_COMPLETIONS"] = (
|
|
753
|
+
tempDocument.shared_data.get("LAST_JEDI_COMPLETIONS", None)
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
return completions
|
|
757
|
+
|
|
758
|
+
def m_text_document__completion(self, textDocument=None, position=None, **_kwargs):
|
|
759
|
+
# textDocument here is just a dict with a uri
|
|
760
|
+
workspace = self._match_uri_to_workspace(textDocument["uri"])
|
|
761
|
+
document = workspace.get_document(textDocument["uri"])
|
|
762
|
+
if isinstance(document, Cell):
|
|
763
|
+
return self._cell_document__completion(document, position, **_kwargs)
|
|
764
|
+
return self.completions(textDocument["uri"], position)
|
|
765
|
+
|
|
766
|
+
def _cell_document__definition(self, cellDocument, position=None, **_kwargs):
|
|
767
|
+
workspace = self._match_uri_to_workspace(cellDocument.notebook_uri)
|
|
768
|
+
notebookDocument = workspace.get_maybe_document(cellDocument.notebook_uri)
|
|
769
|
+
if notebookDocument is None:
|
|
770
|
+
raise ValueError("Invalid notebook document")
|
|
771
|
+
|
|
772
|
+
cell_data = notebookDocument.cell_data()
|
|
773
|
+
|
|
774
|
+
# Concatenate all cells to be a single temporary document
|
|
775
|
+
total_source = "\n".join(data["source"] for data in cell_data.values())
|
|
776
|
+
with workspace.temp_document(total_source) as temp_uri:
|
|
777
|
+
# update position to be the position in the temp document
|
|
778
|
+
if position is not None:
|
|
779
|
+
position["line"] += cell_data[cellDocument.uri]["line_start"]
|
|
780
|
+
|
|
781
|
+
definitions = self.definitions(temp_uri, position)
|
|
782
|
+
|
|
783
|
+
# Translate temp_uri locations to cell document locations
|
|
784
|
+
for definition in definitions:
|
|
785
|
+
if definition["uri"] == temp_uri:
|
|
786
|
+
# Find the cell the start line is in and adjust the uri and line numbers
|
|
787
|
+
for cell_uri, data in cell_data.items():
|
|
788
|
+
if (
|
|
789
|
+
data["line_start"]
|
|
790
|
+
<= definition["range"]["start"]["line"]
|
|
791
|
+
<= data["line_end"]
|
|
792
|
+
):
|
|
793
|
+
definition["uri"] = cell_uri
|
|
794
|
+
definition["range"]["start"]["line"] -= data["line_start"]
|
|
795
|
+
definition["range"]["end"]["line"] -= data["line_start"]
|
|
796
|
+
break
|
|
797
|
+
|
|
798
|
+
return definitions
|
|
799
|
+
|
|
800
|
+
def m_text_document__definition(self, textDocument=None, position=None, **_kwargs):
|
|
801
|
+
# textDocument here is just a dict with a uri
|
|
802
|
+
workspace = self._match_uri_to_workspace(textDocument["uri"])
|
|
803
|
+
document = workspace.get_document(textDocument["uri"])
|
|
804
|
+
if isinstance(document, Cell):
|
|
805
|
+
return self._cell_document__definition(document, position, **_kwargs)
|
|
806
|
+
return self.definitions(textDocument["uri"], position)
|
|
807
|
+
|
|
808
|
+
def m_text_document__type_definition(
|
|
809
|
+
self, textDocument=None, position=None, **_kwargs
|
|
810
|
+
):
|
|
811
|
+
return self.type_definition(textDocument["uri"], position)
|
|
812
|
+
|
|
813
|
+
def m_text_document__document_highlight(
|
|
814
|
+
self, textDocument=None, position=None, **_kwargs
|
|
815
|
+
):
|
|
816
|
+
return self.highlight(textDocument["uri"], position)
|
|
817
|
+
|
|
818
|
+
def m_text_document__hover(self, textDocument=None, position=None, **_kwargs):
|
|
819
|
+
return self.hover(textDocument["uri"], position)
|
|
820
|
+
|
|
821
|
+
def m_text_document__document_symbol(self, textDocument=None, **_kwargs):
|
|
822
|
+
return self.document_symbols(textDocument["uri"])
|
|
823
|
+
|
|
824
|
+
def m_text_document__formatting(self, textDocument=None, options=None, **_kwargs):
|
|
825
|
+
return self.format_document(textDocument["uri"], options)
|
|
826
|
+
|
|
827
|
+
def m_text_document__rename(
|
|
828
|
+
self, textDocument=None, position=None, newName=None, **_kwargs
|
|
829
|
+
):
|
|
830
|
+
return self.rename(textDocument["uri"], position, newName)
|
|
831
|
+
|
|
832
|
+
def m_text_document__folding_range(self, textDocument=None, **_kwargs):
|
|
833
|
+
return self.folding(textDocument["uri"])
|
|
834
|
+
|
|
835
|
+
def m_text_document__range_formatting(
|
|
836
|
+
self, textDocument=None, range=None, options=None, **_kwargs
|
|
837
|
+
):
|
|
838
|
+
return self.format_range(textDocument["uri"], range, options)
|
|
839
|
+
|
|
840
|
+
def m_text_document__references(
|
|
841
|
+
self, textDocument=None, position=None, context=None, **_kwargs
|
|
842
|
+
):
|
|
843
|
+
exclude_declaration = not context["includeDeclaration"]
|
|
844
|
+
return self.references(textDocument["uri"], position, exclude_declaration)
|
|
845
|
+
|
|
846
|
+
def m_text_document__signature_help(
|
|
847
|
+
self, textDocument=None, position=None, **_kwargs
|
|
848
|
+
):
|
|
849
|
+
return self.signature_help(textDocument["uri"], position)
|
|
850
|
+
|
|
851
|
+
def m_workspace__did_change_configuration(self, settings=None) -> None:
|
|
852
|
+
if self.config is not None:
|
|
853
|
+
self.config.update((settings or {}).get("pylsp", {}))
|
|
854
|
+
for workspace in self.workspaces.values():
|
|
855
|
+
workspace.update_config(settings)
|
|
856
|
+
self._hook("pylsp_workspace_configuration_changed")
|
|
857
|
+
for doc_uri in workspace.documents:
|
|
858
|
+
self.lint(doc_uri, is_saved=False)
|
|
859
|
+
|
|
860
|
+
def m_workspace__did_change_workspace_folders(self, event=None, **_kwargs):
|
|
861
|
+
if event is None:
|
|
862
|
+
return
|
|
863
|
+
added = event.get("added", [])
|
|
864
|
+
removed = event.get("removed", [])
|
|
865
|
+
|
|
866
|
+
for removed_info in removed:
|
|
867
|
+
if "uri" in removed_info:
|
|
868
|
+
removed_uri = removed_info["uri"]
|
|
869
|
+
self.workspaces.pop(removed_uri, None)
|
|
870
|
+
|
|
871
|
+
for added_info in added:
|
|
872
|
+
if "uri" in added_info:
|
|
873
|
+
added_uri = added_info["uri"]
|
|
874
|
+
workspace_config = config.Config(
|
|
875
|
+
added_uri,
|
|
876
|
+
self.config._init_opts,
|
|
877
|
+
self.config._process_id,
|
|
878
|
+
self.config._capabilities,
|
|
879
|
+
)
|
|
880
|
+
workspace_config.update(self.config._settings)
|
|
881
|
+
self.workspaces[added_uri] = Workspace(
|
|
882
|
+
added_uri, self._endpoint, workspace_config
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
root_workspace_removed = any(
|
|
886
|
+
removed_info["uri"] == self.root_uri for removed_info in removed
|
|
887
|
+
)
|
|
888
|
+
workspace_added = len(added) > 0 and "uri" in added[0]
|
|
889
|
+
if root_workspace_removed and workspace_added:
|
|
890
|
+
added_uri = added[0]["uri"]
|
|
891
|
+
self.root_uri = added_uri
|
|
892
|
+
new_root_workspace = self.workspaces[added_uri]
|
|
893
|
+
self.config = new_root_workspace._config
|
|
894
|
+
self.workspace = new_root_workspace
|
|
895
|
+
elif root_workspace_removed:
|
|
896
|
+
# NOTE: Removing the root workspace can only happen when the server
|
|
897
|
+
# is closed, thus the else condition of this if can never happen.
|
|
898
|
+
if self.workspaces:
|
|
899
|
+
log.debug("Root workspace deleted!")
|
|
900
|
+
available_workspaces = sorted(self.workspaces)
|
|
901
|
+
first_workspace = available_workspaces[0]
|
|
902
|
+
new_root_workspace = self.workspaces[first_workspace]
|
|
903
|
+
self.root_uri = first_workspace
|
|
904
|
+
self.config = new_root_workspace._config
|
|
905
|
+
self.workspace = new_root_workspace
|
|
906
|
+
|
|
907
|
+
# Migrate documents that are on the root workspace and have a better
|
|
908
|
+
# match now
|
|
909
|
+
doc_uris = list(self.workspace._docs.keys())
|
|
910
|
+
for uri in doc_uris:
|
|
911
|
+
doc = self.workspace._docs.pop(uri)
|
|
912
|
+
new_workspace = self._match_uri_to_workspace(uri)
|
|
913
|
+
new_workspace._docs[uri] = doc
|
|
914
|
+
|
|
915
|
+
def m_workspace__did_change_watched_files(self, changes=None, **_kwargs):
|
|
916
|
+
changed_py_files = set()
|
|
917
|
+
config_changed = False
|
|
918
|
+
for d in changes or []:
|
|
919
|
+
if d["uri"].endswith(PYTHON_FILE_EXTENSIONS):
|
|
920
|
+
changed_py_files.add(d["uri"])
|
|
921
|
+
elif d["uri"].endswith(CONFIG_FILEs):
|
|
922
|
+
config_changed = True
|
|
923
|
+
|
|
924
|
+
if config_changed:
|
|
925
|
+
self.config.settings.cache_clear()
|
|
926
|
+
elif not changed_py_files:
|
|
927
|
+
# Only externally changed python files and lint configs may result in changed diagnostics.
|
|
928
|
+
return
|
|
929
|
+
|
|
930
|
+
for workspace in self.workspaces.values():
|
|
931
|
+
for doc_uri in workspace.documents:
|
|
932
|
+
# Changes in doc_uri are already handled by m_text_document__did_save
|
|
933
|
+
if doc_uri not in changed_py_files:
|
|
934
|
+
self.lint(doc_uri, is_saved=False)
|
|
935
|
+
|
|
936
|
+
def m_workspace__execute_command(self, command=None, arguments=None):
|
|
937
|
+
return self.execute_command(command, arguments)
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
def flatten(list_of_lists):
|
|
941
|
+
return [item for lst in list_of_lists for item in lst]
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
def merge(list_of_dicts):
|
|
945
|
+
return {k: v for dictionary in list_of_dicts for k, v in dictionary.items()}
|