Skip to content

Commit 3f08d8c

Browse files
authored
Notebook protocol go-to-definition support (#408)
1 parent ceb8af2 commit 3f08d8c

File tree

4 files changed

+157
-5
lines changed

4 files changed

+157
-5
lines changed

pylsp/python_lsp.py

+42-1
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__)
@@ -541,6 +541,7 @@ def m_notebook_document__did_open(
541541
for cell in cellTextDocuments or []:
542542
workspace.put_cell_document(
543543
cell["uri"],
544+
notebookDocument["uri"],
544545
cell["languageId"],
545546
cell["text"],
546547
version=cell.get("version"),
@@ -593,6 +594,7 @@ def m_notebook_document__did_change(
593594
for cell_document in structure["didOpen"]:
594595
workspace.put_cell_document(
595596
cell_document["uri"],
597+
notebookDocument["uri"],
596598
cell_document["languageId"],
597599
cell_document["text"],
598600
cell_document.get("version"),
@@ -671,7 +673,46 @@ def m_text_document__code_lens(self, textDocument=None, **_kwargs):
671673
def m_text_document__completion(self, textDocument=None, position=None, **_kwargs):
672674
return self.completions(textDocument["uri"], position)
673675

676+
def _cell_document__definition(self, cellDocument, position=None, **_kwargs):
677+
workspace = self._match_uri_to_workspace(cellDocument.notebook_uri)
678+
notebookDocument = workspace.get_maybe_document(cellDocument.notebook_uri)
679+
if notebookDocument is None:
680+
raise ValueError("Invalid notebook document")
681+
682+
cell_data = notebookDocument.cell_data()
683+
684+
# Concatenate all cells to be a single temporary document
685+
total_source = "\n".join(data["source"] for data in cell_data.values())
686+
with workspace.temp_document(total_source) as temp_uri:
687+
# update position to be the position in the temp document
688+
if position is not None:
689+
position["line"] += cell_data[cellDocument.uri]["line_start"]
690+
691+
definitions = self.definitions(temp_uri, position)
692+
693+
# Translate temp_uri locations to cell document locations
694+
for definition in definitions:
695+
if definition["uri"] == temp_uri:
696+
# Find the cell the start line is in and adjust the uri and line numbers
697+
for cell_uri, data in cell_data.items():
698+
if (
699+
data["line_start"]
700+
<= definition["range"]["start"]["line"]
701+
<= data["line_end"]
702+
):
703+
definition["uri"] = cell_uri
704+
definition["range"]["start"]["line"] -= data["line_start"]
705+
definition["range"]["end"]["line"] -= data["line_start"]
706+
break
707+
708+
return definitions
709+
674710
def m_text_document__definition(self, textDocument=None, position=None, **_kwargs):
711+
# textDocument here is just a dict with a uri
712+
workspace = self._match_uri_to_workspace(textDocument["uri"])
713+
document = workspace.get_document(textDocument["uri"])
714+
if isinstance(document, Cell):
715+
return self._cell_document__definition(document, position, **_kwargs)
675716
return self.definitions(textDocument["uri"], position)
676717

677718
def m_text_document__document_highlight(

pylsp/workspace.py

+41-3
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,17 @@ def put_notebook_document(
130130
doc_uri, notebook_type, cells, version, metadata
131131
)
132132

133+
@contextmanager
134+
def temp_document(self, source, path=None):
135+
if path is None:
136+
path = self.root_path
137+
uri = uris.from_fs_path(os.path.join(path, str(uuid.uuid4())))
138+
try:
139+
self.put_document(uri, source)
140+
yield uri
141+
finally:
142+
self.rm_document(uri)
143+
133144
def add_notebook_cells(self, doc_uri, cells, start):
134145
self._docs[doc_uri].add_cells(cells, start)
135146

@@ -139,9 +150,11 @@ def remove_notebook_cells(self, doc_uri, start, delete_count):
139150
def update_notebook_metadata(self, doc_uri, metadata):
140151
self._docs[doc_uri].metadata = metadata
141152

142-
def put_cell_document(self, doc_uri, language_id, source, version=None):
153+
def put_cell_document(
154+
self, doc_uri, notebook_uri, language_id, source, version=None
155+
):
143156
self._docs[doc_uri] = self._create_cell_document(
144-
doc_uri, language_id, source, version
157+
doc_uri, notebook_uri, language_id, source, version
145158
)
146159

147160
def rm_document(self, doc_uri):
@@ -340,11 +353,14 @@ def _create_notebook_document(
340353
metadata=metadata,
341354
)
342355

343-
def _create_cell_document(self, doc_uri, language_id, source=None, version=None):
356+
def _create_cell_document(
357+
self, doc_uri, notebook_uri, language_id, source=None, version=None
358+
):
344359
# TODO: remove what is unnecessary here.
345360
path = uris.to_fs_path(doc_uri)
346361
return Cell(
347362
doc_uri,
363+
notebook_uri=notebook_uri,
348364
language_id=language_id,
349365
workspace=self,
350366
source=source,
@@ -585,6 +601,26 @@ def add_cells(self, new_cells: List, start: int) -> None:
585601
def remove_cells(self, start: int, delete_count: int) -> None:
586602
del self.cells[start : start + delete_count]
587603

604+
def cell_data(self):
605+
"""Extract current cell data.
606+
607+
Returns a dict (ordered by cell position) where the key is the cell uri and the
608+
value is a dict with line_start, line_end, and source attributes.
609+
"""
610+
cell_data = {}
611+
offset = 0
612+
for cell in self.cells:
613+
cell_uri = cell["document"]
614+
cell_document = self.workspace.get_cell_document(cell_uri)
615+
num_lines = cell_document.line_count
616+
cell_data[cell_uri] = {
617+
"line_start": offset,
618+
"line_end": offset + num_lines - 1,
619+
"source": cell_document.source,
620+
}
621+
offset += num_lines
622+
return cell_data
623+
588624

589625
class Cell(Document):
590626
"""
@@ -599,6 +635,7 @@ class Cell(Document):
599635
def __init__(
600636
self,
601637
uri,
638+
notebook_uri,
602639
language_id,
603640
workspace,
604641
source=None,
@@ -611,6 +648,7 @@ def __init__(
611648
uri, workspace, source, version, local, extra_sys_path, rope_project_builder
612649
)
613650
self.language_id = language_id
651+
self.notebook_uri = notebook_uri
614652

615653
@property
616654
@lock

test/test_notebook_document.py

+72
Original file line numberDiff line numberDiff line change
@@ -544,3 +544,75 @@ def test_notebook__did_close(
544544
)
545545
wait_for_condition(lambda: mock_notify.call_count >= 2)
546546
assert len(server.workspace.documents) == 0
547+
548+
549+
@pytest.mark.skipif(IS_WIN, reason="Flaky on Windows")
550+
def test_notebook_definition(client_server_pair):
551+
client, server = client_server_pair
552+
client._endpoint.request(
553+
"initialize",
554+
{
555+
"processId": 1234,
556+
"rootPath": os.path.dirname(__file__),
557+
"initializationOptions": {},
558+
},
559+
).result(timeout=CALL_TIMEOUT_IN_SECONDS)
560+
561+
# Open notebook
562+
with patch.object(server._endpoint, "notify") as mock_notify:
563+
client._endpoint.notify(
564+
"notebookDocument/didOpen",
565+
{
566+
"notebookDocument": {
567+
"uri": "notebook_uri",
568+
"notebookType": "jupyter-notebook",
569+
"cells": [
570+
{
571+
"kind": NotebookCellKind.Code,
572+
"document": "cell_1_uri",
573+
},
574+
{
575+
"kind": NotebookCellKind.Code,
576+
"document": "cell_2_uri",
577+
},
578+
],
579+
},
580+
"cellTextDocuments": [
581+
{
582+
"uri": "cell_1_uri",
583+
"languageId": "python",
584+
"text": "y=2\nx=1",
585+
},
586+
{
587+
"uri": "cell_2_uri",
588+
"languageId": "python",
589+
"text": "x",
590+
},
591+
],
592+
},
593+
)
594+
# wait for expected diagnostics messages
595+
wait_for_condition(lambda: mock_notify.call_count >= 2)
596+
assert len(server.workspace.documents) == 3
597+
for uri in ["cell_1_uri", "cell_2_uri", "notebook_uri"]:
598+
assert uri in server.workspace.documents
599+
600+
future = client._endpoint.request(
601+
"textDocument/definition",
602+
{
603+
"textDocument": {
604+
"uri": "cell_2_uri",
605+
},
606+
"position": {"line": 0, "character": 1},
607+
},
608+
)
609+
result = future.result(CALL_TIMEOUT_IN_SECONDS)
610+
assert result == [
611+
{
612+
"uri": "cell_1_uri",
613+
"range": {
614+
"start": {"line": 1, "character": 0},
615+
"end": {"line": 1, "character": 1},
616+
},
617+
}
618+
]

test/test_workspace.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88

99
DOC_URI = uris.from_fs_path(__file__)
10+
NOTEBOOK_URI = uris.from_fs_path("notebook_uri")
1011

1112

1213
def path_as_uri(path):
@@ -29,7 +30,7 @@ def test_put_notebook_document(pylsp):
2930

3031

3132
def test_put_cell_document(pylsp):
32-
pylsp.workspace.put_cell_document(DOC_URI, "python", "content")
33+
pylsp.workspace.put_cell_document(DOC_URI, NOTEBOOK_URI, "python", "content")
3334
assert DOC_URI in pylsp.workspace._docs
3435

3536

0 commit comments

Comments
 (0)