1 |
|
2 |
|
3 |
|
4 | import { SessionContext } from '@jupyterlab/apputils';
|
5 | import { Cell, ICellModel } from '@jupyterlab/cells';
|
6 | import { IEditorMimeTypeService } from '@jupyterlab/codeeditor';
|
7 | import {
|
8 | Document,
|
9 | IAdapterOptions,
|
10 | IVirtualPosition,
|
11 | untilReady,
|
12 | VirtualDocument,
|
13 | WidgetLSPAdapter
|
14 | } from '@jupyterlab/lsp';
|
15 | import * as nbformat from '@jupyterlab/nbformat';
|
16 | import { IObservableList } from '@jupyterlab/observables';
|
17 | import { Session } from '@jupyterlab/services';
|
18 | import { PromiseDelegate } from '@lumino/coreutils';
|
19 | import { Signal } from '@lumino/signaling';
|
20 |
|
21 | import { NotebookPanel } from './panel';
|
22 | import { Notebook } from './widget';
|
23 | import { CellList } from './celllist';
|
24 |
|
25 | type ILanguageInfoMetadata = nbformat.ILanguageInfoMetadata;
|
26 |
|
27 | export class NotebookAdapter extends WidgetLSPAdapter<NotebookPanel> {
|
28 | constructor(
|
29 | public editorWidget: NotebookPanel,
|
30 | protected options: IAdapterOptions
|
31 | ) {
|
32 | super(editorWidget, options);
|
33 | this._editorToCell = new Map();
|
34 | this.editor = editorWidget.content;
|
35 | this._cellToEditor = new WeakMap();
|
36 | Promise.all([
|
37 | this.widget.context.sessionContext.ready,
|
38 | this.connectionManager.ready
|
39 | ])
|
40 | .then(async () => {
|
41 | await this.initOnceReady();
|
42 | this._readyDelegate.resolve();
|
43 | })
|
44 | .catch(console.error);
|
45 | }
|
46 |
|
47 | |
48 |
|
49 |
|
50 | readonly editor: Notebook;
|
51 |
|
52 | |
53 |
|
54 |
|
55 | get documentPath(): string {
|
56 | return this.widget.context.path;
|
57 | }
|
58 |
|
59 | |
60 |
|
61 |
|
62 | get mimeType(): string {
|
63 | let mimeType: string | string[];
|
64 | let languageMetadata = this.language_info();
|
65 | if (!languageMetadata || !languageMetadata.mimetype) {
|
66 |
|
67 | mimeType = this.widget.content.codeMimetype;
|
68 | } else {
|
69 | mimeType = languageMetadata.mimetype;
|
70 | }
|
71 | return Array.isArray(mimeType)
|
72 | ? mimeType[0] ?? IEditorMimeTypeService.defaultMimeType
|
73 | : mimeType;
|
74 | }
|
75 |
|
76 | |
77 |
|
78 |
|
79 | get languageFileExtension(): string | undefined {
|
80 | let languageMetadata = this.language_info();
|
81 | if (!languageMetadata || !languageMetadata.file_extension) {
|
82 | return;
|
83 | }
|
84 | return languageMetadata.file_extension.replace('.', '');
|
85 | }
|
86 |
|
87 | |
88 |
|
89 |
|
90 | get wrapperElement(): HTMLElement {
|
91 | return this.widget.node;
|
92 | }
|
93 |
|
94 | |
95 |
|
96 |
|
97 | get editors(): Document.ICodeBlockOptions[] {
|
98 | if (this.isDisposed) {
|
99 | return [];
|
100 | }
|
101 |
|
102 | let notebook = this.widget.content;
|
103 |
|
104 | this._editorToCell.clear();
|
105 |
|
106 | if (notebook.isDisposed) {
|
107 | return [];
|
108 | }
|
109 |
|
110 | return notebook.widgets.map(cell => {
|
111 | return {
|
112 | ceEditor: this._getCellEditor(cell),
|
113 | type: cell.model.type,
|
114 | value: cell.model.sharedModel.getSource()
|
115 | };
|
116 | });
|
117 | }
|
118 |
|
119 | |
120 |
|
121 |
|
122 | get activeEditor(): Document.IEditor | undefined {
|
123 | return this.editor.activeCell
|
124 | ? this._getCellEditor(this.editor.activeCell)
|
125 | : undefined;
|
126 | }
|
127 |
|
128 | |
129 |
|
130 |
|
131 | get ready(): Promise<void> {
|
132 | return this._readyDelegate.promise;
|
133 | }
|
134 |
|
135 | |
136 |
|
137 |
|
138 |
|
139 |
|
140 |
|
141 |
|
142 | getEditorIndexAt(position: IVirtualPosition): number {
|
143 | let cell = this._getCellAt(position);
|
144 | let notebook = this.widget.content;
|
145 | return notebook.widgets.findIndex(otherCell => {
|
146 | return cell === otherCell;
|
147 | });
|
148 | }
|
149 |
|
150 | |
151 |
|
152 |
|
153 |
|
154 |
|
155 | getEditorIndex(ceEditor: Document.IEditor): number {
|
156 | let cell = this._editorToCell.get(ceEditor)!;
|
157 | return this.editor.widgets.findIndex(otherCell => {
|
158 | return cell === otherCell;
|
159 | });
|
160 | }
|
161 |
|
162 | |
163 |
|
164 |
|
165 |
|
166 |
|
167 | getEditorWrapper(ceEditor: Document.IEditor): HTMLElement {
|
168 | let cell = this._editorToCell.get(ceEditor)!;
|
169 | return cell.node;
|
170 | }
|
171 |
|
172 | |
173 |
|
174 |
|
175 |
|
176 |
|
177 |
|
178 |
|
179 | async onKernelChanged(
|
180 | _session: SessionContext,
|
181 | change: Session.ISessionConnection.IKernelChangedArgs
|
182 | ): Promise<void> {
|
183 | if (!change.newValue) {
|
184 | return;
|
185 | }
|
186 | try {
|
187 |
|
188 | const oldLanguageInfo = this._languageInfo;
|
189 | await untilReady(this.isReady, -1);
|
190 | await this._updateLanguageInfo();
|
191 | const newLanguageInfo = this._languageInfo;
|
192 | if (
|
193 | oldLanguageInfo?.name != newLanguageInfo.name ||
|
194 | oldLanguageInfo?.mimetype != newLanguageInfo?.mimetype ||
|
195 | oldLanguageInfo?.file_extension != newLanguageInfo?.file_extension
|
196 | ) {
|
197 | console.log(
|
198 | `Changed to ${this._languageInfo.name} kernel, reconnecting`
|
199 | );
|
200 | this.reloadConnection();
|
201 | } else {
|
202 | console.log(
|
203 | 'Keeping old LSP connection as the new kernel uses the same langauge'
|
204 | );
|
205 | }
|
206 | } catch (err) {
|
207 | console.warn(err);
|
208 |
|
209 | this.reloadConnection();
|
210 | }
|
211 | }
|
212 |
|
213 | |
214 |
|
215 |
|
216 | dispose(): void {
|
217 | if (this.isDisposed) {
|
218 | return;
|
219 | }
|
220 | this.widget.context.sessionContext.kernelChanged.disconnect(
|
221 | this.onKernelChanged,
|
222 | this
|
223 | );
|
224 | this.widget.content.activeCellChanged.disconnect(
|
225 | this._activeCellChanged,
|
226 | this
|
227 | );
|
228 |
|
229 | super.dispose();
|
230 |
|
231 |
|
232 | this._editorToCell.clear();
|
233 | Signal.clearData(this);
|
234 | }
|
235 |
|
236 | |
237 |
|
238 |
|
239 | isReady(): boolean {
|
240 | return (
|
241 | !this.widget.isDisposed &&
|
242 | this.widget.context.isReady &&
|
243 | this.widget.content.isVisible &&
|
244 | this.widget.content.widgets.length > 0 &&
|
245 | this.widget.context.sessionContext.session?.kernel != null
|
246 | );
|
247 | }
|
248 |
|
249 | |
250 |
|
251 |
|
252 |
|
253 |
|
254 |
|
255 | async handleCellChange(
|
256 | cells: CellList,
|
257 | change: IObservableList.IChangedArgs<ICellModel>
|
258 | ): Promise<void> {
|
259 | let cellsAdded: ICellModel[] = [];
|
260 | let cellsRemoved: ICellModel[] = [];
|
261 | const type = this._type;
|
262 | if (change.type === 'set') {
|
263 |
|
264 |
|
265 | let convertedToMarkdownOrRaw = [];
|
266 | let convertedToCode = [];
|
267 |
|
268 | if (change.newValues.length === change.oldValues.length) {
|
269 |
|
270 | for (let i = 0; i < change.newValues.length; i++) {
|
271 | if (
|
272 | change.oldValues[i].type === type &&
|
273 | change.newValues[i].type !== type
|
274 | ) {
|
275 | convertedToMarkdownOrRaw.push(change.newValues[i]);
|
276 | } else if (
|
277 | change.oldValues[i].type !== type &&
|
278 | change.newValues[i].type === type
|
279 | ) {
|
280 | convertedToCode.push(change.newValues[i]);
|
281 | }
|
282 | }
|
283 | cellsAdded = convertedToCode;
|
284 | cellsRemoved = convertedToMarkdownOrRaw;
|
285 | }
|
286 | } else if (change.type == 'add') {
|
287 | cellsAdded = change.newValues.filter(
|
288 | cellModel => cellModel.type === type
|
289 | );
|
290 | }
|
291 |
|
292 |
|
293 |
|
294 |
|
295 |
|
296 | if (
|
297 | cellsRemoved.length ||
|
298 | cellsAdded.length ||
|
299 | change.type === 'set' ||
|
300 | change.type === 'move' ||
|
301 | change.type === 'remove'
|
302 | ) {
|
303 |
|
304 |
|
305 |
|
306 | await this.updateDocuments();
|
307 | }
|
308 |
|
309 | for (let cellModel of cellsAdded) {
|
310 | let cellWidget = this.widget.content.widgets.find(
|
311 | cell => cell.model.id === cellModel.id
|
312 | );
|
313 | if (!cellWidget) {
|
314 | console.warn(
|
315 | `Widget for added cell with ID: ${cellModel.id} not found!`
|
316 | );
|
317 | continue;
|
318 | }
|
319 |
|
320 |
|
321 | this._getCellEditor(cellWidget);
|
322 | }
|
323 | }
|
324 |
|
325 | |
326 |
|
327 |
|
328 | createVirtualDocument(): VirtualDocument {
|
329 | return new VirtualDocument({
|
330 | language: this.language,
|
331 | foreignCodeExtractors: this.options.foreignCodeExtractorsManager,
|
332 | path: this.documentPath,
|
333 | fileExtension: this.languageFileExtension,
|
334 |
|
335 | standalone: false,
|
336 |
|
337 | hasLspSupportedFile: false
|
338 | });
|
339 | }
|
340 |
|
341 | |
342 |
|
343 |
|
344 | protected language_info(): ILanguageInfoMetadata {
|
345 | return this._languageInfo;
|
346 | }
|
347 | |
348 |
|
349 |
|
350 |
|
351 |
|
352 | protected async initOnceReady(): Promise<void> {
|
353 | await untilReady(this.isReady.bind(this), -1);
|
354 | await this._updateLanguageInfo();
|
355 | this.initVirtual();
|
356 |
|
357 |
|
358 |
|
359 | this.connectDocument(this.virtualDocument!, false).catch(console.warn);
|
360 |
|
361 | this.widget.context.sessionContext.kernelChanged.connect(
|
362 | this.onKernelChanged,
|
363 | this
|
364 | );
|
365 |
|
366 | this.widget.content.activeCellChanged.connect(
|
367 | this._activeCellChanged,
|
368 | this
|
369 | );
|
370 | this._connectModelSignals(this.widget);
|
371 | this.editor.modelChanged.connect(notebook => {
|
372 |
|
373 |
|
374 |
|
375 |
|
376 | console.warn(
|
377 | 'Model changed, connecting cell change handler; this is not something we were expecting'
|
378 | );
|
379 | this._connectModelSignals(notebook);
|
380 | });
|
381 | }
|
382 |
|
383 | |
384 |
|
385 |
|
386 |
|
387 |
|
388 | private _connectModelSignals(notebook: NotebookPanel | Notebook) {
|
389 | if (notebook.model === null) {
|
390 | console.warn(
|
391 | `Model is missing for notebook ${notebook}, cannot connect cell changed signal!`
|
392 | );
|
393 | } else {
|
394 | notebook.model.cells.changed.connect(this.handleCellChange, this);
|
395 | }
|
396 | }
|
397 |
|
398 | |
399 |
|
400 |
|
401 | private async _updateLanguageInfo(): Promise<void> {
|
402 | const language_info = (
|
403 | await this.widget.context.sessionContext?.session?.kernel?.info
|
404 | )?.language_info;
|
405 | if (language_info) {
|
406 | this._languageInfo = language_info;
|
407 | } else {
|
408 | throw new Error(
|
409 | 'Language info update failed (no session, kernel, or info available)'
|
410 | );
|
411 | }
|
412 | }
|
413 |
|
414 | |
415 |
|
416 |
|
417 |
|
418 |
|
419 | private _activeCellChanged(notebook: Notebook, cell: Cell | null) {
|
420 | if (!cell || cell.model.type !== this._type) {
|
421 | return;
|
422 | }
|
423 |
|
424 | this._activeEditorChanged.emit({
|
425 | editor: this._getCellEditor(cell)
|
426 | });
|
427 | }
|
428 |
|
429 | |
430 |
|
431 |
|
432 |
|
433 | private _getCellAt(pos: IVirtualPosition): Cell {
|
434 | let editor = this.virtualDocument!.getEditorAtVirtualLine(pos);
|
435 | return this._editorToCell.get(editor)!;
|
436 | }
|
437 |
|
438 | |
439 |
|
440 |
|
441 |
|
442 |
|
443 |
|
444 | private _getCellEditor(cell: Cell): Document.IEditor {
|
445 | if (!this._cellToEditor.has(cell)) {
|
446 | const editor = Object.freeze({
|
447 | getEditor: () => cell.editor,
|
448 | ready: async () => {
|
449 | await cell.ready;
|
450 | return cell.editor!;
|
451 | },
|
452 | reveal: async () => {
|
453 | await this.editor.scrollToCell(cell);
|
454 | return cell.editor!;
|
455 | }
|
456 | });
|
457 |
|
458 | this._cellToEditor.set(cell, editor);
|
459 | this._editorToCell.set(editor, cell);
|
460 | cell.disposed.connect(() => {
|
461 | this._cellToEditor.delete(cell);
|
462 | this._editorToCell.delete(editor);
|
463 | this._editorRemoved.emit({
|
464 | editor
|
465 | });
|
466 | });
|
467 |
|
468 | this._editorAdded.emit({
|
469 | editor
|
470 | });
|
471 | }
|
472 |
|
473 | return this._cellToEditor.get(cell)!;
|
474 | }
|
475 |
|
476 | |
477 |
|
478 |
|
479 | private _editorToCell: Map<Document.IEditor, Cell>;
|
480 |
|
481 | |
482 |
|
483 |
|
484 | private _cellToEditor: WeakMap<Cell, Document.IEditor>;
|
485 |
|
486 | |
487 |
|
488 |
|
489 | private _languageInfo: ILanguageInfoMetadata;
|
490 |
|
491 | private _type: nbformat.CellType = 'code';
|
492 |
|
493 | private _readyDelegate = new PromiseDelegate<void>();
|
494 | }
|