Skip to content

Commit 4faf965

Browse files
committed
Support notebooks
Fixes pappasam#324.
1 parent 112aa02 commit 4faf965

26 files changed

+3214
-79
lines changed

jedi_language_server/jedi_utils.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -148,22 +148,26 @@ def lsp_range(name: Name) -> Optional[Range]:
148148
)
149149

150150

151-
def lsp_location(name: Name) -> Optional[Location]:
151+
def lsp_location(name: Name, uri: Optional[str] = None) -> Optional[Location]:
152152
"""Get LSP location from Jedi definition."""
153-
module_path = name.module_path
154-
if module_path is None:
155-
return None
153+
if uri is None:
154+
module_path = name.module_path
155+
if module_path is None:
156+
return None
157+
uri = module_path.as_uri()
156158

157159
lsp = lsp_range(name)
158160
if lsp is None:
159161
return None
160162

161-
return Location(uri=module_path.as_uri(), range=lsp)
163+
return Location(uri=uri, range=lsp)
162164

163165

164-
def lsp_symbol_information(name: Name) -> Optional[SymbolInformation]:
166+
def lsp_symbol_information(
167+
name: Name, uri: Optional[str] = None
168+
) -> Optional[SymbolInformation]:
165169
"""Get LSP SymbolInformation from Jedi definition."""
166-
location = lsp_location(name)
170+
location = lsp_location(name, uri)
167171
if location is None:
168172
return None
169173

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
"""Utility functions for handling notebook documents."""
2+
3+
from collections import defaultdict
4+
from typing import (
5+
Dict,
6+
Iterable,
7+
List,
8+
NamedTuple,
9+
Optional,
10+
Union,
11+
)
12+
13+
import attrs
14+
from lsprotocol.types import (
15+
AnnotatedTextEdit,
16+
Location,
17+
NotebookDocument,
18+
OptionalVersionedTextDocumentIdentifier,
19+
Position,
20+
Range,
21+
TextDocumentEdit,
22+
TextEdit,
23+
)
24+
from pygls.workspace import TextDocument, Workspace
25+
26+
27+
def notebook_coordinate_mapper(
28+
workspace: Workspace,
29+
*,
30+
notebook_uri: Optional[str] = None,
31+
cell_uri: Optional[str] = None,
32+
) -> Optional["NotebookCoordinateMapper"]:
33+
notebook_document = workspace.get_notebook_document(
34+
notebook_uri=notebook_uri, cell_uri=cell_uri
35+
)
36+
if notebook_document is None:
37+
return None
38+
cells = [
39+
workspace.text_documents[cell.document]
40+
for cell in notebook_document.cells
41+
]
42+
return NotebookCoordinateMapper(notebook_document, cells)
43+
44+
45+
class DocumentPosition(NamedTuple):
46+
"""A position in a document."""
47+
48+
uri: str
49+
position: Position
50+
51+
52+
class DocumentTextEdit(NamedTuple):
53+
"""A text edit in a document."""
54+
55+
uri: str
56+
text_edit: Union[TextEdit, AnnotatedTextEdit]
57+
58+
59+
class NotebookCoordinateMapper:
60+
"""Maps positions between individual notebook cells and the concatenated notebook document."""
61+
62+
def __init__(
63+
self,
64+
notebook_document: NotebookDocument,
65+
cells: List[TextDocument],
66+
):
67+
self._document = notebook_document
68+
self._cells = cells
69+
70+
# Construct helper data structures.
71+
self._cell_by_uri: Dict[str, TextDocument] = {}
72+
self._cell_line_range_by_uri: Dict[str, range] = {}
73+
start_line = 0
74+
for index, cell in enumerate(self._cells):
75+
end_line = start_line + len(cell.lines)
76+
77+
self._cell_by_uri[cell.uri] = cell
78+
self._cell_line_range_by_uri[cell.uri] = range(
79+
start_line, end_line
80+
)
81+
82+
start_line = end_line
83+
84+
@property
85+
def source(self) -> str:
86+
"""Concatenated notebook source."""
87+
return "\n".join(cell.source for cell in self._cells)
88+
89+
def notebook_position(
90+
self, cell_uri: str, cell_position: Position
91+
) -> Position:
92+
"""Convert a cell position to a concatenated notebook position."""
93+
line = (
94+
self._cell_line_range_by_uri[cell_uri].start + cell_position.line
95+
)
96+
return Position(line=line, character=cell_position.character)
97+
98+
def notebook_range(self, cell_uri: str, cell_range: Range) -> Range:
99+
"""Convert a cell range to a concatenated notebook range."""
100+
start = self.notebook_position(cell_uri, cell_range.start)
101+
end = self.notebook_position(cell_uri, cell_range.end)
102+
return Range(start=start, end=end)
103+
104+
def cell_position(
105+
self, notebook_position: Position
106+
) -> Optional[DocumentPosition]:
107+
"""Convert a concatenated notebook position to a cell position."""
108+
for cell in self._cells:
109+
line_range = self._cell_line_range_by_uri[cell.uri]
110+
if notebook_position.line in line_range:
111+
line = notebook_position.line - line_range.start
112+
return DocumentPosition(
113+
uri=cell.uri,
114+
position=Position(
115+
line=line, character=notebook_position.character
116+
),
117+
)
118+
return None
119+
120+
def cell_range(self, notebook_range: Range) -> Optional[Location]:
121+
"""Convert a concatenated notebook range to a cell range.
122+
123+
Returns a `Location` to identify the cell that the range is in.
124+
"""
125+
start = self.cell_position(notebook_range.start)
126+
if start is None:
127+
return None
128+
129+
end = self.cell_position(notebook_range.end)
130+
if end is None:
131+
return None
132+
133+
if start.uri != end.uri:
134+
return None
135+
136+
return Location(
137+
uri=start.uri, range=Range(start=start.position, end=end.position)
138+
)
139+
140+
def cell_location(self, notebook_location: Location) -> Optional[Location]:
141+
"""Convert a concatenated notebook location to a cell location."""
142+
if notebook_location.uri != self._document.uri:
143+
return None
144+
return self.cell_range(notebook_location.range)
145+
146+
def cell_index(self, cell_uri: str) -> Optional[int]:
147+
"""Get the index of a cell by its URI."""
148+
for index, cell in enumerate(self._cells):
149+
if cell.uri == cell_uri:
150+
return index
151+
return None
152+
153+
def cell_text_edit(
154+
self, text_edit: Union[TextEdit, AnnotatedTextEdit]
155+
) -> Optional[DocumentTextEdit]:
156+
"""Convert a concatenated notebook text edit to a cell text edit."""
157+
location = self.cell_range(text_edit.range)
158+
if location is None:
159+
return None
160+
161+
return DocumentTextEdit(
162+
uri=location.uri,
163+
text_edit=attrs.evolve(text_edit, range=location.range),
164+
)
165+
166+
def cell_text_document_edits(
167+
self, text_document_edit: TextDocumentEdit
168+
) -> Iterable[TextDocumentEdit]:
169+
"""Convert a concatenated notebook text document edit to cell text document edits."""
170+
if text_document_edit.text_document.uri != self._document.uri:
171+
return
172+
173+
# Convert edits in the concatenated notebook to per-cell edits, grouped by cell URI.
174+
edits_by_uri: Dict[str, List[Union[TextEdit, AnnotatedTextEdit]]] = (
175+
defaultdict(list)
176+
)
177+
for text_edit in text_document_edit.edits:
178+
cell_text_edit = self.cell_text_edit(text_edit)
179+
if cell_text_edit is not None:
180+
edits_by_uri[cell_text_edit.uri].append(
181+
cell_text_edit.text_edit
182+
)
183+
184+
# Yield per-cell text document edits.
185+
for uri, edits in edits_by_uri.items():
186+
cell = self._cell_by_uri[uri]
187+
version = 0 if cell.version is None else cell.version
188+
yield TextDocumentEdit(
189+
text_document=OptionalVersionedTextDocumentIdentifier(
190+
uri=cell.uri, version=version
191+
),
192+
edits=edits,
193+
)
194+
195+
196+
def text_document_or_cell_locations(
197+
workspace: Workspace, locations: List[Location]
198+
) -> List[Location]:
199+
"""Convert concatenated notebook locations to cell locations, leaving text document locations as-is."""
200+
results = []
201+
for location in locations:
202+
mapper = notebook_coordinate_mapper(
203+
workspace, notebook_uri=location.uri
204+
)
205+
if mapper is not None:
206+
cell_location = mapper.cell_location(location)
207+
if cell_location is not None:
208+
location = cell_location
209+
210+
results.append(location)
211+
return results

0 commit comments

Comments
 (0)