6
6
import os
7
7
import socketserver
8
8
import threading
9
+ import uuid
10
+ from typing import List , Dict , Any
9
11
import ujson as json
10
12
11
13
from pylsp_jsonrpc .dispatchers import MethodDispatcher
14
16
15
17
from . import lsp , _utils , uris
16
18
from .config import config
17
- from .workspace import Workspace
19
+ from .workspace import Workspace , Document , Notebook
18
20
from ._version import __version__
19
21
20
22
log = logging .getLogger (__name__ )
@@ -266,6 +268,11 @@ def capabilities(self):
266
268
},
267
269
'openClose' : True ,
268
270
},
271
+ 'notebookDocumentSync' : {
272
+ 'notebookSelector' : {
273
+ 'cells' : [{'language' : 'python' }]
274
+ }
275
+ },
269
276
'workspace' : {
270
277
'workspaceFolders' : {
271
278
'supported' : True ,
@@ -375,11 +382,79 @@ def hover(self, doc_uri, position):
375
382
def lint (self , doc_uri , is_saved ):
376
383
# Since we're debounced, the document may no longer be open
377
384
workspace = self ._match_uri_to_workspace (doc_uri )
378
- if doc_uri in workspace .documents :
379
- workspace .publish_diagnostics (
380
- doc_uri ,
381
- flatten (self ._hook ('pylsp_lint' , doc_uri , is_saved = is_saved ))
382
- )
385
+ document_object = workspace .documents .get (doc_uri , None )
386
+ if isinstance (document_object , Document ):
387
+ self ._lint_text_document (doc_uri , workspace , is_saved = is_saved )
388
+ elif isinstance (document_object , Notebook ):
389
+ self ._lint_notebook_document (document_object , workspace )
390
+
391
+ def _lint_text_document (self , doc_uri , workspace , is_saved ):
392
+ workspace .publish_diagnostics (
393
+ doc_uri ,
394
+ flatten (self ._hook ('pylsp_lint' , doc_uri , is_saved = is_saved ))
395
+ )
396
+
397
+ def _lint_notebook_document (self , notebook_document , workspace ): # pylint: disable=too-many-locals
398
+ """
399
+ Lint a notebook document.
400
+
401
+ This is a bit more complicated than linting a text document, because we need to
402
+ send the entire notebook document to the pylsp_lint hook, but we need to send
403
+ the diagnostics back to the client on a per-cell basis.
404
+ """
405
+
406
+ # First, we create a temp TextDocument that represents the whole notebook
407
+ # contents. We'll use this to send to the pylsp_lint hook.
408
+ random_uri = str (uuid .uuid4 ())
409
+
410
+ # cell_list helps us map the diagnostics back to the correct cell later.
411
+ cell_list : List [Dict [str , Any ]] = []
412
+
413
+ offset = 0
414
+ total_source = ""
415
+ for cell in notebook_document .cells :
416
+ cell_uri = cell ['document' ]
417
+ cell_document = workspace .get_cell_document (cell_uri )
418
+
419
+ num_lines = cell_document .line_count
420
+
421
+ data = {
422
+ 'uri' : cell_uri ,
423
+ 'line_start' : offset ,
424
+ 'line_end' : offset + num_lines - 1 ,
425
+ 'source' : cell_document .source
426
+ }
427
+
428
+ cell_list .append (data )
429
+ if offset == 0 :
430
+ total_source = cell_document .source
431
+ else :
432
+ total_source += ("\n " + cell_document .source )
433
+
434
+ offset += num_lines
435
+
436
+ workspace .put_document (random_uri , total_source )
437
+
438
+ try :
439
+ document_diagnostics = flatten (self ._hook ('pylsp_lint' , random_uri , is_saved = True ))
440
+
441
+ # Now we need to map the diagnostics back to the correct cell and publish them.
442
+ # Note: this is O(n*m) in the number of cells and diagnostics, respectively.
443
+ for cell in cell_list :
444
+ cell_diagnostics = []
445
+ for diagnostic in document_diagnostics :
446
+ start_line = diagnostic ['range' ]['start' ]['line' ]
447
+ end_line = diagnostic ['range' ]['end' ]['line' ]
448
+
449
+ if start_line > cell ['line_end' ] or end_line < cell ['line_start' ]:
450
+ continue
451
+ diagnostic ['range' ]['start' ]['line' ] = start_line - cell ['line_start' ]
452
+ diagnostic ['range' ]['end' ]['line' ] = end_line - cell ['line_start' ]
453
+ cell_diagnostics .append (diagnostic )
454
+
455
+ workspace .publish_diagnostics (cell ['uri' ], cell_diagnostics )
456
+ finally :
457
+ workspace .rm_document (random_uri )
383
458
384
459
def references (self , doc_uri , position , exclude_declaration ):
385
460
return flatten (self ._hook (
@@ -399,6 +474,91 @@ def folding(self, doc_uri):
399
474
def m_completion_item__resolve (self , ** completionItem ):
400
475
return self .completion_item_resolve (completionItem )
401
476
477
+ def m_notebook_document__did_open (self , notebookDocument = None , cellTextDocuments = None , ** _kwargs ):
478
+ workspace = self ._match_uri_to_workspace (notebookDocument ['uri' ])
479
+ workspace .put_notebook_document (
480
+ notebookDocument ['uri' ],
481
+ notebookDocument ['notebookType' ],
482
+ cells = notebookDocument ['cells' ],
483
+ version = notebookDocument .get ('version' ),
484
+ metadata = notebookDocument .get ('metadata' )
485
+ )
486
+ for cell in (cellTextDocuments or []):
487
+ workspace .put_cell_document (cell ['uri' ], cell ['languageId' ], cell ['text' ], version = cell .get ('version' ))
488
+ self .lint (notebookDocument ['uri' ], is_saved = True )
489
+
490
+ def m_notebook_document__did_close (self , notebookDocument = None , cellTextDocuments = None , ** _kwargs ):
491
+ workspace = self ._match_uri_to_workspace (notebookDocument ['uri' ])
492
+ for cell in (cellTextDocuments or []):
493
+ workspace .publish_diagnostics (cell ['uri' ], [])
494
+ workspace .rm_document (cell ['uri' ])
495
+ workspace .rm_document (notebookDocument ['uri' ])
496
+
497
+ def m_notebook_document__did_change (self , notebookDocument = None , change = None , ** _kwargs ):
498
+ """
499
+ Changes to the notebook document.
500
+
501
+ This could be one of the following:
502
+ 1. Notebook metadata changed
503
+ 2. Cell(s) added
504
+ 3. Cell(s) deleted
505
+ 4. Cell(s) data changed
506
+ 4.1 Cell metadata changed
507
+ 4.2 Cell source changed
508
+ """
509
+ workspace = self ._match_uri_to_workspace (notebookDocument ['uri' ])
510
+
511
+ if change .get ('metadata' ):
512
+ # Case 1
513
+ workspace .update_notebook_metadata (notebookDocument ['uri' ], change .get ('metadata' ))
514
+
515
+ cells = change .get ('cells' )
516
+ if cells :
517
+ # Change to cells
518
+ structure = cells .get ('structure' )
519
+ if structure :
520
+ # Case 2 or 3
521
+ notebook_cell_array_change = structure ['array' ]
522
+ start = notebook_cell_array_change ['start' ]
523
+ cell_delete_count = notebook_cell_array_change ['deleteCount' ]
524
+ if cell_delete_count == 0 :
525
+ # Case 2
526
+ # Cell documents
527
+ for cell_document in structure ['didOpen' ]:
528
+ workspace .put_cell_document (
529
+ cell_document ['uri' ],
530
+ cell_document ['languageId' ],
531
+ cell_document ['text' ],
532
+ cell_document .get ('version' )
533
+ )
534
+ # Cell metadata which is added to Notebook
535
+ workspace .add_notebook_cells (notebookDocument ['uri' ], notebook_cell_array_change ['cells' ], start )
536
+ else :
537
+ # Case 3
538
+ # Cell documents
539
+ for cell_document in structure ['didClose' ]:
540
+ workspace .rm_document (cell_document ['uri' ])
541
+ workspace .publish_diagnostics (cell_document ['uri' ], [])
542
+ # Cell metadata which is removed from Notebook
543
+ workspace .remove_notebook_cells (notebookDocument ['uri' ], start , cell_delete_count )
544
+
545
+ data = cells .get ('data' )
546
+ if data :
547
+ # Case 4.1
548
+ for cell in data :
549
+ # update NotebookDocument.cells properties
550
+ pass
551
+
552
+ text_content = cells .get ('textContent' )
553
+ if text_content :
554
+ # Case 4.2
555
+ for cell in text_content :
556
+ cell_uri = cell ['document' ]['uri' ]
557
+ # Even though the protocol says that `changes` is an array, we assume that it's always a single
558
+ # element array that contains the last change to the cell source.
559
+ workspace .update_document (cell_uri , cell ['changes' ][0 ])
560
+ self .lint (notebookDocument ['uri' ], is_saved = True )
561
+
402
562
def m_text_document__did_close (self , textDocument = None , ** _kwargs ):
403
563
workspace = self ._match_uri_to_workspace (textDocument ['uri' ])
404
564
workspace .publish_diagnostics (textDocument ['uri' ], [])
0 commit comments