UNPKG

@jupyterlab/notebook

Version:
379 lines 14.9 kB
// Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. import { IEditorMimeTypeService } from '@jupyterlab/codeeditor'; import { untilReady, VirtualDocument, WidgetLSPAdapter } from '@jupyterlab/lsp'; import { PromiseDelegate } from '@lumino/coreutils'; import { Signal } from '@lumino/signaling'; export class NotebookAdapter extends WidgetLSPAdapter { constructor(editorWidget, options) { super(editorWidget, options); this.editorWidget = editorWidget; this.options = options; this._type = 'code'; this._readyDelegate = new PromiseDelegate(); this._editorToCell = new Map(); this.editor = editorWidget.content; this._cellToEditor = new WeakMap(); this.isReady = this.isReady.bind(this); Promise.all([ this.widget.context.sessionContext.ready, this.connectionManager.ready ]) .then(async () => { await this.initOnceReady(); this._readyDelegate.resolve(); }) .catch(console.error); } /** * Get current path of the document. */ get documentPath() { return this.widget.context.path; } /** * Get the mime type of the document. */ get mimeType() { var _a; let mimeType; let languageMetadata = this.language_info(); if (!languageMetadata || !languageMetadata.mimetype) { // fallback to the code cell mime type if no kernel in use mimeType = this.widget.content.codeMimetype; } else { mimeType = languageMetadata.mimetype; } return Array.isArray(mimeType) ? (_a = mimeType[0]) !== null && _a !== void 0 ? _a : IEditorMimeTypeService.defaultMimeType : mimeType; } /** * Get the file extension of the document. */ get languageFileExtension() { let languageMetadata = this.language_info(); if (!languageMetadata || !languageMetadata.file_extension) { return; } return languageMetadata.file_extension.replace('.', ''); } /** * Get the inner HTMLElement of the document widget. */ get wrapperElement() { return this.widget.node; } /** * Get the list of CM editor with its type in the document, */ get editors() { if (this.isDisposed) { return []; } let notebook = this.widget.content; this._editorToCell.clear(); if (notebook.isDisposed) { return []; } return notebook.widgets.map(cell => { return { ceEditor: this._getCellEditor(cell), type: cell.model.type, value: cell.model.sharedModel.getSource() }; }); } /** * Get the activated CM editor. */ get activeEditor() { return this.editor.activeCell ? this._getCellEditor(this.editor.activeCell) : undefined; } /** * Promise that resolves once the adapter is initialized */ get ready() { return this._readyDelegate.promise; } /** * Get the index of editor from the cursor position in the virtual * document. * @deprecated This is error-prone and will be removed in JupyterLab 5.0, use `getEditorIndex()` with `virtualDocument.getEditorAtVirtualLine(position)` instead. * * @param position - the position of cursor in the virtual document. */ getEditorIndexAt(position) { let cell = this._getCellAt(position); let notebook = this.widget.content; return notebook.widgets.findIndex(otherCell => { return cell === otherCell; }); } /** * Get the index of input editor * * @param ceEditor - instance of the code editor */ getEditorIndex(ceEditor) { let cell = this._editorToCell.get(ceEditor); return this.editor.widgets.findIndex(otherCell => { return cell === otherCell; }); } /** * Get the wrapper of input editor. * * @param ceEditor - instance of the code editor */ getEditorWrapper(ceEditor) { let cell = this._editorToCell.get(ceEditor); return cell.node; } /** * Callback on kernel changed event, it will disconnect the * document with the language server and then reconnect. * * @param _session - Session context of changed kernel * @param change - Changed data */ async onKernelChanged(_session, change) { if (!change.newValue) { return; } try { // note: we need to wait until ready before updating language info const oldLanguageInfo = this._languageInfo; await untilReady(this.isReady, -1); await this._updateLanguageInfo(); const newLanguageInfo = this._languageInfo; if ((oldLanguageInfo === null || oldLanguageInfo === void 0 ? void 0 : oldLanguageInfo.name) != newLanguageInfo.name || (oldLanguageInfo === null || oldLanguageInfo === void 0 ? void 0 : oldLanguageInfo.mimetype) != (newLanguageInfo === null || newLanguageInfo === void 0 ? void 0 : newLanguageInfo.mimetype) || (oldLanguageInfo === null || oldLanguageInfo === void 0 ? void 0 : oldLanguageInfo.file_extension) != (newLanguageInfo === null || newLanguageInfo === void 0 ? void 0 : newLanguageInfo.file_extension)) { console.log(`Changed to ${this._languageInfo.name} kernel, reconnecting`); this.reloadConnection(); } else { console.log('Keeping old LSP connection as the new kernel uses the same language'); } } catch (err) { console.warn(err); // try to reconnect anyway this.reloadConnection(); } } /** * Dispose the widget. */ dispose() { if (this.isDisposed) { return; } this.widget.context.sessionContext.kernelChanged.disconnect(this.onKernelChanged, this); this.widget.content.activeCellChanged.disconnect(this._activeCellChanged, this); super.dispose(); // editors are needed for the parent dispose() to unbind signals, so they are the last to go this._editorToCell.clear(); Signal.clearData(this); } /** * Method to check if the notebook context is ready. */ isReady() { var _a; return (!this.widget.isDisposed && this.widget.context.isReady && this.widget.content.isVisible && this.widget.content.widgets.length > 0 && ((_a = this.widget.context.sessionContext.session) === null || _a === void 0 ? void 0 : _a.kernel) != null); } /** * Update the virtual document on cell changing event. * * @param cells - Observable list of changed cells * @param change - Changed data */ async handleCellChange(cells, change) { let cellsAdded = []; let cellsRemoved = []; const type = this._type; if (change.type === 'set') { // handling of conversions is important, because the editors get re-used and their handlers inherited, // so we need to clear our handlers from editors of e.g. markdown cells which previously were code cells. let convertedToMarkdownOrRaw = []; let convertedToCode = []; if (change.newValues.length === change.oldValues.length) { // during conversion the cells should not get deleted nor added for (let i = 0; i < change.newValues.length; i++) { if (change.oldValues[i].type === type && change.newValues[i].type !== type) { convertedToMarkdownOrRaw.push(change.newValues[i]); } else if (change.oldValues[i].type !== type && change.newValues[i].type === type) { convertedToCode.push(change.newValues[i]); } } cellsAdded = convertedToCode; cellsRemoved = convertedToMarkdownOrRaw; } } else if (change.type == 'add') { cellsAdded = change.newValues.filter(cellModel => cellModel.type === type); } // note: editorRemoved is not emitted for removal of cells by change of type 'remove' (but only during cell type conversion) // because there is no easy way to get the widget associated with the removed cell(s) - because it is no // longer in the notebook widget list! It would need to be tracked on our side, but it is not necessary // as (except for a tiny memory leak) it should not impact the functionality in any way if (cellsRemoved.length || cellsAdded.length || change.type === 'set' || change.type === 'move' || change.type === 'remove') { // in contrast to the file editor document which can be only changed by the modification of the editor content, // the notebook document can also get modified by a change in the number or arrangement of editors themselves; // for this reason each change has to trigger documents update (so that LSP mirror is in sync). await this.updateDocuments(); } for (let cellModel of cellsAdded) { let cellWidget = this.widget.content.widgets.find(cell => cell.model.id === cellModel.id); if (!cellWidget) { console.warn(`Widget for added cell with ID: ${cellModel.id} not found!`); continue; } // Add editor to the mapping if needed this._getCellEditor(cellWidget); } } /** * Generate the virtual document associated with the document. */ createVirtualDocument() { return new VirtualDocument({ language: this.language, foreignCodeExtractors: this.options.foreignCodeExtractorsManager, path: this.documentPath, fileExtension: this.languageFileExtension, // notebooks are continuous, each cell is dependent on the previous one standalone: false, // notebooks are not supported by LSP servers hasLspSupportedFile: false }); } /** * Get the metadata of notebook. */ language_info() { return this._languageInfo; } /** * Initialization function called once the editor and the LSP connection * manager is ready. This function will create the virtual document and * connect various signals. */ async initOnceReady() { await untilReady(this.isReady.bind(this), -1); await this._updateLanguageInfo(); this.initVirtual(); // connect the document, but do not open it as the adapter will handle this // after registering all features this.connectDocument(this.virtualDocument, false).catch(console.warn); this.widget.context.sessionContext.kernelChanged.connect(this.onKernelChanged, this); this.widget.content.activeCellChanged.connect(this._activeCellChanged, this); this._connectModelSignals(this.widget); this.editor.modelChanged.connect(notebook => { // note: this should not usually happen; // there is no default action that would trigger this, // its just a failsafe in case if another extension decides // to swap the notebook model console.warn('Model changed, connecting cell change handler; this is not something we were expecting'); this._connectModelSignals(notebook); }); } /** * Connect the cell changed event to its handler * * @param notebook - The notebook that emitted event. */ _connectModelSignals(notebook) { if (notebook.model === null) { console.warn(`Model is missing for notebook ${notebook}, cannot connect cell changed signal!`); } else { notebook.model.cells.changed.connect(this.handleCellChange, this); } } /** * Update the stored language info with the one from the notebook. */ async _updateLanguageInfo() { var _a, _b, _c, _d; const language_info = (_d = (await ((_c = (_b = (_a = this.widget.context.sessionContext) === null || _a === void 0 ? void 0 : _a.session) === null || _b === void 0 ? void 0 : _b.kernel) === null || _c === void 0 ? void 0 : _c.info))) === null || _d === void 0 ? void 0 : _d.language_info; if (language_info) { this._languageInfo = language_info; } else { throw new Error('Language info update failed (no session, kernel, or info available)'); } } /** * Handle the cell changed event * @param notebook - The notebook that emitted event * @param cell - Changed cell. */ _activeCellChanged(notebook, cell) { if (!cell || cell.model.type !== this._type) { return; } this._activeEditorChanged.emit({ editor: this._getCellEditor(cell) }); } /** * Get the cell at the cursor position of the virtual document. * @param pos - Position in the virtual document. */ _getCellAt(pos) { let editor = this.virtualDocument.getEditorAtVirtualLine(pos); return this._editorToCell.get(editor); } /** * Get the cell editor and add new ones to the mappings. * * @param cell Cell widget * @returns Cell editor accessor */ _getCellEditor(cell) { if (!this._cellToEditor.has(cell)) { const editor = Object.freeze({ getEditor: () => cell.editor, ready: async () => { await cell.ready; return cell.editor; }, reveal: async () => { await this.editor.scrollToCell(cell); return cell.editor; } }); this._cellToEditor.set(cell, editor); this._editorToCell.set(editor, cell); cell.disposed.connect(() => { this._cellToEditor.delete(cell); this._editorToCell.delete(editor); this._editorRemoved.emit({ editor }); }); this._editorAdded.emit({ editor }); } return this._cellToEditor.get(cell); } } //# sourceMappingURL=notebooklspadapter.js.map