66import os
77import socketserver
88import threading
9+ import uuid
10+ from typing import List , Dict , Any
911import ujson as json
1012
1113from pylsp_jsonrpc .dispatchers import MethodDispatcher
1416
1517from . import lsp , _utils , uris
1618from .config import config
17- from .workspace import Workspace
19+ from .workspace import Workspace , Document , Notebook
1820from ._version import __version__
1921
2022log = logging .getLogger (__name__ )
@@ -266,6 +268,11 @@ def capabilities(self):
266268 },
267269 'openClose' : True ,
268270 },
271+ 'notebookDocumentSync' : {
272+ 'notebookSelector' : {
273+ 'cells' : [{'language' : 'python' }]
274+ }
275+ },
269276 'workspace' : {
270277 'workspaceFolders' : {
271278 'supported' : True ,
@@ -375,11 +382,79 @@ def hover(self, doc_uri, position):
375382 def lint (self , doc_uri , is_saved ):
376383 # Since we're debounced, the document may no longer be open
377384 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 )
383458
384459 def references (self , doc_uri , position , exclude_declaration ):
385460 return flatten (self ._hook (
@@ -399,6 +474,91 @@ def folding(self, doc_uri):
399474 def m_completion_item__resolve (self , ** completionItem ):
400475 return self .completion_item_resolve (completionItem )
401476
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+
402562 def m_text_document__did_close (self , textDocument = None , ** _kwargs ):
403563 workspace = self ._match_uri_to_workspace (textDocument ['uri' ])
404564 workspace .publish_diagnostics (textDocument ['uri' ], [])
0 commit comments