Skip to content

Commit fab7d91

Browse files
committed
Add go to definition support for notebook cell documents
1 parent 5434d1f commit fab7d91

File tree

4 files changed

+163
-9
lines changed

4 files changed

+163
-9
lines changed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,8 @@ Dev install
165165
166166
```
167167
# create conda env
168-
cc python-lsp-server
169-
ca python-lsp-server
168+
conda create python-lsp-server
169+
conda activate python-lsp-server
170170

171171
pip install ".[all]"
172172
pip install ".[websockets]"

pylsp/python_lsp.py

+76-3
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
from . import lsp, _utils, uris
1818
from .config import config
19-
from .workspace import Workspace, Document, Notebook
19+
from .workspace import Workspace, Document, Notebook, Cell
2020
from ._version import __version__
2121

2222
log = logging.getLogger(__name__)
@@ -480,7 +480,7 @@ def m_notebook_document__did_open(self, notebookDocument=None, cellTextDocuments
480480
cells=notebookDocument['cells'], version=notebookDocument.get('version'),
481481
metadata=notebookDocument.get('metadata'))
482482
for cell in (cellTextDocuments or []):
483-
workspace.put_cell_document(cell['uri'], cell['languageId'], cell['text'], version=cell.get('version'))
483+
workspace.put_cell_document(cell['uri'], notebookDocument['uri'], cell['languageId'], cell['text'], version=cell.get('version'))
484484
self.lint(notebookDocument['uri'], is_saved=True)
485485

486486
def m_notebook_document__did_close(self, notebookDocument=None, cellTextDocuments=None, **_kwargs):
@@ -521,7 +521,7 @@ def m_notebook_document__did_change(self, notebookDocument=None, change=None, **
521521
# Case 2
522522
# Cell documents
523523
for cell_document in structure['didOpen']:
524-
workspace.put_cell_document(cell_document['uri'], cell_document['languageId'],
524+
workspace.put_cell_document(cell_document['uri'], notebookDocument['uri'], cell_document['languageId'],
525525
cell_document['text'], cell_document.get('version'))
526526
# Cell metadata which is added to Notebook
527527
workspace.add_notebook_cells(notebookDocument['uri'], notebook_cell_array_change['cells'], start)
@@ -585,7 +585,80 @@ def m_text_document__code_lens(self, textDocument=None, **_kwargs):
585585
def m_text_document__completion(self, textDocument=None, position=None, **_kwargs):
586586
return self.completions(textDocument['uri'], position)
587587

588+
def _cell_document__definition(self, cellDocument=None, position=None, **_kwargs):
589+
# First, we create a temp TextDocument to send to the hook that represents the whole notebook
590+
# contents.
591+
workspace = self._match_uri_to_workspace(cellDocument.notebook_uri)
592+
notebookDocument = workspace.get_maybe_document(cellDocument.notebook_uri)
593+
if notebookDocument is None:
594+
raise ValueError("Invalid notebook document")
595+
596+
random_uri = str(uuid.uuid4())
597+
# cell_list helps us map the diagnostics back to the correct cell later.
598+
cell_list: List[Dict[str, Any]] = []
599+
600+
offset = 0
601+
total_source = ""
602+
for cell in notebookDocument.cells:
603+
cell_uri = cell['document']
604+
cell_document = workspace.get_cell_document(cell_uri)
605+
606+
num_lines = cell_document.line_count
607+
608+
data = {
609+
'uri': cell_uri,
610+
'line_start': offset,
611+
'line_end': offset + num_lines - 1,
612+
'source': cell_document.source
613+
}
614+
615+
if position is not None and cell_uri == cellDocument.uri:
616+
position['line'] += offset
617+
618+
cell_list.append(data)
619+
if offset == 0:
620+
total_source = cell_document.source
621+
else:
622+
total_source += ("\n" + cell_document.source)
623+
624+
offset += num_lines
625+
626+
# TODO: make a workspace temp document context manager that yields the random uri and cleans up afterwards
627+
workspace.put_document(random_uri, total_source)
628+
log.info(f'Making new document {random_uri}')
629+
try:
630+
definitions = self.definitions(random_uri, position)
631+
log.info(f'Got definitions: {definitions}')
632+
633+
# {
634+
# 'uri': uris.uri_with(document.uri, path=str(d.module_path)),
635+
# 'range': {
636+
# 'start': {'line': d.line - 1, 'character': d.column},
637+
# 'end': {'line': d.line - 1, 'character': d.column + len(d.name)},
638+
# }
639+
# }
640+
print(definitions)
641+
for definition in definitions:
642+
# TODO: a better test for if a definition is the random_uri
643+
if random_uri in definition['uri']:
644+
# Find the cell the start is in
645+
for cell in cell_list:
646+
# TODO: perhaps it is more correct to check definition['range']['end']['line'] <= cell['line_end'], but
647+
# that would mess up if a definition was split over cells
648+
if cell['line_start'] <= definition['range']['start']['line'] <= cell['line_end']:
649+
definition['uri'] = cell['uri']
650+
definition['range']['start']['line'] -= cell['line_start']
651+
definition['range']['end']['line'] -= cell['line_start']
652+
return definitions
653+
finally:
654+
workspace.rm_document(random_uri)
655+
588656
def m_text_document__definition(self, textDocument=None, position=None, **_kwargs):
657+
# textDocument here is just a dict with a uri
658+
workspace = self._match_uri_to_workspace(textDocument['uri'])
659+
document = workspace.get_document(textDocument['uri'])
660+
if isinstance(document, Cell):
661+
return self._cell_document__definition(document, position, **_kwargs)
589662
return self.definitions(textDocument['uri'], position)
590663

591664
def m_text_document__document_highlight(self, textDocument=None, position=None, **_kwargs):

pylsp/workspace.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,8 @@ def remove_notebook_cells(self, doc_uri, start, delete_count):
128128
def update_notebook_metadata(self, doc_uri, metadata):
129129
self._docs[doc_uri].metadata = metadata
130130

131-
def put_cell_document(self, doc_uri, language_id, source, version=None):
132-
self._docs[doc_uri] = self._create_cell_document(doc_uri, language_id, source, version)
131+
def put_cell_document(self, doc_uri, notebook_uri, language_id, source, version=None):
132+
self._docs[doc_uri] = self._create_cell_document(doc_uri, notebook_uri, language_id, source, version)
133133

134134
def rm_document(self, doc_uri):
135135
self._docs.pop(doc_uri)
@@ -288,11 +288,12 @@ def _create_notebook_document(self, doc_uri, notebook_type, cells, version=None,
288288
metadata=metadata
289289
)
290290

291-
def _create_cell_document(self, doc_uri, language_id, source=None, version=None):
291+
def _create_cell_document(self, doc_uri, notebook_uri, language_id, source=None, version=None):
292292
# TODO: remove what is unnecessary here.
293293
path = uris.to_fs_path(doc_uri)
294294
return Cell(
295295
doc_uri,
296+
notebook_uri=notebook_uri,
296297
language_id=language_id,
297298
workspace=self,
298299
source=source,
@@ -511,10 +512,11 @@ def remove_cells(self, start: int, delete_count: int) -> None:
511512
class Cell(Document):
512513
"""Represents a cell in a notebook."""
513514

514-
def __init__(self, uri, language_id, workspace, source=None, version=None, local=True, extra_sys_path=None,
515+
def __init__(self, uri, notebook_uri, language_id, workspace, source=None, version=None, local=True, extra_sys_path=None,
515516
rope_project_builder=None):
516517
super().__init__(uri, workspace, source, version, local, extra_sys_path, rope_project_builder)
517518
self.language_id = language_id
519+
self.notebook_uri = notebook_uri
518520

519521
@property
520522
@lock

test/test_notebook_document.py

+79
Original file line numberDiff line numberDiff line change
@@ -582,3 +582,82 @@ def test_notebook__did_close(
582582
)
583583
wait_for_condition(lambda: mock_notify.call_count >= 2)
584584
assert len(server.workspace.documents) == 0
585+
586+
def test_notebook_definition(
587+
client_server_pair,
588+
): # pylint: disable=redefined-outer-name
589+
client, server = client_server_pair
590+
client._endpoint.request(
591+
"initialize",
592+
{
593+
"processId": 1234,
594+
"rootPath": os.path.dirname(__file__),
595+
"initializationOptions": {},
596+
},
597+
).result(timeout=CALL_TIMEOUT_IN_SECONDS)
598+
599+
# Open notebook
600+
with patch.object(server._endpoint, "notify") as mock_notify:
601+
client._endpoint.notify(
602+
"notebookDocument/didOpen",
603+
{
604+
"notebookDocument": {
605+
"uri": "notebook_uri",
606+
"notebookType": "jupyter-notebook",
607+
"cells": [
608+
{
609+
"kind": NotebookCellKind.Code,
610+
"document": "cell_1_uri",
611+
},
612+
{
613+
"kind": NotebookCellKind.Code,
614+
"document": "cell_2_uri",
615+
},
616+
],
617+
},
618+
"cellTextDocuments": [
619+
{
620+
"uri": "cell_1_uri",
621+
"languageId": "python",
622+
"text": "y=2\nx=1",
623+
},
624+
{
625+
"uri": "cell_2_uri",
626+
"languageId": "python",
627+
"text": "x",
628+
},
629+
],
630+
},
631+
)
632+
# wait for expected diagnostics messages
633+
wait_for_condition(lambda: mock_notify.call_count >= 2)
634+
assert len(server.workspace.documents) == 3
635+
for uri in ["cell_1_uri", "cell_2_uri", "notebook_uri"]:
636+
assert uri in server.workspace.documents
637+
638+
future = client._endpoint.request(
639+
"textDocument/definition",
640+
{
641+
"textDocument": {
642+
"uri": "cell_2_uri",
643+
},
644+
"position": {
645+
"line": 0,
646+
"character": 1
647+
}
648+
},
649+
)
650+
result = future.result(CALL_TIMEOUT_IN_SECONDS)
651+
assert result == [{
652+
'uri': 'cell_1_uri',
653+
'range': {
654+
'start': {
655+
'line': 1,
656+
'character': 0
657+
},
658+
'end': {
659+
'line': 1,
660+
'character': 1
661+
}
662+
}
663+
}]

0 commit comments

Comments
 (0)