|
15 | 15 | import sysconfig |
16 | 16 | import traceback |
17 | 17 | from typing import Any, Callable, Dict, List, Optional, Sequence, Union |
| 18 | +from urllib.parse import urlparse, urlunparse |
18 | 19 |
|
19 | 20 |
|
20 | 21 | # ********************************************************** |
@@ -74,11 +75,55 @@ def update_environ_path() -> None: |
74 | 75 | RUNNER = pathlib.Path(__file__).parent / "runner.py" |
75 | 76 |
|
76 | 77 | MAX_WORKERS = 5 |
| 78 | +NOTEBOOK_SYNC_OPTIONS = lsp.NotebookDocumentSyncOptions( |
| 79 | + notebook_selector=[ |
| 80 | + lsp.NotebookDocumentFilterWithNotebook( |
| 81 | + notebook="jupyter-notebook", |
| 82 | + cells=[ |
| 83 | + lsp.NotebookCellLanguage(language="python"), |
| 84 | + ], |
| 85 | + ), |
| 86 | + lsp.NotebookDocumentFilterWithNotebook( |
| 87 | + notebook="interactive", |
| 88 | + cells=[ |
| 89 | + lsp.NotebookCellLanguage(language="python"), |
| 90 | + ], |
| 91 | + ), |
| 92 | + ], |
| 93 | + save=True, |
| 94 | +) |
77 | 95 | LSP_SERVER = LanguageServer( |
78 | | - name="pylint-server", version="v0.1.0", max_workers=MAX_WORKERS |
| 96 | + name="pylint-server", |
| 97 | + version="v0.1.0", |
| 98 | + max_workers=MAX_WORKERS, |
| 99 | + notebook_document_sync=NOTEBOOK_SYNC_OPTIONS, |
79 | 100 | ) |
80 | 101 |
|
81 | 102 |
|
| 103 | +def _get_document_path(document: str) -> str: |
| 104 | + """Returns the filesystem path for a document. |
| 105 | +
|
| 106 | + Examples: |
| 107 | + file:///path/to/file.py -> /path/to/file.py |
| 108 | + vscode-notebook-cell:/path/to/notebook.ipynb#C00001 -> /path/to/notebook.ipynb |
| 109 | + """ |
| 110 | + if not document.startswith("file:"): |
| 111 | + parsed = urlparse(document) |
| 112 | + file_uri = urlunparse( |
| 113 | + ( |
| 114 | + "file", |
| 115 | + parsed.netloc, |
| 116 | + parsed.path, |
| 117 | + parsed.params, |
| 118 | + parsed.query, |
| 119 | + "", |
| 120 | + ) |
| 121 | + ) |
| 122 | + if result := uris.to_fs_path(file_uri): |
| 123 | + return result |
| 124 | + return uris.to_fs_path(document) or document |
| 125 | + |
| 126 | + |
82 | 127 | # ********************************************************** |
83 | 128 | # Tool specific code goes below this. |
84 | 129 | # ********************************************************** |
@@ -143,8 +188,92 @@ def did_change(params: lsp.DidChangeTextDocumentParams) -> None: |
143 | 188 | ) |
144 | 189 |
|
145 | 190 |
|
146 | | -def _linting_helper(document: workspace.TextDocument) -> list[lsp.Diagnostic]: |
| 191 | +@LSP_SERVER.feature(lsp.NOTEBOOK_DOCUMENT_DID_OPEN) |
| 192 | +def notebook_did_open(params: lsp.DidOpenNotebookDocumentParams) -> None: |
| 193 | + """Run diagnostics on each code cell when a notebook is opened.""" |
| 194 | + nb = LSP_SERVER.workspace.get_notebook_document( |
| 195 | + notebook_uri=params.notebook_document.uri |
| 196 | + ) |
| 197 | + if nb is None: |
| 198 | + return |
| 199 | + for cell in nb.cells: |
| 200 | + if cell.kind == lsp.NotebookCellKind.Code and cell.document is not None: |
| 201 | + _lint_notebook_cell(cell.document) |
| 202 | + |
| 203 | + |
| 204 | +@LSP_SERVER.feature(lsp.NOTEBOOK_DOCUMENT_DID_CHANGE) |
| 205 | +def notebook_did_change(params: lsp.DidChangeNotebookDocumentParams) -> None: |
| 206 | + """Re-lint cells whose text changed or that were newly added.""" |
| 207 | + if params.change is None or params.change.cells is None: |
| 208 | + return |
| 209 | + |
| 210 | + for cell_content in params.change.cells.text_content or []: |
| 211 | + if cell_content.document: |
| 212 | + _lint_notebook_cell(cell_content.document.uri) |
| 213 | + |
| 214 | + structure = params.change.cells.structure |
| 215 | + if structure and structure.did_open: |
| 216 | + for cell_document in structure.did_open: |
| 217 | + _lint_notebook_cell(cell_document.uri) |
| 218 | + |
| 219 | + if structure and structure.did_close: |
| 220 | + for cell_document in structure.did_close: |
| 221 | + _clear_notebook_cell_diagnostics(cell_document.uri) |
| 222 | + |
| 223 | + |
| 224 | +@LSP_SERVER.feature(lsp.NOTEBOOK_DOCUMENT_DID_SAVE) |
| 225 | +def notebook_did_save(params: lsp.DidSaveNotebookDocumentParams) -> None: |
| 226 | + """Re-lint all cells when a notebook is saved.""" |
| 227 | + nb = LSP_SERVER.workspace.get_notebook_document( |
| 228 | + notebook_uri=params.notebook_document.uri |
| 229 | + ) |
| 230 | + if nb is None: |
| 231 | + return |
| 232 | + for cell in nb.cells: |
| 233 | + if cell.kind == lsp.NotebookCellKind.Code and cell.document is not None: |
| 234 | + _lint_notebook_cell(cell.document) |
| 235 | + |
| 236 | + |
| 237 | +@LSP_SERVER.feature(lsp.NOTEBOOK_DOCUMENT_DID_CLOSE) |
| 238 | +def notebook_did_close(params: lsp.DidCloseNotebookDocumentParams) -> None: |
| 239 | + """Clear diagnostics for all cells when the notebook is closed.""" |
| 240 | + for cell_doc in params.cell_text_documents: |
| 241 | + _clear_notebook_cell_diagnostics(cell_doc.uri) |
| 242 | + |
| 243 | + |
| 244 | +def _lint_notebook_cell(cell_uri: str) -> None: |
| 245 | + """Lint a single notebook cell and publish its diagnostics.""" |
| 246 | + document = LSP_SERVER.workspace.get_text_document(cell_uri) |
| 247 | + if document is None: |
| 248 | + return |
| 249 | + # Update path as pygls generates an invalid path. |
| 250 | + # TODO: Remove when fixed. # pylint: disable=fixme |
| 251 | + document.path = _get_document_path(cell_uri) |
| 252 | + # Linting is only supported for python cells in notebooks. |
| 253 | + if document.language_id != "python": |
| 254 | + return |
| 255 | + diagnostics: list[lsp.Diagnostic] = _linting_helper(document, is_notebook=True) |
| 256 | + LSP_SERVER.text_document_publish_diagnostics( |
| 257 | + lsp.PublishDiagnosticsParams(uri=cell_uri, diagnostics=diagnostics) |
| 258 | + ) |
| 259 | + |
| 260 | + |
| 261 | +def _clear_notebook_cell_diagnostics(cell_uri: str) -> None: |
| 262 | + """Clear diagnostics for a single notebook cell.""" |
| 263 | + LSP_SERVER.text_document_publish_diagnostics( |
| 264 | + lsp.PublishDiagnosticsParams(uri=cell_uri, diagnostics=[]) |
| 265 | + ) |
| 266 | + |
| 267 | + |
| 268 | +def _linting_helper( |
| 269 | + document: workspace.TextDocument, is_notebook: bool = False |
| 270 | +) -> list[lsp.Diagnostic]: |
147 | 271 | try: |
| 272 | + # Skip notebook cells — they are linted via _lint_notebook_cell which |
| 273 | + # passes cell content directly, not the notebook file path. |
| 274 | + if not is_notebook and str(document.uri).startswith("vscode-notebook-cell"): |
| 275 | + return [] |
| 276 | + |
148 | 277 | # Notify the client that linting has started for this document. |
149 | 278 | LSP_SERVER.protocol.notify( |
150 | 279 | "pylint/lintingStarted", |
@@ -776,10 +905,6 @@ def _run_tool_on_document( |
776 | 905 | log_warning("See `pylint.enabled` in settings.json to enabling linting.") |
777 | 906 | return None |
778 | 907 |
|
779 | | - if str(document.uri).startswith("vscode-notebook-cell"): |
780 | | - log_warning(f"Skipping notebook cells [Not Supported]: {str(document.uri)}") |
781 | | - return None |
782 | | - |
783 | 908 | if utils.is_stdlib_file(document.path): |
784 | 909 | log_warning( |
785 | 910 | f"Skipping standard library file (stdlib excluded): {document.path}" |
|
0 commit comments